package httpapi_test import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/xerrors" "cdr.dev/slog/v3" "cdr.dev/slog/v3/sloggers/slogtest" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/httpmw/loggermw/loggermock" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" "github.com/coder/quartz" ) func TestInternalServerError(t *testing.T) { t.Parallel() t.Run("NoError", func(t *testing.T) { t.Parallel() w := httptest.NewRecorder() httpapi.InternalServerError(w, nil) var resp codersdk.Response err := json.NewDecoder(w.Body).Decode(&resp) require.NoError(t, err) require.Equal(t, http.StatusInternalServerError, w.Code) require.NotEmpty(t, resp.Message) require.Empty(t, resp.Detail) }) t.Run("WithError", func(t *testing.T) { t.Parallel() var ( w = httptest.NewRecorder() httpErr = xerrors.New("error!") ) httpapi.InternalServerError(w, httpErr) var resp codersdk.Response err := json.NewDecoder(w.Body).Decode(&resp) require.NoError(t, err) require.Equal(t, http.StatusInternalServerError, w.Code) require.NotEmpty(t, resp.Message) require.Equal(t, httpErr.Error(), resp.Detail) }) } func TestWrite(t *testing.T) { t.Parallel() t.Run("NoErrors", func(t *testing.T) { t.Parallel() ctx := context.Background() rw := httptest.NewRecorder() httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{ Message: "Wow.", }) var m map[string]interface{} err := json.NewDecoder(rw.Body).Decode(&m) require.NoError(t, err) _, ok := m["errors"] require.False(t, ok) }) } func TestRead(t *testing.T) { t.Parallel() t.Run("EmptyStruct", func(t *testing.T) { t.Parallel() ctx := context.Background() rw := httptest.NewRecorder() r := httptest.NewRequest("POST", "/", bytes.NewBufferString("{}")) v := struct{}{} require.True(t, httpapi.Read(ctx, rw, r, &v)) }) t.Run("NoBody", func(t *testing.T) { t.Parallel() ctx := context.Background() rw := httptest.NewRecorder() r := httptest.NewRequest("POST", "/", nil) var v json.RawMessage require.False(t, httpapi.Read(ctx, rw, r, v)) }) t.Run("BodyTooLarge", func(t *testing.T) { t.Parallel() ctx := context.Background() rw := httptest.NewRecorder() r := httptest.NewRequest("POST", "/", strings.NewReader(`{"value":"too large"}`)) r.Body = http.MaxBytesReader(rw, r.Body, 4) var v json.RawMessage require.False(t, httpapi.Read(ctx, rw, r, &v)) require.Equal(t, http.StatusRequestEntityTooLarge, rw.Code) }) t.Run("Validate", func(t *testing.T) { t.Parallel() type toValidate struct { Value string `json:"value" validate:"required"` } ctx := context.Background() rw := httptest.NewRecorder() r := httptest.NewRequest("POST", "/", bytes.NewBufferString(`{"value":"hi"}`)) var validate toValidate require.True(t, httpapi.Read(ctx, rw, r, &validate)) require.Equal(t, "hi", validate.Value) }) t.Run("ValidateFailure", func(t *testing.T) { t.Parallel() type toValidate struct { Value string `json:"value" validate:"required"` } ctx := context.Background() rw := httptest.NewRecorder() r := httptest.NewRequest("POST", "/", bytes.NewBufferString("{}")) var validate toValidate require.False(t, httpapi.Read(ctx, rw, r, &validate)) var v codersdk.Response err := json.NewDecoder(rw.Body).Decode(&v) require.NoError(t, err) require.Len(t, v.Validations, 1) require.Equal(t, "value", v.Validations[0].Field) require.Equal(t, "Validation failed for tag \"required\" with value: \"\"", v.Validations[0].Detail) }) } // readBody is decoded by the request body limit tests. It carries no validate // tags so that a decode failure is unambiguously a body-size failure. type readBody struct { Value string `json:"value"` } // jsonBodyOfSize returns a JSON object that decodes into readBody and is // exactly size bytes long. func jsonBodyOfSize(size int) string { const ( prefix = `{"value":"` suffix = `"}` ) return prefix + strings.Repeat("a", size-len(prefix)-len(suffix)) + suffix } func TestReadBodyLimit(t *testing.T) { t.Parallel() // requireTooLarge asserts the 413 response shape shared by every // over-limit case. requireTooLarge := func(t *testing.T, rw *httptest.ResponseRecorder, limit int) { t.Helper() require.Equal(t, http.StatusRequestEntityTooLarge, rw.Code) var resp codersdk.Response require.NoError(t, json.NewDecoder(rw.Body).Decode(&resp)) require.Equal(t, "Request body too large.", resp.Message) require.Contains(t, resp.Detail, strconv.Itoa(limit), "the detail must name the limit so the error is actionable") } t.Run("AtDefaultLimit", func(t *testing.T) { t.Parallel() body := jsonBodyOfSize(httpapi.DefaultMaxRequestBodyBytes) rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) var v readBody require.True(t, httpapi.Read(context.Background(), rw, r, &v)) require.Len(t, v.Value, httpapi.DefaultMaxRequestBodyBytes-len(`{"value":""}`)) }) t.Run("OverDefaultLimitByOneByte", func(t *testing.T) { t.Parallel() body := jsonBodyOfSize(httpapi.DefaultMaxRequestBodyBytes + 1) rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) var v readBody require.False(t, httpapi.Read(context.Background(), rw, r, &v)) requireTooLarge(t, rw, httpapi.DefaultMaxRequestBodyBytes) }) // The limit is enforced on bytes actually read, so neither an absent nor a // dishonest Content-Length can raise it. Asserted in-process rather than // over a connection because a client still streaming an oversized body may // see the connection reset instead of the 413, which would make a // network-driven assertion racy. t.Run("NoContentLength", func(t *testing.T) { t.Parallel() body := jsonBodyOfSize(httpapi.DefaultMaxRequestBodyBytes + 1) rw := httptest.NewRecorder() // io.NopCloser hides the length, which is what net/http sees for a // chunked request. r := httptest.NewRequest(http.MethodPost, "/", io.NopCloser(strings.NewReader(body))) r.TransferEncoding = []string{"chunked"} require.EqualValues(t, -1, r.ContentLength) var v readBody require.False(t, httpapi.Read(context.Background(), rw, r, &v)) requireTooLarge(t, rw, httpapi.DefaultMaxRequestBodyBytes) }) t.Run("UnderstatedContentLength", func(t *testing.T) { t.Parallel() body := jsonBodyOfSize(httpapi.DefaultMaxRequestBodyBytes + 1) rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) r.ContentLength = 10 var v readBody require.False(t, httpapi.Read(context.Background(), rw, r, &v)) requireTooLarge(t, rw, httpapi.DefaultMaxRequestBodyBytes) }) t.Run("ChunkedUnderLimit", func(t *testing.T) { t.Parallel() body := jsonBodyOfSize(httpapi.DefaultMaxRequestBodyBytes) rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", io.NopCloser(strings.NewReader(body))) r.TransferEncoding = []string{"chunked"} var v readBody require.True(t, httpapi.Read(context.Background(), rw, r, &v)) }) } func TestReadLimit(t *testing.T) { t.Parallel() // A limit above the default must not be tightened by the default that Read // installs. This is the unit-level regression test for the endpoints that // legitimately accept more than DefaultMaxRequestBodyBytes; without // ReadLimit they would be silently capped at the default. t.Run("AboveDefaultIsNotTightened", func(t *testing.T) { t.Parallel() const limit = 8 << 20 body := jsonBodyOfSize(6 << 20) require.Greater(t, len(body), httpapi.DefaultMaxRequestBodyBytes) rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) var v readBody require.True(t, httpapi.ReadLimit(context.Background(), rw, r, limit, &v)) require.Len(t, v.Value, len(body)-len(`{"value":""}`)) }) t.Run("BelowDefaultIsEnforced", func(t *testing.T) { t.Parallel() const limit = 1024 rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(jsonBodyOfSize(limit+1))) var v readBody require.False(t, httpapi.ReadLimit(context.Background(), rw, r, limit, &v)) require.Equal(t, http.StatusRequestEntityTooLarge, rw.Code) var resp codersdk.Response require.NoError(t, json.NewDecoder(rw.Body).Decode(&resp)) require.Contains(t, resp.Detail, strconv.Itoa(limit)) }) t.Run("AtLimit", func(t *testing.T) { t.Parallel() const limit = 1024 rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(jsonBodyOfSize(limit))) var v readBody require.True(t, httpapi.ReadLimit(context.Background(), rw, r, limit, &v)) }) // The limit lands on the request's existing log line rather than a line of // its own, because a caller can produce 413s at will and a dedicated line // would let them drive log volume. Without the limit recorded somewhere, a // rejection is indistinguishable from a client that hung up. t.Run("RecordsLimitOnRequestLog", func(t *testing.T) { t.Parallel() const limit = 1024 ctrl := gomock.NewController(t) requestLogger := loggermock.NewMockRequestLogger(ctrl) requestLogger.EXPECT(). WithFields(slog.F("max_request_body_bytes", int64(limit))). Times(1) ctx := loggermw.WithRequestLogger(context.Background(), requestLogger) rw := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(jsonBodyOfSize(limit+1))).WithContext(ctx) var v readBody require.False(t, httpapi.ReadLimit(ctx, rw, r, limit, &v)) require.Equal(t, http.StatusRequestEntityTooLarge, rw.Code) }) } // TestMaxBytesReaderNesting pins how http.MaxBytesReader composes: nesting two // readers yields the tighter of the two limits, regardless of which one is // outermost. httpapi.Read wraps the body it decodes, so handlers that need a // different limit cannot get one by wrapping the body themselves, and must use // httpapi.ReadLimit instead. That requirement is only true because composition // behaves this way, so it is asserted here rather than assumed. func TestMaxBytesReaderNesting(t *testing.T) { t.Parallel() const ( tight = 4 loose = 8 ) body := strings.Repeat("a", loose+1) // readNested wraps body in two MaxBytesReaders and reports how many bytes // were readable before the limit tripped. readNested := func(t *testing.T, outer, inner int64) int { t.Helper() rw := httptest.NewRecorder() rc := io.NopCloser(strings.NewReader(body)) rc = http.MaxBytesReader(rw, rc, inner) rc = http.MaxBytesReader(rw, rc, outer) read, err := io.ReadAll(rc) _, ok := errors.AsType[*http.MaxBytesError](err) require.True(t, ok, "expected *http.MaxBytesError, got %v", err) return len(read) } require.Equal(t, tight, readNested(t, tight, loose), "tight limit outside a loose one must win") require.Equal(t, tight, readNested(t, loose, tight), "tight limit inside a loose one must win") } func TestWebsocketCloseMsg(t *testing.T) { t.Parallel() t.Run("Sprintf", func(t *testing.T) { t.Parallel() var ( msg = "this is my message %q %q" opts = []any{"colin", "kyle"} ) expected := fmt.Sprintf(msg, opts...) got := httpapi.WebsocketCloseSprintf(msg, opts...) assert.Equal(t, expected, got) }) t.Run("TruncateSingleByteCharacters", func(t *testing.T) { t.Parallel() msg := strings.Repeat("d", 255) trunc := httpapi.WebsocketCloseSprintf("%s", msg) assert.Equal(t, len(trunc), 123) }) t.Run("TruncateMultiByteCharacters", func(t *testing.T) { t.Parallel() msg := strings.Repeat("こんにちは", 10) trunc := httpapi.WebsocketCloseSprintf("%s", msg) assert.Equal(t, len(trunc), 123) }) } // Our WebSocket library accepts any arbitrary ResponseWriter at the type level, // but the writer must also implement http.Hijacker for long-lived connections. type mockOneWaySocketWriter struct { serverRecorder *httptest.ResponseRecorder serverConn net.Conn clientConn net.Conn serverReadWriter *bufio.ReadWriter testContext *testing.T } func (m mockOneWaySocketWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return m.serverConn, m.serverReadWriter, nil } func (m mockOneWaySocketWriter) Flush() { err := m.serverReadWriter.Flush() require.NoError(m.testContext, err) } func (m mockOneWaySocketWriter) Header() http.Header { return m.serverRecorder.Header() } func (m mockOneWaySocketWriter) Write(b []byte) (int, error) { return m.serverReadWriter.Write(b) } func (m mockOneWaySocketWriter) WriteHeader(code int) { m.serverRecorder.WriteHeader(code) } func TestOneWayWebSocketEventSender(t *testing.T) { t.Parallel() newBaseRequest := func(ctx context.Context) *http.Request { url := "ws://www.fake-website.com/logs" req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) require.NoError(t, err) h := req.Header h.Add("Connection", "Upgrade") h.Add("Upgrade", "websocket") h.Add("Sec-WebSocket-Version", "13") h.Add("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") // Just need any string return req } newOneWayWriter := func(t *testing.T) mockOneWaySocketWriter { mockServer, mockClient := net.Pipe() recorder := httptest.NewRecorder() return mockOneWaySocketWriter{ testContext: t, serverConn: mockServer, clientConn: mockClient, serverRecorder: recorder, serverReadWriter: bufio.NewReadWriter( bufio.NewReader(mockServer), bufio.NewWriter(mockServer), ), } } t.Run("Produces error if the socket connection could not be established", func(t *testing.T) { t.Parallel() incorrectProtocols := []struct { major int minor int proto string }{ {0, 9, "HTTP/0.9"}, {1, 0, "HTTP/1.0"}, } for _, p := range incorrectProtocols { ctx := testutil.Context(t, testutil.WaitShort) req := newBaseRequest(ctx) req.ProtoMajor = p.major req.ProtoMinor = p.minor req.Proto = p.proto writer := newOneWayWriter(t) _, _, err := httpapi.OneWayWebSocketEventSender(slogtest.Make(t, nil), nil)(writer, req) require.ErrorContains(t, err, p.proto) } }) t.Run("Returned callback can publish new event to WebSocket connection", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) wsw := httpapi.NewWSWatcher(quartz.NewReal(), nil) req := newBaseRequest(ctx) writer := newOneWayWriter(t) send, _, err := httpapi.OneWayWebSocketEventSender(slogtest.Make(t, nil), wsw)(writer, req) require.NoError(t, err) serverPayload := codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, Data: "Blah", } err = send(serverPayload) require.NoError(t, err) // The client connection will receive a little bit of additional data on // top of the main payload. Have to make sure check has tolerance for // extra data being present serverBytes, err := json.Marshal(serverPayload) require.NoError(t, err) clientBytes, err := io.ReadAll(writer.clientConn) require.NoError(t, err) require.True(t, bytes.Contains(clientBytes, serverBytes)) }) t.Run("Signals to outside consumer when socket has been closed", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(testutil.Context(t, testutil.WaitShort)) wsw := httpapi.NewWSWatcher(quartz.NewReal(), nil) req := newBaseRequest(ctx) writer := newOneWayWriter(t) _, done, err := httpapi.OneWayWebSocketEventSender(slogtest.Make(t, nil), wsw)(writer, req) require.NoError(t, err) successC := make(chan bool) ticker := time.NewTicker(testutil.WaitShort) go func() { select { case <-done: successC <- true case <-ticker.C: successC <- false } }() cancel() require.True(t, <-successC) }) t.Run("Socket will immediately close if client sends any message", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) wsw := httpapi.NewWSWatcher(quartz.NewReal(), nil) req := newBaseRequest(ctx) writer := newOneWayWriter(t) _, done, err := httpapi.OneWayWebSocketEventSender(slogtest.Make(t, nil), wsw)(writer, req) require.NoError(t, err) successC := make(chan bool) ticker := time.NewTicker(testutil.WaitShort) go func() { select { case <-done: successC <- true case <-ticker.C: successC <- false } }() type JunkClientEvent struct { Value string } b, err := json.Marshal(JunkClientEvent{"Hi :)"}) require.NoError(t, err) _, err = writer.clientConn.Write(b) require.NoError(t, err) require.True(t, <-successC) }) t.Run("Renders the socket inert if the request context cancels", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(testutil.Context(t, testutil.WaitShort)) wsw := httpapi.NewWSWatcher(quartz.NewReal(), nil) req := newBaseRequest(ctx) writer := newOneWayWriter(t) send, done, err := httpapi.OneWayWebSocketEventSender(slogtest.Make(t, nil), wsw)(writer, req) require.NoError(t, err) successC := make(chan bool) ticker := time.NewTicker(testutil.WaitShort) go func() { select { case <-done: successC <- true case <-ticker.C: successC <- false } }() cancel() require.True(t, <-successC) err = send(codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, Data: "Didn't realize you were closed - sorry! I'll try coming back tomorrow.", }) require.Equal(t, err, ctx.Err()) _, open := <-done require.False(t, open) _, err = writer.serverConn.Write([]byte{}) require.Equal(t, err, io.ErrClosedPipe) _, err = writer.clientConn.Read([]byte{}) require.Equal(t, err, io.EOF) }) t.Run("Sends a heartbeat to the socket on a fixed internal of time to keep connections alive", func(t *testing.T) { t.Parallel() // Need add at least three heartbeats for something to be reliably // counted as an interval, but also need some wiggle room heartbeatCount := 3 hbDuration := time.Duration(heartbeatCount) * httpapi.HeartbeatInterval timeout := hbDuration + (5 * time.Second) ctx := testutil.Context(t, timeout) wsw := httpapi.NewWSWatcher(quartz.NewReal(), nil) req := newBaseRequest(ctx) writer := newOneWayWriter(t) _, _, err := httpapi.OneWayWebSocketEventSender(slogtest.Make(t, nil), wsw)(writer, req) require.NoError(t, err) type Result struct { Err error Success bool } resultC := make(chan Result) go func() { err := writer. clientConn. SetReadDeadline(time.Now().Add(timeout)) if err != nil { resultC <- Result{err, false} return } for range heartbeatCount { pingBuffer := make([]byte, 1) pingSize, err := writer.clientConn.Read(pingBuffer) if err != nil || pingSize != 1 { resultC <- Result{err, false} return } } resultC <- Result{nil, true} }() result := <-resultC require.NoError(t, result.Err) require.True(t, result.Success) }) } // ServerSentEventSender accepts any arbitrary ResponseWriter at the type level, // but the writer must also implement http.Flusher for long-lived connections type mockServerSentWriter struct { serverRecorder *httptest.ResponseRecorder serverConn net.Conn clientConn net.Conn buffer *bytes.Buffer testContext *testing.T } func (m mockServerSentWriter) Flush() { b := m.buffer.Bytes() _, err := m.serverConn.Write(b) require.NoError(m.testContext, err) m.buffer.Reset() // Must close server connection to indicate EOF for any reads from the // client connection; otherwise reads block forever. This is a testing // limitation compared to the one-way websockets, since we have no way to // frame the data and auto-indicate EOF for each message err = m.serverConn.Close() require.NoError(m.testContext, err) } func (m mockServerSentWriter) Header() http.Header { return m.serverRecorder.Header() } func (m mockServerSentWriter) Write(b []byte) (int, error) { return m.buffer.Write(b) } func (m mockServerSentWriter) WriteHeader(code int) { m.serverRecorder.WriteHeader(code) } func TestServerSentEventSender(t *testing.T) { t.Parallel() newBaseRequest := func(ctx context.Context) *http.Request { url := "ws://www.fake-website.com/logs" req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) require.NoError(t, err) return req } newServerSentWriter := func(t *testing.T) mockServerSentWriter { mockServer, mockClient := net.Pipe() return mockServerSentWriter{ testContext: t, serverRecorder: httptest.NewRecorder(), clientConn: mockClient, serverConn: mockServer, buffer: &bytes.Buffer{}, } } t.Run("Mutates response headers to support SSE connections", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) req := newBaseRequest(ctx) writer := newServerSentWriter(t) _, _, err := httpapi.ServerSentEventSender(writer, req) require.NoError(t, err) h := writer.Header() require.Equal(t, h.Get("Content-Type"), "text/event-stream") require.Equal(t, h.Get("Cache-Control"), "no-cache") require.Equal(t, h.Get("Connection"), "keep-alive") require.Equal(t, h.Get("X-Accel-Buffering"), "no") }) t.Run("Returned callback can publish new event to SSE connection", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) req := newBaseRequest(ctx) writer := newServerSentWriter(t) send, _, err := httpapi.ServerSentEventSender(writer, req) require.NoError(t, err) serverPayload := codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, Data: "Blah", } err = send(serverPayload) require.NoError(t, err) clientBytes, err := io.ReadAll(writer.clientConn) require.NoError(t, err) require.Equal( t, string(clientBytes), "event: data\ndata: \"Blah\"\n\n", ) }) t.Run("Signals to outside consumer when connection has been closed", func(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(testutil.Context(t, testutil.WaitShort)) req := newBaseRequest(ctx) writer := newServerSentWriter(t) _, done, err := httpapi.ServerSentEventSender(writer, req) require.NoError(t, err) successC := make(chan bool) ticker := time.NewTicker(testutil.WaitShort) go func() { select { case <-done: successC <- true case <-ticker.C: successC <- false } }() cancel() require.True(t, <-successC) }) t.Run("Sends a heartbeat to the client on a fixed internal of time to keep connections alive", func(t *testing.T) { t.Parallel() // Need add at least three heartbeats for something to be reliably // counted as an interval, but also need some wiggle room heartbeatCount := 3 hbDuration := time.Duration(heartbeatCount) * httpapi.HeartbeatInterval timeout := hbDuration + (5 * time.Second) ctx := testutil.Context(t, timeout) req := newBaseRequest(ctx) writer := newServerSentWriter(t) _, _, err := httpapi.ServerSentEventSender(writer, req) require.NoError(t, err) type Result struct { Err error Success bool } resultC := make(chan Result) go func() { err := writer. clientConn. SetReadDeadline(time.Now().Add(timeout)) if err != nil { resultC <- Result{err, false} return } for range heartbeatCount { pingBuffer := make([]byte, 1) pingSize, err := writer.clientConn.Read(pingBuffer) if err != nil || pingSize != 1 { resultC <- Result{err, false} return } } resultC <- Result{nil, true} }() result := <-resultC require.NoError(t, result.Err) require.True(t, result.Success) }) } // TestRecordRequestBodyLimit covers the pairing that every 413-for-an-oversized- // body site depends on. The sites answer in their own error shapes, codersdk, // RFC 6749, RFC 7591 and RFC 7644, so what they share is this call rather than a // response writer. Both halves matter: the log field is what tells an operator // which limit tripped, and the tracker is what stops the metric attributing a // 413 to body size when it was raised for another reason. func TestRecordRequestBodyLimit(t *testing.T) { t.Parallel() t.Run("RecordsFieldAndMarksTracker", func(t *testing.T) { t.Parallel() const limit = int64(4096) ctrl := gomock.NewController(t) requestLogger := loggermock.NewMockRequestLogger(ctrl) requestLogger.EXPECT(). WithFields(slog.F("max_request_body_bytes", limit)). Times(1) tracker := &httpapi.RequestBodyLimitTracker{} ctx := httpapi.WithRequestBodyLimitTracker( loggermw.WithRequestLogger(context.Background(), requestLogger), tracker) require.False(t, tracker.Exceeded()) httpapi.RecordRequestBodyLimit(ctx, limit) require.True(t, tracker.Exceeded()) }) // The middleware that installs the tracker is not mounted on every route, so // a call without one must not panic. t.Run("NoTrackerInContext", func(t *testing.T) { t.Parallel() httpapi.RecordRequestBodyLimit(context.Background(), 4096) }) }