Skip to content

Guard header copy against committed response in NettyRoutingFilter - #4272

Open
adityaanikam wants to merge 2 commits into
spring-cloud:mainfrom
adityaanikam:fix-readonly-headers-4270
Open

Guard header copy against committed response in NettyRoutingFilter#4272
adityaanikam wants to merge 2 commits into
spring-cloud:mainfrom
adityaanikam:fix-readonly-headers-4270

Conversation

@adityaanikam

Copy link
Copy Markdown

Fixes gh-4270

Problem

NettyRoutingFilter copies filtered response headers back onto the
client response near the end of filter():

response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
...
response.getHeaders().addAll(filteredResponseHeaders);

If the response has already been committed elsewhere in the filter
chain before routing completes, AbstractServerHttpResponse#getHeaders()
returns a cached ReadOnlyHttpHeaders wrapper instead of the mutable
headers map. Calling remove(...) or addAll(...) on that wrapper
throws UnsupportedOperationException, which surfaces as an error
signal on the filter chain instead of the response completing
normally.

Fix

Guard the header-copy step with response.isCommitted(). Once a
response is committed, headers can no longer reach the client
regardless, so skipping the copy is safe and matches an existing
precedent already in the test codebase
(BaseWebClientTests#modifyResponseFilter, which uses the same
isCommitted() check before touching response headers).

Testing

  • Added NettyRoutingFilterTests#routingAfterResponseAlreadyCommittedDoesNotThrow,
    which pre-commits a MockServerWebExchange and asserts
    nettyRoutingFilter.filter(...) completes normally instead of
    erroring.
  • Full NettyRoutingFilterTests suite passes.
  • Verified with a negative control: reverting only the fix (keeping
    the new test) reproduces the exact reported
    UnsupportedOperationException at
    ReadOnlyHttpHeaders.remove(ReadOnlyHttpHeaders.java:166), matching
    the original issue's stack trace. Restoring the fix passes cleanly.

NettyRoutingFilter unconditionally called response.getHeaders()
.remove(...) and .addAll(...) when copying the filtered response
headers onto the client response. Once a response is committed,
AbstractServerHttpResponse#getHeaders() returns a cached
ReadOnlyHttpHeaders wrapper, and mutating it throws
UnsupportedOperationException.

Guard the header-copy step with response.isCommitted(), matching
the same check already used elsewhere in this codebase (see
BaseWebClientTests#modifyResponseFilter). Headers can no longer
reach the client past that point regardless, so skipping is safe.

Fixes spring-cloudgh-4270

Signed-off-by: adityaanikam <adityanikam9502@gmail.com>

@HDPark95 HDPark95 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.

filteredResponseHeaders.remove(TRANSFER_ENCODING) (L182) is a no-op: the enclosing if already requires !filteredResponseHeaders.containsHeader(TRANSFER_ENCODING). The old line mutated response.getHeaders(), so this drops the scrub rather than guarding it.

Checked on main: a probe that presets Transfer-Encoding on the response and routes to a Content-Length upstream passes on main and fails with this patch, leaving both headers set. Keeping response.getHeaders().remove(...) inside your else branch preserves it.

Prior art on the same guard: #4115.

The previous commit moved the Transfer-Encoding removal from
response.getHeaders() onto filteredResponseHeaders. That branch is
only entered when filteredResponseHeaders does not contain
Transfer-Encoding, so removing it there is a no-op, and the scrub of
a stale Transfer-Encoding already set on the downstream response was
lost.

Restore the original response.getHeaders().remove(...) and move the
whole block inside the not-committed branch, where the headers are
still mutable. When the response is committed nothing is written
either way, so the scrub is not needed there.

Add a test that presets Transfer-Encoding on the response and routes
to an upstream answering with Content-Length. It fails without this
change, leaving both headers set.

Signed-off-by: adityaanikam <adityanikam9502@gmail.com>
@adityaanikam

Copy link
Copy Markdown
Author

Good catch, thanks you're right on both counts. The enclosing if already guarantees filteredResponseHeaders has no Transfer-Encoding, so removing it there does nothing, and the original line was scrubbing a stale header off the downstream response, which my change dropped.

Pushed a fix: response.getHeaders().remove(...) is restored as it was, with the whole block moved inside the not committed branch so it still can't touch a ReadOnlyHttpHeaders.

I also added a test matching the probe you described it presets Transfer-Encoding on the response and routes to an upstream answering with Content length. Reverting just the source change makes it fail with both headers still set.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UnsupportedOperationException in NettyRoutingFilter when handling immutable ReadOnlyHttpHeaders

3 participants