Skip to content

fix: allow response error handler to set status code#1963

Merged
mromaszewicz merged 5 commits into
oapi-codegen:mainfrom
sean-cunniffe:fix/allow-response-err-handler-set-status-code
Mar 6, 2026
Merged

fix: allow response error handler to set status code#1963
mromaszewicz merged 5 commits into
oapi-codegen:mainfrom
sean-cunniffe:fix/allow-response-err-handler-set-status-code

Conversation

@sean-cunniffe

@sean-cunniffe sean-cunniffe commented May 4, 2025

Copy link
Copy Markdown
Contributor

As mentioned in #1964

Version: 2.4.1

Issue: Status code is set to 200 before marshalling data into body inside visit functions, so when an error occurs marshalling the ResponseErrorHandlerFunc cannot set the status code.

Reproduce:

openapi: 3.0.1
info:
  title: error-handler-example
  version: 1.0.0
paths:
  /api/example:
    post:
      operationId: exampleHandler
      responses:
        200:
          description: example
          content:
            application/json:
              schema:
                type: object
                properties:
                  example:
                    type: string
                    format: email
package: api
generate:
  gorilla-server: true
  strict-server: true
  models: true
output: generated/gen.go
package main

import (
	"context"
	"net/http"

	api "example.go/generated"
	openapi_types "github.com/oapi-codegen/runtime/types"
)

type ExampleHandler struct {
}

func main() {
	e := &ExampleHandler{}
	h := api.NewStrictHandlerWithOptions(e, nil, api.StrictHTTPServerOptions{
		ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
			w.WriteHeader(500)
		},
	})
	handler := api.Handler(h)
	err := http.ListenAndServe(":8080", handler)
	panic(err)
}

func (*ExampleHandler) ExampleHandler(ctx context.Context,
	request api.ExampleHandlerRequestObject) (api.ExampleHandlerResponseObject, error) {
	email := openapi_types.Email("invalid email")
	return api.ExampleHandler200JSONResponse{
		Example: &email,
	}, nil
}

Generated visit function

func (response ExampleHandler200JSONResponse) VisitExampleHandlerResponse(w http.ResponseWriter) error {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)

	return json.NewEncoder(w).Encode(response)
}

Proposed changes would have the output

func (response ExampleHandler200JSONResponse) VisitExampleHandlerResponse(w http.ResponseWriter) error {
	if err := json.NewEncoder(w).Encode(response); err != nil {
		return err
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(200)
	return nil
}

change the order the visit response body function sets the status code
and content type so if an error occurs during json marshalling the
response error handler can set the status code
@sean-cunniffe sean-cunniffe requested a review from a team as a code owner May 4, 2025 02:46
@jamietanna jamietanna added server:strict bug Something isn't working labels May 4, 2025
@jamietanna jamietanna added this to the v2.5.0 milestone May 4, 2025
@jamietanna

Copy link
Copy Markdown
Member

Thanks for reporting + raising a suggested fix! Will try to look at it soon 🤞🏼

@jamietanna jamietanna modified the milestones: v2.5.0, v2.6.0 May 11, 2025
sean-cunniffe and others added 2 commits May 15, 2025 06:33
Resolved conflict in strict-interface.tmpl: kept PR's reordering
(body encoding before headers/WriteHeader) while incorporating
upstream's toGoString helper and $hasBodyVar whitespace change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mromaszewicz

mromaszewicz commented Mar 5, 2026

Copy link
Copy Markdown
Member

Thanks for the PR and for identifying the issue — you're right that it's a problem that ResponseErrorHandlerFunc can't set the status code when marshalling fails. However, this approach introduces some issues that I think need to be addressed before this can be merged.

I used AI to analyze every possible path through this code, and this is what it found:

Non-JSON content types lose headers and status code

The Text, Formdata, Multipart, and io.Reader template branches all contain return statements that exit the function before reaching the moved header/WriteHeader block. This means for every non-JSON content type:

  • Content-Type header is never set
  • Custom response headers are never set
  • WriteHeader is never called (net/http defaults to 200 on first Write, so any non-200 status code would be lost)

This would be a breaking regression for all non-JSON strict server responses.

JSON path: headers set after writing the body are ineffective

For the JSON path, json.NewEncoder(w).Encode(...) writes directly to the http.ResponseWriter. In Go, the first call to w.Write() implicitly triggers WriteHeader(200) and flushes headers. So the w.Header().Set("Content-Type", ...) and w.WriteHeader(200) calls that follow the Encode are effectively no-ops (and WriteHeader will log a "superfluous response.WriteHeader call" warning).

The fix doesn't fully achieve its stated goal

Since json.NewEncoder(w).Encode() writes directly to w, if it writes any bytes before encountering an error, an implicit 200 status and partial body are already on the wire. The error handler can't un-send that.

Suggested approach: encode to a buffer first

To properly fix this, the body should be encoded to a buffer before anything is written to the ResponseWriter. For example, for the JSON case:

var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(response); err != nil {
    return err  // nothing written to w yet, so error handler can safely set status
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
_, err := buf.WriteTo(w)
return err

This ensures:

  1. If encoding fails, nothing has been written to w, giving ResponseErrorHandlerFunc full control over the response status
  2. Headers and status code are set before the body is written
  3. Content-Type and custom headers are correctly applied

The downside:
We now need to buffer the entire encoded response in memory, where it was possible to stream it in the past.

JSON responses in strict server Visit*Response functions are now encoded
to a bytes.Buffer before setting headers and calling WriteHeader. This
ensures that if JSON encoding fails, nothing has been written to the
ResponseWriter, allowing ResponseErrorHandlerFunc to set an appropriate
status code (e.g. 500 instead of 200).

Non-JSON content types (text, formdata, multipart, binary/io.Reader)
retain the original headers-first ordering since their write patterns
do not benefit from buffering.

Adds regression tests for all content type paths and an integration test
verifying that ResponseErrorHandlerFunc can set the status code when
JSON encoding fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mromaszewicz

Copy link
Copy Markdown
Member

@greptileai

@greptile-apps

greptile-apps Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a critical bug where generated Visit*Response functions called w.WriteHeader(...) before marshalling the response body, preventing ResponseErrorHandlerFunc from changing the HTTP status code when encoding errors occurred.

Key changes:

  • JSON responses (template lines 80–90): encoding is now buffered into bytes.Buffer first; headers and WriteHeader are only written after successful encode, allowing marshalling failures to be caught before any response bytes commit.
  • Formdata responses (template lines 92–102): runtime.MarshalForm is now called upfront and errors before WriteHeader, matching the JSON fix.
  • New test package (internal/test/issues/issue-1963): covers happy-path Visit calls for all five content types and two error-path integration tests confirming ResponseErrorHandlerFunc can set status 500 when JSON encoding or form marshalling fails.
  • Generated files throughout the test suite are updated to reflect the new buffered pattern.

Known limitations: Text, Multipart, and binary (io.Reader) responses still call WriteHeader before writing the body. Multipart is particularly notable because the user-supplied callback executes after WriteHeader, so an error in that callback still cannot be intercepted by the error handler. These are harder to buffer (multipart streams incrementally, binary is an io.Reader) and were out of scope for this PR, but documenting this gap would help clarify architectural decisions for future maintainers.

Confidence Score: 4/5

  • Safe to merge. The core fix correctly buffers JSON and Formdata encoding before headers are committed, directly solving the reported issue. Regression tests validate the fix works end-to-end.
  • The fix is sound and well-tested. JSON and Formdata error-path tests confirm that ResponseErrorHandlerFunc can now set status codes when encoding fails. The main deductions are: (1) the error-path test uses a hand-written Visit implementation rather than the generated one, slightly weakening coverage of the exact template output; and (2) Multipart, Text, and Binary responses remain inconsistent with the same architectural issue, though that's documented as intentionally out of scope.
  • pkg/codegen/templates/strict/strict-interface.tmpl — the Multipart branch (lines 103–127) still calls WriteHeader before the user callback, mirroring the original bug. Documenting this remaining gap would clarify the intentional architectural boundary.

Last reviewed commit: 2b56060

Comment thread pkg/codegen/templates/strict/strict-interface.tmpl Outdated
Also buffer formdata responses before writing headers

Apply the same marshal-before-headers pattern to Formdata responses:
runtime.MarshalForm is called before setting Content-Type or calling
WriteHeader, so if marshalling fails the error handler can still set
an appropriate status code.

Adds a regression test for the formdata encoding error path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mromaszewicz

Copy link
Copy Markdown
Member

@greptileai

Comment thread pkg/codegen/templates/strict/strict-interface.tmpl
@mromaszewicz mromaszewicz merged commit 5b83219 into oapi-codegen:main Mar 6, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working server:strict

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants