Skip to content

Commit a161b76

Browse files
Rahul TyagiRahul Tyagi
authored andcommitted
feat: v0.2 — response insight (A) and request power (B)
Implements milestone v0.2 from docs/SPEC.md. Backend - Introduce structured RequestSpec (method, url, headers, body, settings) and a new Send(spec) bound method. Builds a per-request http.Client from Settings: timeout, follow-redirects (CheckRedirect), TLS verify. - Extend RequestResult with Status, StatusText, DurationMs, SizeBytes. - Non-JSON response bodies are now returned verbatim instead of dropped on json.Indent failure. - Keep MakeRequest as a thin wrapper around Send for backward compat. - Add SaveTextFile binding (used by Save body) via runtime.SaveFileDialog. - New send_test.go covering: status/text round-trip, duration & size, timeout, TLS verify off, redirect-following off, JSON pretty + plain. Frontend - Response insight (A): status chip colored by HTTP class, duration, size formatter (B/KB/MB), Pretty/Raw body toggle, search-in-response with match count and <mark> highlighting, Copy & Save body buttons. - Request power (B): - Query params builder, two-way synced with the URL field. - Auth: None / Bearer / Basic / API key (header or query target). - Body types: None / JSON (with Format button) / Form / Raw. - Per-request Settings: timeout, follow redirects, verify TLS. - Copy as cURL. - Request editor reorganized into one tabbed panel (Headers / Params / Body / Auth / Settings). - New shared helpers: kv (key/value rows), formatters, auth, settings, url-sync, curl-builder. Reusable KVRow replaces the per-purpose RequestHeader component (deleted). - Wails bindings regenerated. Deferred to v0.3 per spec: multipart / file uploads, iframe HTML preview.
1 parent da79b8d commit a161b76

21 files changed

Lines changed: 1649 additions & 236 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Git hooks (`.githooks/pre-commit`, `.githooks/pre-push`) that enforce formatting and
2424
mirror CI checks locally; enable with `make hooks`.
2525
- `Makefile` with `hooks`, `setup`, `lint`, `test`, and `build` targets.
26+
- **v0.2 — Response insight (bundle A):** response status code + status text, request
27+
duration, and body size shown next to the Response heading; status chip is colored by
28+
HTTP class.
29+
- **v0.2 — Body Pretty/Raw toggle** and **search-in-response** with match count, plus
30+
**Copy** and **Save** buttons (the latter via a new `SaveTextFile` Go binding using a
31+
native save dialog).
32+
- **v0.2 — Request power (bundle B):** query-param builder synced two-way with the URL
33+
field; **Auth** section (None / Bearer / Basic / API key with header-or-query target);
34+
**Body** selector (None / JSON with a Format button / Form / Raw); per-request
35+
**Settings** (timeout, follow-redirects, verify TLS); **Copy as cURL** action that
36+
mirrors cURL import.
2637

2738
### Changed
39+
- Backend: introduced `RequestSpec` and a new `Send(spec) RequestResult` bound method
40+
that builds a per-request `http.Client` from settings (timeout, redirects, TLS verify).
41+
`MakeRequest` is retained as a thin wrapper for backward compatibility.
42+
- `RequestResult` now includes `Status`, `StatusText`, `DurationMs`, and `SizeBytes`.
43+
- Non-JSON response bodies are returned verbatim instead of swallowed when JSON pretty-
44+
printing fails.
45+
- Request editor reorganized into tabbed sections (Headers / Params / Body / Auth /
46+
Settings) within a single panel.
2847
- cURL import now populates the header rows and request body of the active tab.
2948
- The response view now follows the active request tab.
3049
- The "add header" action moved next to the Request Headers title.
3150

51+
### Deferred (planned for v0.3)
52+
- Multipart / file-upload body type.
53+
- Iframe HTML preview tab for the response body.
54+
3255
### Fixed
3356
- `//go:embed all:frontend/dist` caused a compile failure on clean checkouts because
3457
`frontend/dist/` was gitignored; fixed by committing a `.gitkeep` and updating

app.go

Lines changed: 138 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"bytes"
55
"context"
6+
"crypto/tls"
67
"encoding/json"
78
"io"
89
"net/http"
@@ -35,7 +36,7 @@ func (a *App) startup(ctx context.Context) {
3536
a.ctx = ctx
3637
}
3738

38-
func makeRequest(c *http.Client,
39+
func doRequest(c *http.Client,
3940
r *http.Request) ([]byte, *http.Response, error) {
4041
resp, err := c.Do(r)
4142
if err != nil {
@@ -62,6 +63,29 @@ func HeadersToStr(h *http.Header) string {
6263
return result
6364
}
6465

66+
// BodySpec describes the request body type and content.
67+
type BodySpec struct {
68+
Type string `json:"type"` // "none" | "json" | "form" | "raw"
69+
Raw string `json:"raw"` // raw/json text or pre-encoded form body
70+
}
71+
72+
// RequestSettings contains per-request client settings.
73+
type RequestSettings struct {
74+
TimeoutMs int `json:"timeoutMs"` // 0 = use default (50s)
75+
FollowRedirects bool `json:"followRedirects"` // true = follow
76+
VerifyTLS bool `json:"verifyTLS"` // true = verify
77+
}
78+
79+
// RequestSpec is the structured request used by Send().
80+
type RequestSpec struct {
81+
Method string `json:"method"`
82+
URL string `json:"url"`
83+
Headers map[string]string `json:"headers"`
84+
Body BodySpec `json:"body"`
85+
Settings RequestSettings `json:"settings"`
86+
}
87+
88+
// RequestResult is the response shape returned to the UI.
6589
type RequestResult struct {
6690
Method string `json:"Method"`
6791
URL string `json:"URL"`
@@ -70,6 +94,97 @@ type RequestResult struct {
7094
Body string `json:"Body"`
7195
HeadersStr string `json:"HeadersStr"`
7296
Error string `json:"Error"`
97+
Status int `json:"Status"`
98+
StatusText string `json:"StatusText"`
99+
DurationMs int64 `json:"DurationMs"`
100+
SizeBytes int `json:"SizeBytes"`
101+
}
102+
103+
// buildClient creates an http.Client configured from RequestSettings.
104+
func buildClient(s RequestSettings) *http.Client {
105+
timeout := 50 * time.Second
106+
if s.TimeoutMs > 0 {
107+
timeout = time.Duration(s.TimeoutMs) * time.Millisecond
108+
}
109+
110+
tr := &http.Transport{
111+
TLSClientConfig: &tls.Config{InsecureSkipVerify: !s.VerifyTLS}, //nolint:gosec
112+
}
113+
114+
client := &http.Client{
115+
Timeout: timeout,
116+
Transport: tr,
117+
}
118+
119+
if !s.FollowRedirects {
120+
client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
121+
return http.ErrUseLastResponse
122+
}
123+
}
124+
125+
return client
126+
}
127+
128+
// Send builds a per-request http.Client from Settings and executes the request.
129+
func (a *App) Send(spec RequestSpec) RequestResult {
130+
result := RequestResult{
131+
URL: spec.URL,
132+
Method: spec.Method,
133+
}
134+
135+
var bodyReader io.Reader
136+
if spec.Body.Type != "none" && spec.Body.Raw != "" {
137+
bodyReader = strings.NewReader(spec.Body.Raw)
138+
result.RequestBody = spec.Body.Raw
139+
}
140+
141+
r, err := http.NewRequest(spec.Method, spec.URL, bodyReader)
142+
if err != nil {
143+
result.Error = err.Error()
144+
return result
145+
}
146+
147+
for key, value := range spec.Headers {
148+
r.Header.Add(key, value)
149+
}
150+
151+
result.ReqHeaders = HeadersToStr(&r.Header)
152+
153+
client := buildClient(spec.Settings)
154+
155+
start := time.Now()
156+
res, httpResp, err := doRequest(client, r)
157+
result.DurationMs = time.Since(start).Milliseconds()
158+
159+
if err != nil {
160+
result.Error = err.Error()
161+
// still capture status if response was partially received
162+
if httpResp != nil {
163+
result.Status = httpResp.StatusCode
164+
result.StatusText = httpResp.Status
165+
}
166+
return result
167+
}
168+
169+
result.Status = httpResp.StatusCode
170+
result.StatusText = httpResp.Status
171+
result.HeadersStr = HeadersToStr(&httpResp.Header)
172+
result.SizeBytes = len(res)
173+
174+
// Pretty-print JSON bodies; on failure, return raw body without error
175+
b := bytes.NewBuffer(make([]byte, 0, len(res)))
176+
if jsonErr := json.Indent(b, res, "", " "); jsonErr == nil {
177+
result.Body = b.String()
178+
} else {
179+
result.Body = string(res)
180+
}
181+
182+
return result
183+
}
184+
185+
// SaveTextFile opens a native save dialog and writes text content to the chosen file.
186+
func (a *App) SaveTextFile(filename, contents string) error {
187+
return saveTextFile(a.ctx, filename, contents)
73188
}
74189

75190
func (a *App) RunCurl(curl string) RequestResult {
@@ -88,50 +203,42 @@ func (a *App) RunCurl(curl string) RequestResult {
88203
return res
89204
}
90205

206+
// Header is a single key/value header pair (kept for Export compatibility).
91207
type Header struct {
92208
Key string
93209
Value string
94210
}
95211

96-
// Greet returns a greeting for the given name
212+
// MakeRequest is kept for backward compatibility; it delegates to Send.
97213
func (a *App) MakeRequest(
98214
urlIn string,
99215
method string,
100216
body string,
101217
headers Headers,
102218
) RequestResult {
103-
result := RequestResult{
104-
URL: urlIn,
105-
Method: method,
106-
RequestBody: body,
107-
}
108-
rbody := bytes.NewBuffer([]byte(body))
109-
r, err := http.NewRequest(method, urlIn, rbody)
110-
if err != nil {
111-
result.Error = err.Error()
112-
return result
219+
hdrs := make(map[string]string, len(headers))
220+
for k, v := range headers {
221+
hdrs[k] = v
113222
}
114223

115-
for key, value := range headers {
116-
r.Header.Add(key, value)
117-
}
118-
119-
res, httpResp, err := makeRequest(a.client, r)
120-
if err != nil {
121-
result.Error = err.Error()
122-
return result
123-
}
124-
125-
result.HeadersStr = HeadersToStr(&httpResp.Header)
126-
b := bytes.NewBuffer(make([]byte, 0, len(res)))
127-
err = json.Indent(b, res, "\n", " ")
128-
if err != nil {
129-
return RequestResult{
130-
Body: string(res),
131-
Error: err.Error(),
132-
}
224+
spec := RequestSpec{
225+
Method: method,
226+
URL: urlIn,
227+
Headers: hdrs,
228+
Body: BodySpec{
229+
Type: "raw",
230+
Raw: body,
231+
},
232+
Settings: RequestSettings{
233+
FollowRedirects: true,
234+
VerifyTLS: true,
235+
},
133236
}
134237

135-
result.Body = b.String()
238+
result := a.Send(spec)
239+
// preserve old fields that Send populates differently
240+
result.Method = method
241+
result.URL = urlIn
242+
result.RequestBody = body
136243
return result
137244
}

export.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/json"
56
"log"
67
"os"
@@ -56,3 +57,38 @@ func (a *App) Export(req Request, reqHeaders [][]Header, reqBodies []string, r R
5657

5758
return nil
5859
}
60+
61+
// saveTextFile opens a native save dialog and writes plain text to the chosen path.
62+
func saveTextFile(ctx context.Context, defaultName, contents string) error {
63+
if defaultName == "" {
64+
defaultName = "response.txt"
65+
}
66+
67+
fp, err := runtime.SaveFileDialog(ctx, runtime.SaveDialogOptions{
68+
DefaultFilename: defaultName,
69+
Title: "Save response body",
70+
})
71+
if err != nil {
72+
log.Println(err)
73+
return err
74+
}
75+
76+
if fp == "" {
77+
// user cancelled the dialog
78+
return nil
79+
}
80+
81+
f, err := os.Create(fp)
82+
if err != nil {
83+
log.Println(err)
84+
return err
85+
}
86+
defer f.Close()
87+
88+
if _, err := f.WriteString(contents); err != nil {
89+
log.Println(err)
90+
return err
91+
}
92+
93+
return nil
94+
}

0 commit comments

Comments
 (0)