Skip to content

Commit b2d8a3a

Browse files
authored
Fix header regression (#1007)
1 parent a38b536 commit b2d8a3a

File tree

2 files changed

+80
-2
lines changed

2 files changed

+80
-2
lines changed

adapters/humaecho/humaecho_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,87 @@ import (
1414

1515
"github.com/danielgtaylor/huma/v2"
1616
"github.com/labstack/echo/v4"
17+
"github.com/stretchr/testify/assert"
1718
)
1819

1920
var lastModified = time.Now()
2021

22+
// flushingResponseWriter simulates a ResponseWriter that flushes headers on WriteHeader.
23+
type flushingResponseWriter struct {
24+
rec *httptest.ResponseRecorder
25+
header http.Header
26+
wroteHeader bool
27+
}
28+
29+
func (m *flushingResponseWriter) Header() http.Header {
30+
return m.header
31+
}
32+
33+
func (m *flushingResponseWriter) WriteHeader(code int) {
34+
if m.wroteHeader {
35+
return
36+
}
37+
m.wroteHeader = true
38+
// Write current headers to the recorder
39+
for k, v := range m.header {
40+
m.rec.Header()[k] = v
41+
}
42+
m.rec.WriteHeader(code)
43+
// Create a new header map so subsequent changes are NOT reflected in the recorder
44+
m.header = make(http.Header)
45+
}
46+
47+
func (m *flushingResponseWriter) Write(b []byte) (int, error) {
48+
if !m.wroteHeader {
49+
m.WriteHeader(http.StatusOK)
50+
}
51+
return m.rec.Write(b)
52+
}
53+
54+
func TestEchoLinkHeader(t *testing.T) {
55+
e := echo.New()
56+
conf := huma.DefaultConfig("My API", "1.0.0")
57+
conf.SchemasPath = "/schemas"
58+
api := New(e, conf)
59+
60+
huma.Register(api, huma.Operation{
61+
Method: http.MethodGet,
62+
Path: "/hello",
63+
OperationID: "get-hello",
64+
}, func(ctx context.Context, input *struct{}) (*struct {
65+
Body struct {
66+
Message string `json:"message"`
67+
}
68+
}, error) {
69+
resp := &struct {
70+
Body struct {
71+
Message string `json:"message"`
72+
}
73+
}{}
74+
resp.Body.Message = "Hello"
75+
return resp, nil
76+
})
77+
78+
req, _ := http.NewRequest(http.MethodGet, "/hello", nil)
79+
rec := httptest.NewRecorder()
80+
81+
// Use our simulated flushing response writer to catch regression if
82+
// SetStatus is called before Transform.
83+
f := &flushingResponseWriter{
84+
rec: rec,
85+
header: make(http.Header),
86+
}
87+
88+
e.ServeHTTP(f, req)
89+
90+
assert.Equal(t, http.StatusOK, rec.Code)
91+
92+
// Check for Link header
93+
link := rec.Header().Get("Link")
94+
assert.NotEmpty(t, link, "Link header should not be empty")
95+
assert.Contains(t, link, "rel=\"describedBy\"")
96+
}
97+
2198
func BenchmarkHumaEcho(b *testing.B) {
2299
type GreetingInput struct {
23100
ID string `path:"id"`

huma.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,16 +607,17 @@ func transformAndWrite(api API, ctx Context, status int, ct string, body any) er
607607
statusStr = strconv.Itoa(status)
608608
}
609609

610-
ctx.SetStatus(status)
611-
612610
tVal, tErr := api.Transform(ctx, statusStr, body)
613611
if tErr != nil {
612+
ctx.SetStatus(status)
614613
ctx.BodyWriter().Write([]byte("error transforming response"))
615614
// When including tVal in the panic message, the server may become unresponsive for some time if the value is very large
616615
// therefore, it has been removed from the panic message
617616
return fmt.Errorf("error transforming response for %s %s %d: %w", ctx.Operation().Method, ctx.Operation().Path, status, tErr)
618617
}
619618

619+
ctx.SetStatus(status)
620+
620621
if status != http.StatusNoContent && status != http.StatusNotModified {
621622
if mErr := api.Marshal(ctx.BodyWriter(), ct, tVal); mErr != nil {
622623
if errors.Is(ctx.Context().Err(), context.Canceled) {

0 commit comments

Comments
 (0)