diff --git a/huma.go b/huma.go index 1c0f3f4f..448685ee 100644 --- a/huma.go +++ b/huma.go @@ -512,6 +512,13 @@ func transformAndWrite(api API, ctx Context, status int, ct string, body any) er ctx.SetStatus(status) if status != http.StatusNoContent && status != http.StatusNotModified { if merr := api.Marshal(ctx.BodyWriter(), ct, tval); merr != nil { + if errors.Is(ctx.Context().Err(), context.Canceled) { + // The client disconnected, so don't bother writing anything. Attempt + // to set the status in case it'll get logged. Technically this was + // not a normal successful request. + ctx.SetStatus(499) + return nil + } ctx.BodyWriter().Write([]byte("error marshaling response")) // When including tval in the panic message, the server may become unresponsive for some time if the value is very large // therefore, it has been removed from the panic message diff --git a/huma_test.go b/huma_test.go index 8ca4c292..d5e984b4 100644 --- a/huma_test.go +++ b/huma_test.go @@ -2117,6 +2117,39 @@ func TestCustomError(t *testing.T) { assert.Equal(t, `{"$schema":"http://localhost/schemas/MyError.json","message":"not found","details":["some-other-error"]}`+"\n", resp.Body.String()) } +type BrokenWriter struct { + http.ResponseWriter +} + +func (br *BrokenWriter) Write(p []byte) (n int, err error) { + return 0, errors.New("failed writing") +} + +func TestClientDisconnect(t *testing.T) { + _, api := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0")) + + huma.Get(api, "/error", func(ctx context.Context, i *struct{}) (*struct { + Body string + }, error) { + return &struct{ Body string }{Body: "test"}, nil + }) + + // Create and immediately cancel the context. This simulates a client + // that has disconnected. + ctx, cancel := context.WithCancel(context.Background()) + cancel() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/error", nil) + + // Also make the response writer fail when writing. + recorder := httptest.NewRecorder() + resp := &BrokenWriter{recorder} + + // We do not want any panics as this is not a real error. + assert.NotPanics(t, func() { + api.Adapter().ServeHTTP(resp, req) + }) +} + type NestedResolversStruct struct { Field2 string `json:"field2"` }