From c67ae296721a28365344f35658daa0cfebc9a34c Mon Sep 17 00:00:00 2001 From: Daniel Taylor Date: Tue, 12 Nov 2024 19:10:29 -0800 Subject: [PATCH 1/2] fix: do not panic on client disconnect --- huma.go | 7 +++++++ huma_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/huma.go b/huma.go index 1c0f3f4..448685e 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 8ca4c29..128fa87 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, fmt.Errorf("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"` } From f516eb66ac64da7d9b1896cd7d5baa40a79d3e71 Mon Sep 17 00:00:00 2001 From: Daniel Taylor Date: Tue, 12 Nov 2024 19:20:51 -0800 Subject: [PATCH 2/2] fix: linter --- huma_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/huma_test.go b/huma_test.go index 128fa87..d5e984b 100644 --- a/huma_test.go +++ b/huma_test.go @@ -2122,7 +2122,7 @@ type BrokenWriter struct { } func (br *BrokenWriter) Write(p []byte) (n int, err error) { - return 0, fmt.Errorf("failed writing") + return 0, errors.New("failed writing") } func TestClientDisconnect(t *testing.T) {