From 41db0fe5a34d1ea8b9a277790c05d12dd42c536d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 31 Aug 2026 18:36:09 +0200 Subject: [PATCH] Preserve original headers and cookies when mutating client response Prior to this commit, the `DefaultClientResponseBuilder` would assume that an original client HTTP response, when mutated, would not be reused nor read anymore. While this is the advised use case, there was some inconsistency with the builder API here when mutating: some data like the response status would copied, but the HTTP headers and cookies would refer directly to the previous entries, making all changes visible to the previous response instance. This commit ensures that deep copies are performed when mutating a client response with the builder API. Fixes gh-37086 --- .../function/client/DefaultClientResponseBuilder.java | 8 ++++++-- .../client/DefaultClientResponseBuilderTests.java | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index ce5e6c4dba9..6c5473185e0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -18,6 +18,7 @@ package org.springframework.web.reactive.function.client; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -144,7 +145,7 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { @SuppressWarnings({"ConstantConditions", "NullAway"}) private HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = new HttpHeaders(this.originalResponse.headers().asHttpHeaders()); + this.headers = HttpHeaders.copyOf(this.originalResponse.headers().asHttpHeaders()); } return this.headers; } @@ -166,7 +167,10 @@ final class DefaultClientResponseBuilder implements ClientResponse.Builder { @SuppressWarnings({"ConstantConditions", "NullAway"}) private MultiValueMap getCookies() { if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(this.originalResponse.cookies()); + MultiValueMap originalCookies = this.originalResponse.cookies(); + this.cookies = new LinkedMultiValueMap<>(originalCookies.size()); + originalCookies.forEach( + (name, values) -> this.cookies.put(name, new ArrayList<>(values))); } return this.cookies; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java index bb1f4056296..6fb195097df 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -83,16 +83,21 @@ class DefaultClientResponseBuilderTests { ClientResponse result = otherResponse.mutate() .statusCode(HttpStatus.BAD_REQUEST) .headers(headers -> headers.set("foo", "baar")) - .cookies(cookies -> cookies.set("baz", ResponseCookie.from("baz", "quux").build())) + .cookies(cookies -> cookies.add("baz", ResponseCookie.from("baz", "pop").build())) .build(); + assertThat(otherResponse.headers().asHttpHeaders().getFirst("foo")).isEqualTo("bar"); + assertThat(otherResponse.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); + assertThat(otherResponse.cookies().get("baz")).hasSize(1); assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(result.headers().asHttpHeaders().size()).isEqualTo(3); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); assertThat(result.cookies()).hasSize(1); - assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("quux"); + assertThat(result.cookies().get("baz")).hasSize(2); + assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("qux"); + assertThat(result.cookies().get("baz").get(1).getValue()).isEqualTo("pop"); assertThat(result.logPrefix()).isEqualTo("my-prefix"); StepVerifier.create(result.bodyToFlux(String.class))