cli/api: honor caller Content-Type and error on oversize bodies · Entire
cli/api: honor caller Content-Type and error on oversize bodies
3c68ec8→main·
Soph·2w ago·4 files·+88 added/-13 removed
Address PR review + a trail finding on entire api:
- do() now defaults a body's Content-Type to application/json only when the caller didn't set one, so
entire api -H 'Content-Type: …'can POST a non-JSON --input body (was unconditionally clobbered). Fixes trail finding 019f2341 / Copilot. - readWithinLimit reads up to the cap + errors when the source exceeds it, instead of silently truncating — both the response body and stdin --input now fail loudly rather than printing/sending malformed JSON.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
3dc687406d87View transcript
Changes
4
cmd/entire/cli
api
Mclient.go+6/-2
Mclient_test.go+47/-9
Mapi_cmd.go+16/-2
Mapi_cmd_test.go+19
192 unmodified lines
193
194
195
196
196
197
198
199
200
21 unmodified lines
222
223
224
224
225
226
227
228
229
230
231
192 unmodified lines
// extra headers, and an optional raw body. It's the general-purpose escape
// hatch behind `entire api`; prefer the typed verbs (Get/Post/…) for normal
// use. The bearer, User-Agent, and default Accept are still attached by the
// transport; a body still gets Content-Type: application/json.
// transport; a body defaults to Content-Type: application/json unless the
// caller supplies its own via headers.
func (c *Client) Request(ctx context.Context, method, path string, headers http.Header, body io.Reader) (*http.Response, error) {
return c.do(ctx, method, path, body, headers)
}
21 unmodified lines
}
}
if body != nil {
// Default a body's Content-Type to JSON, but don't clobber a caller-supplied
// one — the `entire api -H 'Content-Type: …'` escape hatch must be able to
// send non-JSON bodies.
if body != nil && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
}
Mcmd/entire/cli/api/client.go+6/-2
11 unmodified lines
12
13
14
15
15
16
17
18
19
20
21
33 unmodified lines
55
56
57
55
56
58
59
60
61
62
111 unmodified lines
174
175
176
174
177
178
179
180
41 unmodified lines
222
223
224
222
225
226
227
228
42 unmodified lines
271
272
273
271
274
275
276
277
18 unmodified lines
296
297
298
296
299
300
301
302
42 unmodified lines
345
346
347
345
348
349
350
351
51 unmodified lines
403
404
405
403
406
407
408
409
75 unmodified lines
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
11 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
)
const testBearerHeader = "Bearer tok"
const (
testBearerHeader = "Bearer tok"
jsonContentType = "application/json"
)
func TestBearerTransport_InjectsAuthHeader(t *testing.T) {
t.Parallel()
33 unmodified lines
if want := versioninfo.UserAgent(); gotUA != want {
t.Errorf("User-Agent = %q, want %q", gotUA, want)
}
if gotAccept != "application/json" {
t.Errorf("Accept = %q, want %q", gotAccept, "application/json")
if gotAccept != jsonContentType {
t.Errorf("Accept = %q, want %q", gotAccept, jsonContentType)
}
}
111 unmodified lines
if r.Header.Get("Authorization") != "Bearer my-token" {
t.Errorf("Authorization = %q", r.Header.Get("Authorization"))
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Type", jsonContentType)
w.Write([]byte(`{"ok": true}`)) //nolint:errcheck // test handler
})
defer server.Close()
41 unmodified lines
if resp.StatusCode != http.StatusCreated {
t.Errorf("status = %d, want 201", resp.StatusCode)
}
if gotContentType != "application/json" {
if gotContentType != jsonContentType {
t.Errorf("Content-Type = %q, want application/json", gotContentType)
}
if gotBody["name"] != "test" {
42 unmodified lines
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Type", jsonContentType)
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error": "insufficient permissions"}`)) //nolint:errcheck // test handler
}))
18 unmodified lines
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Type", jsonContentType)
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error":{"code":"not_found","message":"session not found","field":null,"retryable":false}}`)) //nolint:errcheck // test handler
}))
42 unmodified lines
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Type", jsonContentType)
w.Write([]byte(`{"id": "abc", "status": "ok"}`)) //nolint:errcheck // test handler
}))
defer server.Close()
51 unmodified lines
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Type", jsonContentType)
w.Write(encoded) //nolint:errcheck // test handler
}))
defer server.Close()
75 unmodified lines
t.Fatalf("request reached the other host (Authorization=%q); bearer must not follow a cross-host redirect", leakedAuth)
}
}
// TestClient_Request_RespectsCallerContentType verifies a caller-supplied
// Content-Type survives (the -H escape hatch), while a body with no
// Content-Type still defaults to JSON.
func TestClient_Request_RespectsCallerContentType(t *testing.T) {
t.Parallel()
var gotCT string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotCT = r.Header.Get("Content-Type")
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
c := NewClientWithBaseURL("tok", server.URL)
resp, err := c.Request(context.Background(), http.MethodPost, "/x",
http.Header{"Content-Type": {"text/plain"}}, strings.NewReader("hi"))
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
if gotCT != "text/plain" {
t.Errorf("caller Content-Type = %q, want text/plain (must not be clobbered)", gotCT)
}
resp, err = c.Request(context.Background(), http.MethodPost, "/y", nil, strings.NewReader("{}"))
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
if gotCT != jsonContentType {
t.Errorf("default Content-Type = %q, want application/json", gotCT)
}
}