Skip to content

fix(render): don't modify caller's headers map in Reader.Render - #4900

Open
dhruv-techdev wants to merge 2 commits into
gin-gonic:masterfrom
dhruv-techdev:fix/render-reader-headers-mutation
Open

dhruv-techdev wants to merge 2 commits into
gin-gonic:masterfrom
dhruv-techdev:fix/render-reader-headers-mutation

Conversation

@dhruv-techdev

Copy link
Copy Markdown

Fixes #4899

Reader.Render wrote Content-Length into the caller-supplied Headers map, so DataFromReader leaked a stale Content-Length into later responses that reused the map and raced when the map was shared across requests. Set Content-Length on the response headers instead.

Problem

Context.DataFromReader passes extraHeaders straight to render.Reader, and Reader.Render changed it:

r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
  • Stale header: if the same map is reused, a later call with contentLength = -1 still sends the old Content-Length. Go's server then rejects the body because it is longer than the declared length, and the client gets an empty 200 response.
  • Data race: when concurrent requests share one map, go test -race reports a race at render/reader.go:28, and the runtime can abort with fatal error: concurrent map writes.

Fix

Content-Length is now set directly on w.Header(), so the caller's map is only read. Which value wins is unchanged:

  • a Content-Length already set on the response still takes priority;
  • the contentLength argument still overrides a Content-Length passed in extraHeaders.

Tests

Added TestReaderRenderDoesNotModifyHeaders in render/reader_test.go. It checks that the caller's map is unchanged and that reusing the map for an unknown-length response does not carry over Content-Length. It fails on the old code and passes with the fix.

go test -race -count=1 ./render/
ok  	github.com/gin-gonic/gin/render
go test -race -count=1 -run DataFromReader .
ok  	github.com/gin-gonic/gin

This replaces #3568, which was closed without being merged.

Pull Request Checklist

  • Open your pull request against the master branch.
  • All tests pass in available continuous integration systems (e.g., GitHub Actions).
  • Tests are added or modified as needed to cover code changes.
  • If the pull request introduces a new feature, the feature is documented in the docs/doc.md.

Thank you for contributing!

Reader.Render wrote Content-Length into the caller-supplied Headers map, so DataFromReader leaked a stale Content-Length into later responses that reused the map and raced when the map was shared across requests. Set Content-Length on the response headers instead.
@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.65%. Comparing base (0f09c3a) to head (283f36e).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4900      +/-   ##
==========================================
- Coverage   98.65%   98.65%   -0.01%     
==========================================
  Files          48       48              
  Lines        3188     3186       -2     
==========================================
- Hits         3145     3143       -2     
  Misses         43       43              
Flag Coverage Δ
--ldflags="-checklinkname=0" -tags sonic 98.64% <100.00%> (-0.01%) ⬇️
-tags go_json 98.57% <100.00%> (-0.01%) ⬇️
-tags nomsgpack 98.63% <100.00%> (-0.01%) ⬇️
go-1.26 98.33% <100.00%> (-0.01%) ⬇️
go-1.27 98.66% <100.00%> (-0.01%) ⬇️
macos-latest 98.65% <100.00%> (-0.01%) ⬇️
ubuntu-latest 98.65% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dhruv-techdev

Copy link
Copy Markdown
Author

Hi @appleboy, could you please take a look at #4900 when you get a chance?

This PR fixes an unintended mutation of the caller-provided headers map in Reader.Render, preventing stale Content-Length headers and concurrent map-write races. I've added regression tests and verified them with go test -race.

I'd appreciate your feedback. Thanks for maintaining Gin!

@Tales-of-zuna Tales-of-zuna left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally against upstream master (Go 1.27.3, -race):

  • Reproduced #4899 on master: the caller's map gained Content-Length, a stale length leaked into a later -1 response, and concurrent use raced.
  • With this PR, all three are fixed, and go test -race ./... passes.
  • The new tests fail with the reader.go change reverted, so they guard the regression.
  • Precedence is unchanged: a Content-Length already set on the writer (e.g. by middleware) still wins, matching how writeHeaders behaves on master.

It also avoids the per-response map copy used in #4905. The failing Trivy check fails on master too, so it's unrelated.

LGTM 👍 (first review here, happy to be corrected)

@dhruv-techdev

Copy link
Copy Markdown
Author

Thank you, @Tales-of-zuna, for testing this against upstream master and for the detailed review! I really appreciate you taking the time to verify the fix and regression tests.

@dhruv-techdev

Copy link
Copy Markdown
Author

Just a quick note for the maintainers: The CI failure here is due to the global repository build issue and is unrelated to my changes. Ready for core review once the pipeline is fixed upstream! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DataFromReader writes Content-Length into the caller's extraHeaders map (stale header + data race)

2 participants