Skip to content

Commit 87f7e36

Browse files
authored
Merge pull request #109 from infraspecdev/feat/disable-redirects
feat: add option to disable redirects
2 parents c08d674 + c06c994 commit 87f7e36

8 files changed

Lines changed: 243 additions & 110 deletions

File tree

cmd/config.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ func init() {
2323
}
2424

2525
type RunConfig struct {
26-
Target string
27-
ParsedTarget *url.URL
28-
Requests int
29-
Concurrency int
30-
Timeout time.Duration
31-
Duration time.Duration
32-
Method string
33-
Body string
34-
BodyFile string
35-
Headers []string
36-
Verbose bool
26+
Target string
27+
ParsedTarget *url.URL
28+
Requests int
29+
Concurrency int
30+
Timeout time.Duration
31+
Duration time.Duration
32+
Method string
33+
Body string
34+
BodyFile string
35+
Headers []string
36+
Verbose bool
37+
DisableRedirects bool
3738
}
3839

3940
var validMethods = map[string]bool{
@@ -99,14 +100,15 @@ func (c *RunConfig) Validate() error {
99100

100101
func (c *RunConfig) ToHTTPConfig() httpclient.Config {
101102
return httpclient.Config{
102-
Target: c.Target,
103-
Requests: c.Requests,
104-
Concurrency: c.Concurrency,
105-
Timeout: c.Timeout,
106-
Duration: c.Duration,
107-
Method: c.Method,
108-
Body: c.Body,
109-
Headers: c.Headers,
110-
Verbose: c.Verbose,
103+
Target: c.Target,
104+
Requests: c.Requests,
105+
Concurrency: c.Concurrency,
106+
Timeout: c.Timeout,
107+
Duration: c.Duration,
108+
Method: c.Method,
109+
Body: c.Body,
110+
Headers: c.Headers,
111+
Verbose: c.Verbose,
112+
DisableRedirects: c.DisableRedirects,
111113
}
112114
}

cmd/config_test.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -300,29 +300,31 @@ func TestRunConfig_Validate(t *testing.T) {
300300

301301
func TestRunConfig_ToHTTPConfig(t *testing.T) {
302302
rc := RunConfig{
303-
Target: "https://example.com/api",
304-
Requests: 100,
305-
Concurrency: 10,
306-
Timeout: 5 * time.Second,
307-
Duration: 30 * time.Second,
308-
Method: "POST",
309-
Body: `{"key":"value"}`,
310-
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
311-
Verbose: true,
303+
Target: "https://example.com/api",
304+
Requests: 100,
305+
Concurrency: 10,
306+
Timeout: 5 * time.Second,
307+
Duration: 30 * time.Second,
308+
Method: "POST",
309+
Body: `{"key":"value"}`,
310+
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
311+
Verbose: true,
312+
DisableRedirects: true,
312313
}
313314

314315
got := rc.ToHTTPConfig()
315316

316317
want := httpclient.Config{
317-
Target: "https://example.com/api",
318-
Requests: 100,
319-
Concurrency: 10,
320-
Timeout: 5 * time.Second,
321-
Duration: 30 * time.Second,
322-
Method: "POST",
323-
Body: `{"key":"value"}`,
324-
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
325-
Verbose: true,
318+
Target: "https://example.com/api",
319+
Requests: 100,
320+
Concurrency: 10,
321+
Timeout: 5 * time.Second,
322+
Duration: 30 * time.Second,
323+
Method: "POST",
324+
Body: `{"key":"value"}`,
325+
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
326+
Verbose: true,
327+
DisableRedirects: true,
326328
}
327329

328330
if got.Target != want.Target {
@@ -331,6 +333,9 @@ func TestRunConfig_ToHTTPConfig(t *testing.T) {
331333
if got.Verbose != want.Verbose {
332334
t.Errorf("Verbose: got %v, want %v", got.Verbose, want.Verbose)
333335
}
336+
if got.DisableRedirects != want.DisableRedirects {
337+
t.Errorf("DisableRedirects: got %v, want %v", got.DisableRedirects, want.DisableRedirects)
338+
}
334339
if got.Requests != want.Requests {
335340
t.Errorf("Requests: got %d, want %d", got.Requests, want.Requests)
336341
}

cmd/configfile.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ import (
1313
)
1414

1515
type fileConfig struct {
16-
Target *string `json:"target" yaml:"target"`
17-
Requests *int `json:"requests" yaml:"requests"`
18-
Concurrency *int `json:"concurrency" yaml:"concurrency"`
19-
Timeout *string `json:"timeout" yaml:"timeout"`
20-
Duration *string `json:"duration" yaml:"duration"`
21-
Method *string `json:"method" yaml:"method"`
22-
Body *string `json:"body" yaml:"body"`
23-
BodyFile *string `json:"body_file" yaml:"body_file"`
24-
Headers []string `json:"headers" yaml:"headers"`
25-
Verbose *bool `json:"verbose" yaml:"verbose"`
16+
Target *string `json:"target" yaml:"target"`
17+
Requests *int `json:"requests" yaml:"requests"`
18+
Concurrency *int `json:"concurrency" yaml:"concurrency"`
19+
Timeout *string `json:"timeout" yaml:"timeout"`
20+
Duration *string `json:"duration" yaml:"duration"`
21+
Method *string `json:"method" yaml:"method"`
22+
Body *string `json:"body" yaml:"body"`
23+
BodyFile *string `json:"body_file" yaml:"body_file"`
24+
Headers []string `json:"headers" yaml:"headers"`
25+
Verbose *bool `json:"verbose" yaml:"verbose"`
26+
DisableRedirects *bool `json:"disable_redirects" yaml:"disable_redirects"`
2627
}
2728

2829
func loadConfig(path string) (*fileConfig, error) {
@@ -130,5 +131,9 @@ func mergeConfig(file *fileConfig, cli RunConfig, changed map[string]bool) (RunC
130131
merged.Verbose = *file.Verbose
131132
}
132133

134+
if file.DisableRedirects != nil && !changed["disable-redirects"] {
135+
merged.DisableRedirects = *file.DisableRedirects
136+
}
137+
133138
return merged, nil
134139
}

cmd/configfile_merge_test.go

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,16 @@ func TestMergeConfig_NilFileConfig(t *testing.T) {
3333

3434
func TestMergeConfig_FileValuesUsedWhenCLIUnchanged(t *testing.T) {
3535
file := &fileConfig{
36-
Target: strPtr("https://file.example.com"),
37-
Requests: intPtr(200),
38-
Concurrency: intPtr(20),
39-
Timeout: strPtr("30s"),
40-
Duration: strPtr("1m"),
41-
Method: strPtr("POST"),
42-
Body: strPtr(`{"data":"test"}`),
43-
Headers: []string{"X-From: file"},
44-
Verbose: func() *bool { b := true; return &b }(),
36+
Target: strPtr("https://file.example.com"),
37+
Requests: intPtr(200),
38+
Concurrency: intPtr(20),
39+
Timeout: strPtr("30s"),
40+
Duration: strPtr("1m"),
41+
Method: strPtr("POST"),
42+
Body: strPtr(`{"data":"test"}`),
43+
Headers: []string{"X-From: file"},
44+
Verbose: func() *bool { b := true; return &b }(),
45+
DisableRedirects: func() *bool { b := true; return &b }(),
4546
}
4647

4748
cli := RunConfig{
@@ -64,6 +65,9 @@ func TestMergeConfig_FileValuesUsedWhenCLIUnchanged(t *testing.T) {
6465
if got.Verbose != true {
6566
t.Errorf("Verbose: got %v, want true", got.Verbose)
6667
}
68+
if got.DisableRedirects != true {
69+
t.Errorf("DisableRedirects: got %v, want true", got.DisableRedirects)
70+
}
6771
if got.Requests != 200 {
6872
t.Errorf("Requests: got %d, want 200", got.Requests)
6973
}
@@ -89,32 +93,35 @@ func TestMergeConfig_FileValuesUsedWhenCLIUnchanged(t *testing.T) {
8993

9094
func TestMergeConfig_CLIOverridesFileValues(t *testing.T) {
9195
file := &fileConfig{
92-
Target: strPtr("https://file.example.com"),
93-
Requests: intPtr(200),
94-
Concurrency: intPtr(20),
95-
Timeout: strPtr("30s"),
96-
Method: strPtr("POST"),
97-
Body: strPtr(`{"data":"file"}`),
98-
Headers: []string{"X-From: file"},
96+
Target: strPtr("https://file.example.com"),
97+
Requests: intPtr(200),
98+
Concurrency: intPtr(20),
99+
Timeout: strPtr("30s"),
100+
Method: strPtr("POST"),
101+
Body: strPtr(`{"data":"file"}`),
102+
Headers: []string{"X-From: file"},
103+
DisableRedirects: func() *bool { b := true; return &b }(),
99104
}
100105

101106
cli := RunConfig{
102-
Target: "https://cli.example.com",
103-
Requests: 500,
104-
Concurrency: 50,
105-
Timeout: 5 * time.Second,
106-
Method: "PUT",
107-
Body: `{"data":"cli"}`,
108-
Headers: []string{"X-From: cli"},
107+
Target: "https://cli.example.com",
108+
Requests: 500,
109+
Concurrency: 50,
110+
Timeout: 5 * time.Second,
111+
Method: "PUT",
112+
Body: `{"data":"cli"}`,
113+
Headers: []string{"X-From: cli"},
114+
DisableRedirects: false,
109115
}
110116

111117
changed := map[string]bool{
112-
"requests": true,
113-
"concurrency": true,
114-
"timeout": true,
115-
"method": true,
116-
"body": true,
117-
"header": true,
118+
"requests": true,
119+
"concurrency": true,
120+
"timeout": true,
121+
"method": true,
122+
"body": true,
123+
"header": true,
124+
"disable-redirects": true,
118125
}
119126

120127
got, err := mergeConfig(file, cli, changed)
@@ -143,6 +150,9 @@ func TestMergeConfig_CLIOverridesFileValues(t *testing.T) {
143150
if len(got.Headers) != 1 || got.Headers[0] != "X-From: cli" {
144151
t.Errorf("Headers: got %v, want [X-From: cli] (CLI override)", got.Headers)
145152
}
153+
if got.DisableRedirects != false {
154+
t.Errorf("DisableRedirects: got %v, want false (CLI override)", got.DisableRedirects)
155+
}
146156
}
147157

148158
func TestMergeConfig_PartialFileConfig(t *testing.T) {

cmd/run.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Latency Percentiles:
5757
bodyFile, _ := f.GetString("body-file")
5858
headers, _ := f.GetStringArray("header")
5959
verbose, _ := f.GetBool("verbose")
60+
disableRedirects, _ := f.GetBool("disable-redirects")
6061
outputFormat, _ := f.GetString("output")
6162

6263
if outputFormat != "text" && outputFormat != "json" {
@@ -69,16 +70,17 @@ Latency Percentiles:
6970
}
7071

7172
cliConfig := RunConfig{
72-
Target: target,
73-
Requests: requests,
74-
Concurrency: concurrency,
75-
Timeout: timeout,
76-
Duration: duration,
77-
Method: strings.ToUpper(method),
78-
Body: body,
79-
BodyFile: bodyFile,
80-
Headers: headers,
81-
Verbose: verbose,
73+
Target: target,
74+
Requests: requests,
75+
Concurrency: concurrency,
76+
Timeout: timeout,
77+
Duration: duration,
78+
Method: strings.ToUpper(method),
79+
Body: body,
80+
BodyFile: bodyFile,
81+
Headers: headers,
82+
Verbose: verbose,
83+
DisableRedirects: disableRedirects,
8284
}
8385

8486
changed := make(map[string]bool)
@@ -142,6 +144,7 @@ Latency Percentiles:
142144
cmd.Flags().StringArrayP("header", "H", []string{}, "HTTP header in 'Key: Value' format (can be repeated)")
143145
cmd.Flags().StringP("config", "f", "", "Path to configuration file (JSON/YAML)")
144146
cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output")
147+
cmd.Flags().Bool("disable-redirects", false, "Do not follow HTTP redirects")
145148
cmd.Flags().StringP("output", "o", "text", "Output format (text or json)")
146149

147150
return cmd

cmd/run_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func TestFlagRegistration(t *testing.T) {
2727
{"header", "H", []string{}},
2828
{"config", "f", ""},
2929
{"verbose", "v", false},
30+
{"disable-redirects", "", false},
3031
{"output", "o", "text"},
3132
}
3233

internal/httpclient/client.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,34 @@ import (
1616
"github.com/infraspecdev/goperf/internal/stats"
1717
)
1818

19-
func NewHTTPClient(concurrency int) *http.Client {
20-
return &http.Client{
19+
func NewHTTPClient(concurrency int, disableRedirects bool) *http.Client {
20+
client := &http.Client{
2121
Transport: &http.Transport{
2222
MaxIdleConnsPerHost: concurrency,
2323
DisableCompression: true,
2424
},
2525
}
26+
if disableRedirects {
27+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
28+
return http.ErrUseLastResponse
29+
}
30+
}
31+
return client
2632
}
2733

2834
type Config struct {
29-
Target string
30-
Requests int
31-
Concurrency int
32-
Timeout time.Duration
33-
Duration time.Duration
34-
Method string
35-
Body string
36-
Headers []string
37-
Verbose bool
38-
Version string
39-
Stderr io.Writer
35+
Target string
36+
Requests int
37+
Concurrency int
38+
Timeout time.Duration
39+
Duration time.Duration
40+
Method string
41+
Body string
42+
Headers []string
43+
Verbose bool
44+
Version string
45+
Stderr io.Writer
46+
DisableRedirects bool
4047
}
4148

4249
type HTTPDoer interface {
@@ -126,7 +133,7 @@ func formatErrorForStats(err error) string {
126133
return err.Error()
127134
}
128135

129-
func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verboseWriter io.Writer, statusCode int, latency time.Duration, err error) {
136+
func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verboseWriter io.Writer, statusCode int, latency time.Duration, err error, disableRedirects bool) {
130137
if err != nil && ctx.Err() != nil && errors.Is(err, ctx.Err()) {
131138
return
132139
}
@@ -139,7 +146,7 @@ func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verbos
139146
}
140147
if err != nil {
141148
recorder.RecordErrorResult(statusCode, formatErrorForStats(err))
142-
} else if statusCode >= 200 && statusCode < 300 {
149+
} else if (statusCode >= 200 && statusCode < 300) || (disableRedirects && statusCode >= 300 && statusCode < 400) {
143150
if statusCode > 0 {
144151
recorder.RecordStatusCode(statusCode)
145152
}
@@ -150,7 +157,7 @@ func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verbos
150157
}
151158

152159
func Run(ctx context.Context, cfg Config) *stats.HistogramRecorder {
153-
client := NewHTTPClient(cfg.Concurrency)
160+
client := NewHTTPClient(cfg.Concurrency, cfg.DisableRedirects)
154161
recorder := stats.NewHistogramRecorder(cfg.Timeout)
155162

156163
var verboseWriter io.Writer
@@ -224,7 +231,7 @@ func Run(ctx context.Context, cfg Config) *stats.HistogramRecorder {
224231
}
225232
}
226233
statusCode, d, err := MakeRequest(reqCtx, client, cfg)
227-
recordResult(reqCtx, recorder, verboseWriter, statusCode, d, err)
234+
recordResult(reqCtx, recorder, verboseWriter, statusCode, d, err, cfg.DisableRedirects)
228235
}
229236
}()
230237
}

0 commit comments

Comments
 (0)