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:

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

3dc687406d87View transcript

Changes

4

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)
    }
}