Add requestion timeout cli option

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-02-05 03:21:16 +0100
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-02-05 03:21:16 +0100
Commit 96812ba395170d5551634e6351a93eda2fa57cf1 (patch)
-rw-r--r-- README.md 2
-rw-r--r-- main.go 42
2 files changed, 25 insertions, 19 deletions
diff --git a/README.md b/README.md
...
61
*   `-req`: Comma-separated list of request names to execute.
61
*   `-req`: Comma-separated list of request names to execute.
62
*   `-group`: The name of a request group to execute.
62
*   `-group`: The name of a request group to execute.
63
*   `-headers`: Show response headers in the output.
63
*   `-headers`: Show response headers in the output.
  
64
*   `-timeout`: Request timeout duration (default: 10s).
  
65
*   `-state`: Path to state file.
64
  
66
  
65
## Core Concepts
67
## Core Concepts
66
  
68
  
...
diff --git a/main.go b/main.go
...
79
	reqNames := flag.String("req", "", "Comma-separated list of request names to execute")
79
	reqNames := flag.String("req", "", "Comma-separated list of request names to execute")
80
	groupName := flag.String("group", "", "Group to execute")
80
	groupName := flag.String("group", "", "Group to execute")
81
	showHeaders := flag.Bool("headers", false, "Display response headers")
81
	showHeaders := flag.Bool("headers", false, "Display response headers")
  
82
	timeout := flag.Duration("timeout", 10*time.Second, "Request timeout duration")
82
	flag.Parse()
83
	flag.Parse()
83
  
84
  
84
	if filePath == "" {
85
	if filePath == "" {
...
87
		os.Exit(1)
88
		os.Exit(1)
88
	}
89
	}
89
  
90
  
90
	runner, err := NewRunner(filePath, envName, statePath)
91
	runner, err := NewRunner(filePath, envName, statePath, *timeout)
91
	if err != nil {
92
	if err != nil {
92
		log.Fatalf("Error: %v", err)
93
		log.Fatalf("Error: %v", err)
93
	}
94
	}
...
112
}
113
}
113
  
114
  
114
// NewRunner initializes a new Hepi runner.
115
// NewRunner initializes a new Hepi runner.
115
func NewRunner(filePath, envName, stateFile string) (*Runner, error) {
116
func NewRunner(filePath, envName, stateFile string, timeout time.Duration) (*Runner, error) {
116
	data, err := os.ReadFile(filePath)
117
	data, err := os.ReadFile(filePath)
117
	if err != nil {
118
	if err != nil {
118
		return nil, fmt.Errorf("failed to read file: %w", err)
119
		return nil, fmt.Errorf("%sfailed to read file: %w%s", colorRed, err, colorReset)
119
	}
120
	}
120
  
121
  
121
	var config Config
122
	var config Config
122
	if err := yaml.Unmarshal(data, &config); err != nil {
123
	if err := yaml.Unmarshal(data, &config); err != nil {
123
		return nil, fmt.Errorf("failed to parse YAML: %w", err)
124
		return nil, fmt.Errorf("%sfailed to parse YAML: %w%s", colorRed, err, colorReset)
124
	}
125
	}
125
  
126
  
126
	if config.Environments.Kind != yaml.MappingNode {
127
	if config.Environments.Kind != yaml.MappingNode {
127
		return nil, fmt.Errorf("environments must be a mapping")
128
		return nil, fmt.Errorf("%senvironments must be a mapping%s", colorRed, colorReset)
128
	}
129
	}
129
  
130
  
130
	selectedEnvName := envName
131
	selectedEnvName := envName
...
138
			availableEnvs = append(availableEnvs, name)
139
			availableEnvs = append(availableEnvs, name)
139
			if name == envName {
140
			if name == envName {
140
				if err := config.Environments.Content[i+1].Decode(&selectedEnv); err != nil {
141
				if err := config.Environments.Content[i+1].Decode(&selectedEnv); err != nil {
141
					return nil, fmt.Errorf("failed to decode environment %q: %w", envName, err)
142
					return nil, fmt.Errorf("%sfailed to decode environment %q: %w%s", colorRed, envName, err, colorReset)
142
				}
143
				}
143
				found = true
144
				found = true
144
				break
145
				break
145
			}
146
			}
146
		}
147
		}
147
		if !found {
148
		if !found {
148
			return nil, fmt.Errorf("environment %q not found\nAvailable environments:\n- %s", envName, strings.Join(availableEnvs, "\n- "))
149
			return nil, fmt.Errorf("%senvironment %q not found\nAvailable environments:\n- %s%s", colorRed, envName, strings.Join(availableEnvs, "\n- "), colorReset)
149
		}
150
		}
150
	}
151
	}
151
  
152
  
...
155
		Environment: selectedEnv,
156
		Environment: selectedEnv,
156
		State:       loadState(selectedEnvName, stateFile),
157
		State:       loadState(selectedEnvName, stateFile),
157
		StateFile:   stateFile,
158
		StateFile:   stateFile,
158
		HTTPClient:  &http.Client{Timeout: 10 * time.Second},
159
		HTTPClient:  &http.Client{Timeout: timeout},
159
	}, nil
160
	}, nil
160
}
161
}
161
  
162
  
...
163
func (r *Runner) ExecuteGroup(groupName string) error {
164
func (r *Runner) ExecuteGroup(groupName string) error {
164
	group, ok := r.Config.Groups[groupName]
165
	group, ok := r.Config.Groups[groupName]
165
	if !ok {
166
	if !ok {
166
		return fmt.Errorf("group %q not found", groupName)
167
		return fmt.Errorf("%sgroup %q not found%s", colorRed, groupName, colorReset)
167
	}
168
	}
168
  
169
  
169
	for _, reqName := range group {
170
	for _, reqName := range group {
...
187
  
188
  
188
	requestsNode := r.Config.Requests
189
	requestsNode := r.Config.Requests
189
	if requestsNode.Kind != yaml.MappingNode {
190
	if requestsNode.Kind != yaml.MappingNode {
190
		return fmt.Errorf("requests must be a mapping")
191
		return fmt.Errorf("%srequests must be a mapping%s", colorRed, colorReset)
191
	}
192
	}
192
  
193
  
193
	for i := 0; i < len(requestsNode.Content); i += 2 {
194
	for i := 0; i < len(requestsNode.Content); i += 2 {
...
202
  
203
  
203
		var req Request
204
		var req Request
204
		if err := valNode.Decode(&req); err != nil {
205
		if err := valNode.Decode(&req); err != nil {
205
			return fmt.Errorf("failed to decode request %q: %w", name, err)
206
			return fmt.Errorf("%sfailed to decode request %q: %w%s", colorRed, name, err, colorReset)
206
		}
207
		}
207
  
208
  
208
		fmt.Printf("\n%s--- %s[%s]%s %s ---%s\n", colorBold, colorCyan, name, colorReset, req.Description, colorReset)
209
		fmt.Printf("\n%s--- %s[%s]%s %s ---%s\n", colorBold, colorCyan, name, colorReset, req.Description, colorReset)
209
		if err := r.executeRequest(name, req); err != nil {
210
		if err := r.executeRequest(name, req); err != nil {
210
			log.Printf("Warning: request %q failed: %v", name, err)
211
			return err
211
		}
212
		}
212
	}
213
	}
213
  
214
  
...
218
		}
219
		}
219
	}
220
	}
220
	if len(missing) > 0 {
221
	if len(missing) > 0 {
221
		return fmt.Errorf("requests not found: %s", strings.Join(missing, ", "))
222
		return fmt.Errorf("%srequests not found: %s%s", colorRed, strings.Join(missing, ", "), colorReset)
222
	}
223
	}
223
  
224
  
224
	return nil
225
	return nil
...
231
	if req.Params != nil {
232
	if req.Params != nil {
232
		u, err := url.Parse(rawURL)
233
		u, err := url.Parse(rawURL)
233
		if err != nil {
234
		if err != nil {
234
			return fmt.Errorf("failed to parse URL %q: %w", rawURL, err)
235
			return fmt.Errorf("%sfailed to parse URL %q: %w%s", colorRed, rawURL, err, colorReset)
235
		}
236
		}
236
		q := u.Query()
237
		q := u.Query()
237
		params := r.substituteMap(req.Params)
238
		params := r.substituteMap(req.Params)
...
281
			substitutedPath := r.substitute(path)
282
			substitutedPath := r.substitute(path)
282
			file, err := os.Open(substitutedPath)
283
			file, err := os.Open(substitutedPath)
283
			if err != nil {
284
			if err != nil {
284
				return fmt.Errorf("failed to open file %q: %w", substitutedPath, err)
285
				return fmt.Errorf("%sfailed to open file %q: %w%s", colorRed, substitutedPath, err, colorReset)
285
			}
286
			}
286
			defer file.Close()
287
			defer file.Close()
287
  
288
  
288
			part, err := writer.CreateFormFile(field, substitutedPath)
289
			part, err := writer.CreateFormFile(field, substitutedPath)
289
			if err != nil {
290
			if err != nil {
290
				return fmt.Errorf("failed to create form file for %q: %w", field, err)
291
				return fmt.Errorf("%sfailed to create form file for %q: %w%s", colorRed, field, err, colorReset)
291
			}
292
			}
292
			_, _ = io.Copy(part, file)
293
			_, _ = io.Copy(part, file)
293
		}
294
		}
...
307
  
308
  
308
	httpReq, err := http.NewRequest(req.Method, rawURL, bodyReader)
309
	httpReq, err := http.NewRequest(req.Method, rawURL, bodyReader)
309
	if err != nil {
310
	if err != nil {
310
		return fmt.Errorf("failed to create HTTP request: %w", err)
311
		return fmt.Errorf("%sfailed to create HTTP request: %w%s", colorRed, err, colorReset)
311
	}
312
	}
312
  
313
  
313
	if contentType != "" {
314
	if contentType != "" {
...
321
	startTime := time.Now()
322
	startTime := time.Now()
322
	resp, err := r.HTTPClient.Do(httpReq)
323
	resp, err := r.HTTPClient.Do(httpReq)
323
	if err != nil {
324
	if err != nil {
324
		return fmt.Errorf("request failed: %w", err)
325
		if os.IsTimeout(err) {
  
326
			return fmt.Errorf("%srequest timed out after %v%s", colorRed, r.HTTPClient.Timeout, colorReset)
  
327
		}
  
328
		return fmt.Errorf("%srequest failed: %w%s", colorRed, err, colorReset)
325
	}
329
	}
326
	duration := time.Since(startTime)
330
	duration := time.Since(startTime)
327
	defer resp.Body.Close()
331
	defer resp.Body.Close()
...
344
  
348
  
345
	respData, err := io.ReadAll(resp.Body)
349
	respData, err := io.ReadAll(resp.Body)
346
	if err != nil {
350
	if err != nil {
347
		return fmt.Errorf("failed to read response body: %w", err)
351
		return fmt.Errorf("%sfailed to read response body: %w%s", colorRed, err, colorReset)
348
	}
352
	}
349
  
353
  
350
	if len(respData) > 0 {
354
	if len(respData) > 0 {
...