Skip to content

v2.37.3: Link response header lost because SetStatus is called before Transform in transformAndWrite #1005

Description

@ottaaa

Bug Description

After upgrading from v2.37.2 to v2.37.3, the Link response header (e.g., </schemas/MyOutputBody.json>; rel="describedBy") is no longer present in API responses when using the Echo adapter (humaecho).

The $schema field in the response body still works correctly — only the HTTP Link header is affected.

Root Cause

In PR #997, ctx.SetStatus(status) was moved before api.Transform() in transformAndWrite() (huma.go, line 610).

In the humaecho adapter, SetStatus calls c.orig.Response().WriteHeader(code) (humaecho.go, line 109), which — per Go's net/http contract — flushes the status code and all currently-set headers to the client.

The SchemaLinkTransformer.Transform method then calls ctx.AppendHeader("Link", info.header) (transforms.go, line 190), but since WriteHeader was already called, the header addition is silently ignored.

v2.37.2 (working)

api.Transform(ctx, ...)   // AppendHeader("Link", ...) — headers not yet flushed ✅
ctx.SetStatus(status)      // WriteHeader flushes status + headers (including Link)
api.Marshal(...)           // write body

v2.37.3 (broken)

ctx.SetStatus(status)      // WriteHeader flushes status + headers (Link not yet set)
api.Transform(ctx, ...)   // AppendHeader("Link", ...) — too late, headers already flushed ❌
api.Marshal(...)           // write body

Reproduction

Any API endpoint using the Echo adapter with docs enabled:

conf := huma.DefaultConfig("My API", "1.0.0")
conf.SchemasPath = "/schemas"
api := humaecho.New(e, conf)

huma.Register(api, huma.Operation{
    Method:      http.MethodGet,
    Path:        "/hello",
    OperationID: "get-hello",
}, func(ctx context.Context, input *struct{}) (*struct {
    Body struct {
        Message string `json:"message"`
    }
}, error) {
    resp := &struct {
        Body struct {
            Message string `json:"message"`
        }
    }{}
    resp.Body.Message = "Hello"
    return resp, nil
})

Expected Link header: </schemas/GetHelloOutputBody.json>; rel="describedBy"
Actual Link header: (empty)

Suggested Fix

Move ctx.SetStatus(status) back to after api.Transform() but before api.Marshal(), so that headers set during transformation are included, while still allowing the status to be changed to 500 if marshaling fails:

tVal, tErr := api.Transform(ctx, statusStr, body)
if tErr \!= nil { ... }

ctx.SetStatus(status)  // Set status after transform, before marshal

if status \!= http.StatusNoContent && status \!= http.StatusNotModified {
    if mErr := api.Marshal(ctx.BodyWriter(), ct, tVal); mErr \!= nil {
        ctx.SetStatus(http.StatusInternalServerError)  // Override on marshal failure
        ...
    }
}

Environment

  • huma v2.37.3
  • Adapter: humaecho (Echo v4)
  • Go 1.24

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions