spring-cloud / spring-cloud/spring-cloud-gateway

Cannot modify headers on After Filter when response is created with `ServerResponse.ok().build()`

Open
#4,168 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

waiting-for-triage
Dominant language
Java
Stars
4.9k
Forks
3.5k
Avg merge
20h 57m
Merged PRs (30d)
8

Description

Describe the bug
When a route handler returns a ServerResponse created via ServerResponse.ok().build(). The headers exposed to ServerResponse.headers() are immutable (ReadOnlyHttpHeaders).
Attempting to modify response headers inside an After Filter (even with predefined AfterFilterFunctions.addResponseHeader() function) throws UnsupportedOperationException with stacktrace:

java.lang.UnsupportedOperationException
        at org.springframework.http.ReadOnlyHttpHeaders.addAll(ReadOnlyHttpHeaders.java:102) ~[spring-web-6.2.18.jar:6.2.18]
        at org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.lambda$addResponseHeader$0(AfterFilterFunctions.java:59) ~[spring-cloud-gateway-server-mv
c-4.3.4.jar:4.3.4]
--- snip ---

However, the same .after() filter works correctly when using predefined handlers such as HandlerFunctions.http()

Expected behavior
AfterFilterFunctions.addResponseHeader() (and any After Filter Functions that modifies headers) should be able to modify response headers consistently for ServerResponse returned from route handlers.

Alternatively, if immutable headers are intentional, the framework should avoid mutating response.headers() directly inside AfterFilterFunctions.addResponseHeader().

Sample
Minimal, reproducible code example

  1. fooRoute (failing route) returns 500 Internal Server Error
public RouterFunction<ServerResponse> fooRoute() {
    return route("fooRoute")
            .GET(path("/foo"), req -> ServerResponse.ok().build())
            .after(addResponseHeader("X-Test", "TestFoo"))
            .build();
}
  1. barRoute (working route) returns 200 OK with expected response header
public RouterFunction<ServerResponse> barRoute() {
    return route("barRoute")
            .GET(path("/bar"), http())
            .before(uri("http://httpbin.org"))
            .before(setPath("/get"))
            .after(addResponseHeader("X-Test", "TestBar"))
            .build();
}

Java version: 21
Spring boot: 3.5.14
Spring cloud version: 2025.0.2
Spring cloud gateway web MVC: 4.3.4

This issue appears specific to response created directly from route handlers, since proxied responses trough HandlerFunctions.http() behave as expected.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at AfterFilterFunctions.addResponseHeader(), identified in the stack trace, and reproduce the difference between the fooRoute ServerResponse.ok().build() route and the barRoute HandlerFunctions.http() route. Inspect how response headers are exposed in this path and verify the existing after-filter behavior. Done means X-Test is added without UnsupportedOperationException for direct ServerResponse responses while the proxy route continues to work.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.