From a4ec7cb19780b9a97de680e51ec92ab9e0fa13aa Mon Sep 17 00:00:00 2001 From: qnnn Date: Fri, 17 Apr 2026 22:18:48 +0800 Subject: [PATCH 1/6] Apply CodecCustomizer to body filters to ensure consistent encoder/decoder. Signed-off-by: qnnn --- .../config/GatewayAutoConfiguration.java | 17 ++++++++------ .../GatewayFunctionAutoConfiguration.java | 8 +++++-- .../gateway/filter/FunctionRoutingFilter.java | 20 +++++++++++++++- ...ModifyRequestBodyGatewayFilterFactory.java | 19 ++++++++++++++- ...odifyResponseBodyGatewayFilterFactory.java | 23 ++++++++++++++++++- .../gateway/support/BodyInserterContext.java | 2 +- 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 934f19022..b1e2f60fd 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -56,6 +56,7 @@ import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory; import org.springframework.boot.reactor.netty.NettyServerCustomizer; import org.springframework.boot.reactor.netty.autoconfigure.NettyReactiveWebServerFactoryCustomizer; @@ -578,8 +579,9 @@ public AddResponseHeaderGatewayFilterFactory addResponseHeaderGatewayFilterFacto @Bean @ConditionalOnEnabledFilter public ModifyRequestBodyGatewayFilterFactory modifyRequestBodyGatewayFilterFactory( - ServerCodecConfigurer codecConfigurer) { - return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer.getReaders()); + ServerCodecConfigurer codecConfigurer, ObjectProvider codecCustomizers) { + return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer.getReaders(), + codecCustomizers.orderedStream().toList()); } @Bean @@ -592,8 +594,9 @@ public DedupeResponseHeaderGatewayFilterFactory dedupeResponseHeaderGatewayFilte @ConditionalOnEnabledFilter public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory( ServerCodecConfigurer codecConfigurer, Set bodyDecoders, - Set bodyEncoders) { - return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer.getReaders(), bodyDecoders, bodyEncoders); + Set bodyEncoders, ObjectProvider codecCustomizers) { + return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer.getReaders(), bodyDecoders, bodyEncoders, + codecCustomizers.orderedStream().toList()); } @Bean @@ -625,9 +628,9 @@ public RedirectToGatewayFilterFactory redirectToGatewayFilterFactory() { @ConditionalOnEnabledFilter public RemoveJsonAttributesResponseBodyGatewayFilterFactory removeJsonAttributesResponseBodyGatewayFilterFactory( ServerCodecConfigurer codecConfigurer, Set bodyDecoders, - Set bodyEncoders) { - return new RemoveJsonAttributesResponseBodyGatewayFilterFactory( - new ModifyResponseBodyGatewayFilterFactory(codecConfigurer.getReaders(), bodyDecoders, bodyEncoders)); + Set bodyEncoders, ObjectProvider codecCustomizers) { + return new RemoveJsonAttributesResponseBodyGatewayFilterFactory(new ModifyResponseBodyGatewayFilterFactory( + codecConfigurer.getReaders(), bodyDecoders, bodyEncoders, codecCustomizers.orderedStream().toList())); } @Bean diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java index c78718701..b306f89cd 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayFunctionAutoConfiguration.java @@ -18,11 +18,13 @@ import java.util.Set; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; @@ -45,8 +47,10 @@ class GatewayFunctionAutoConfiguration { @ConditionalOnEnabledGlobalFilter @ConditionalOnBean(FunctionCatalog.class) public FunctionRoutingFilter functionRoutingFilter(FunctionCatalog functionCatalog, - ServerCodecConfigurer codecConfigurer, Set messageBodyEncoders) { - return new FunctionRoutingFilter(functionCatalog, codecConfigurer.getReaders(), messageBodyEncoders); + ServerCodecConfigurer codecConfigurer, Set messageBodyEncoders, + ObjectProvider codecCustomizers) { + return new FunctionRoutingFilter(functionCatalog, codecConfigurer.getReaders(), messageBodyEncoders, + codecCustomizers.orderedStream().toList()); } } diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java index 3d88f65d4..5944b3b8b 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage; @@ -50,6 +51,7 @@ import org.springframework.util.MimeType; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; @@ -71,12 +73,28 @@ public class FunctionRoutingFilter implements GlobalFilter, Ordered { private final Map messageBodyEncoders; + private final ExchangeStrategies exchangeStrategies; + public FunctionRoutingFilter(FunctionCatalog functionCatalog, List> messageReaders, Set messageBodyEncoders) { this.functionCatalog = functionCatalog; this.messageReaders = messageReaders; this.messageBodyEncoders = messageBodyEncoders.stream() .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); + this.exchangeStrategies = ExchangeStrategies.withDefaults(); + } + + public FunctionRoutingFilter(FunctionCatalog functionCatalog, List> messageReaders, + Set messageBodyEncoders, List codecCustomizers) { + this.functionCatalog = functionCatalog; + this.messageReaders = messageReaders; + this.messageBodyEncoders = messageBodyEncoders.stream() + .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); + + ExchangeStrategies.Builder exchangeStrategiesBuilder = ExchangeStrategies.builder(); + exchangeStrategiesBuilder + .codecs((codecs) -> codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); + this.exchangeStrategies = exchangeStrategiesBuilder.build(); } @Override @@ -149,7 +167,7 @@ protected Mono processRequest(ServerWebExchange exchange, FunctionInvocati CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders()); - return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { + return bodyInserter.insert(outputMessage, new BodyInserterContext(exchangeStrategies)).then(Mono.defer(() -> { ServerHttpResponse response = exchange.getResponse(); Mono messageBody = writeBody(response, outputMessage, outClass); HttpHeaders responseHeaders = response.getHeaders(); diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java index 660362da8..f459f1ef4 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java @@ -24,6 +24,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; @@ -37,6 +38,7 @@ import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; @@ -55,14 +57,29 @@ public class ModifyRequestBodyGatewayFilterFactory private final List> messageReaders; + private final ExchangeStrategies exchangeStrategies; + public ModifyRequestBodyGatewayFilterFactory() { super(Config.class); this.messageReaders = HandlerStrategies.withDefaults().messageReaders(); + this.exchangeStrategies = ExchangeStrategies.withDefaults(); } public ModifyRequestBodyGatewayFilterFactory(List> messageReaders) { super(Config.class); this.messageReaders = messageReaders; + this.exchangeStrategies = ExchangeStrategies.withDefaults(); + } + + public ModifyRequestBodyGatewayFilterFactory(List> messageReaders, + List codecCustomizers) { + super(Config.class); + this.messageReaders = messageReaders; + + ExchangeStrategies.Builder exchangeStrategiesBuilder = ExchangeStrategies.builder(); + exchangeStrategiesBuilder + .codecs((codecs) -> codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); + this.exchangeStrategies = exchangeStrategiesBuilder.build(); } @Override @@ -98,7 +115,7 @@ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { headers.set(HttpHeaders.CONTENT_TYPE, config.getContentType()); } CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); - return bodyInserter.insert(outputMessage, new BodyInserterContext()) + return bodyInserter.insert(outputMessage, new BodyInserterContext(exchangeStrategies)) // .log("modify_request", Level.INFO) .then(Mono.defer(() -> { ServerHttpRequest decorator = decorate(exchange, headers, outputMessage); diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java index 200bf14cd..481fdc902 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter; @@ -46,6 +47,7 @@ import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.server.ServerWebExchange; import static java.util.function.Function.identity; @@ -68,6 +70,8 @@ public class ModifyResponseBodyGatewayFilterFactory private final List> messageReaders; + private final ExchangeStrategies exchangeStrategies; + public ModifyResponseBodyGatewayFilterFactory(List> messageReaders, Set messageBodyDecoders, Set messageBodyEncoders) { super(Config.class); @@ -76,6 +80,23 @@ public ModifyResponseBodyGatewayFilterFactory(List> message .collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity())); this.messageBodyEncoders = messageBodyEncoders.stream() .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); + this.exchangeStrategies = ExchangeStrategies.withDefaults(); + } + + public ModifyResponseBodyGatewayFilterFactory(List> messageReaders, + Set messageBodyDecoders, Set messageBodyEncoders, + List codecCustomizers) { + super(Config.class); + this.messageReaders = messageReaders; + this.messageBodyDecoders = messageBodyDecoders.stream() + .collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity())); + this.messageBodyEncoders = messageBodyEncoders.stream() + .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); + + ExchangeStrategies.Builder exchangeStrategiesBuilder = ExchangeStrategies.builder(); + exchangeStrategiesBuilder + .codecs((codecs) -> codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); + this.exchangeStrategies = exchangeStrategiesBuilder.build(); } @Override @@ -237,7 +258,7 @@ public Mono writeWith(Publisher body) { BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders()); - return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { + return bodyInserter.insert(outputMessage, new BodyInserterContext(exchangeStrategies)).then(Mono.defer(() -> { Mono messageBody = writeBody(getDelegate(), outputMessage, outClass); HttpHeaders headers = getDelegate().getHeaders(); if (!headers.containsHeader(HttpHeaders.TRANSFER_ENCODING) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java index 7f698db02..fcea091fa 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java @@ -35,7 +35,7 @@ public BodyInserterContext() { } public BodyInserterContext(ExchangeStrategies exchangeStrategies) { - this.exchangeStrategies = exchangeStrategies; // TODO: support custom strategies + this.exchangeStrategies = exchangeStrategies; } @Override From 9acb6e9f01a2af20e23409059ba8155655ee0f12 Mon Sep 17 00:00:00 2001 From: qnnn Date: Fri, 8 May 2026 17:47:27 +0800 Subject: [PATCH 2/6] Deprecate body filter constructors without CodecCustomizer Signed-off-by: qnnn --- .../cloud/gateway/filter/FunctionRoutingFilter.java | 5 +++++ .../rewrite/ModifyRequestBodyGatewayFilterFactory.java | 10 ++++++++++ .../ModifyResponseBodyGatewayFilterFactory.java | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java index 5944b3b8b..00e68da20 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java @@ -75,6 +75,11 @@ public class FunctionRoutingFilter implements GlobalFilter, Ordered { private final ExchangeStrategies exchangeStrategies; + /** + * @deprecated Use {@link #FunctionRoutingFilter(FunctionCatalog, List, Set, List)} instead, + * which supports CodecCustomizer for consistent encoder/decoder. + */ + @Deprecated public FunctionRoutingFilter(FunctionCatalog functionCatalog, List> messageReaders, Set messageBodyEncoders) { this.functionCatalog = functionCatalog; diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java index f459f1ef4..03446c249 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java @@ -59,12 +59,22 @@ public class ModifyRequestBodyGatewayFilterFactory private final ExchangeStrategies exchangeStrategies; + /** + * @deprecated Use {@link #ModifyRequestBodyGatewayFilterFactory(List, List)} instead, + * which supports CodecCustomizer for consistent encoder/decoder. + */ + @Deprecated public ModifyRequestBodyGatewayFilterFactory() { super(Config.class); this.messageReaders = HandlerStrategies.withDefaults().messageReaders(); this.exchangeStrategies = ExchangeStrategies.withDefaults(); } + /** + * @deprecated Use {@link #ModifyRequestBodyGatewayFilterFactory(List, List)} instead, + * which supports CodecCustomizer for consistent encoder/decoder. + */ + @Deprecated public ModifyRequestBodyGatewayFilterFactory(List> messageReaders) { super(Config.class); this.messageReaders = messageReaders; diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java index 481fdc902..8829d2316 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java @@ -72,6 +72,11 @@ public class ModifyResponseBodyGatewayFilterFactory private final ExchangeStrategies exchangeStrategies; + /** + * @deprecated Use {@link #ModifyResponseBodyGatewayFilterFactory(List, Set, Set, List)} instead, + * which supports CodecCustomizer for consistent encoder/decoder. + */ + @Deprecated public ModifyResponseBodyGatewayFilterFactory(List> messageReaders, Set messageBodyDecoders, Set messageBodyEncoders) { super(Config.class); From 023546a41e11e2d5f5b39e02ff4fd37b0d510628 Mon Sep 17 00:00:00 2001 From: qnnn Date: Fri, 8 May 2026 17:48:35 +0800 Subject: [PATCH 3/6] Add codec customizer tests for body filters and function routing. Signed-off-by: qnnn --- .../filter/FunctionRoutingFilterTests.java | 25 ++++++++- ...yRequestBodyGatewayFilterFactoryTests.java | 29 +++++++++- ...ResponseBodyGatewayFilterFactoryTests.java | 26 ++++++++- .../TestCodecCustomizerConfiguration.java | 55 +++++++++++++++++++ 4 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/test/TestCodecCustomizerConfiguration.java diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java index 3ca914fc7..75e162621 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java @@ -18,6 +18,7 @@ import java.net.URI; import java.util.Locale; +import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.cloud.gateway.test.TestCodecCustomizerConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; @@ -57,9 +59,23 @@ public void functionRoutingFilterWorks() { .consumeWith(res -> assertThat(res.getResponseBody()).isEqualTo("HELLO")); } + @Test + public void codecCustomizerWorksWithFunctionRouting() { + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/").build(true).toUri(); + + testClient.post() + .uri(uri) + .bodyValue(Map.of("codec", "v1")) + .header("Host", "www.codeccustomizerworks.org") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectBody(String.class) + .consumeWith(res -> assertThat(res.getResponseBody()).isEqualTo("{\"codec\":\"v3\"}")); + } + @EnableAutoConfiguration @SpringBootConfiguration - @Import(DefaultTestConfig.class) + @Import({ DefaultTestConfig.class, TestCodecCustomizerConfiguration.class }) public static class TestConfig { @Bean @@ -67,11 +83,18 @@ Function upper() { return s -> s.toUpperCase(Locale.ROOT); } + @Bean + Function passThrough() { + return o -> o; + } + @Bean public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("function_routing_filter_java_test", r -> r.path("/").and().host("www.functionroutingfilterjava.org").uri("fn://upper")) + .route("function_routing_codec_customizer_test", + r -> r.path("/").and().host("www.codeccustomizerworks.org").uri("fn://passThrough")) .build(); } diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java index b1d161946..c8e06554f 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.filter.factory.rewrite; import java.util.Locale; +import java.util.Map; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -28,6 +29,7 @@ import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.cloud.gateway.test.TestCodecCustomizerConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.core.ParameterizedTypeReference; @@ -109,9 +111,26 @@ public void modifyRequestBodyParameterizedTypeReference() { .isEqualTo("FOO_BAR_BAZ"); } + @Test + public void codecCustomizerWorksWithModifyRequestBody() { + testClient.post() + .uri("/post") + .header("Host", "www.codeccustomizerworks.org") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) + .body(BodyInserters.fromValue("request")) + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.OK) + .expectBody() + .jsonPath("headers.Content-Type") + .isEqualTo(MediaType.APPLICATION_JSON_VALUE) + .jsonPath("data") + .isEqualTo("{\"codec\":\"v2\"}"); + } + @EnableAutoConfiguration @SpringBootConfiguration - @Import(DefaultTestConfig.class) + @Import({ DefaultTestConfig.class, TestCodecCustomizerConfiguration.class }) public static class TestConfig { @Value("${test.uri}") @@ -157,6 +176,14 @@ public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return Mono.just(body.replaceAll(" ", "_").toUpperCase(Locale.ROOT)); })) .uri(uri)) + .route("modify_request_body_codec_customizer_test", + r -> r.order(-1) + .host("**.codeccustomizerworks.org") + .filters(f -> f.modifyRequestBody(String.class, Object.class, + MediaType.APPLICATION_JSON_VALUE, (serverWebExchange, body) -> { + return Mono.just(Map.of("codec", "v1")); + })) + .uri(uri)) .build(); } diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryTests.java index 8165c6ba5..c4258f2f5 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactoryTests.java @@ -30,6 +30,7 @@ import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.cloud.gateway.test.TestCodecCustomizerConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; @@ -101,9 +102,22 @@ public void modifyResponseBodyToLarge() { .isEqualTo("Content Too Large"); } + @Test + public void codecCustomizerWorksWithModifyResponseBody() { + URI uri = UriComponentsBuilder.fromUriString(this.baseUri + "/").build(true).toUri(); + + testClient.get() + .uri(uri) + .header("Host", "www.codeccustomizerworks.org") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectBody() + .json("{\"codec\":\"v2\"}"); + } + @EnableAutoConfiguration @SpringBootConfiguration - @Import(DefaultTestConfig.class) + @Import({ DefaultTestConfig.class, TestCodecCustomizerConfiguration.class }) public static class TestConfig { @Value("${test.uri}") @@ -143,6 +157,16 @@ public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { return Mono.just("Modified response"); })) .uri(uri)) + .route("modify_response_body_codec_customizer_test", + r -> r.path("/") + .and() + .host("www.codeccustomizerworks.org") + .filters(f -> f.prefixPath("/httpbin") + .modifyResponseBody(String.class, Object.class, MediaType.APPLICATION_JSON_VALUE, + (webExchange, originalResponse) -> { + return Mono.just(Map.of("codec", "v1")); + })) + .uri(uri)) .build(); } diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/test/TestCodecCustomizerConfiguration.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/test/TestCodecCustomizerConfiguration.java new file mode 100644 index 000000000..b0db6f70c --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/test/TestCodecCustomizerConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.test; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.module.SimpleModule; + +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Nan Chiu + */ +@Configuration(proxyBeanMethods = false) +public class TestCodecCustomizerConfiguration { + + @Bean + public JsonMapperBuilderCustomizer codecCustomizingJsonMapperBuilderCustomizer() { + return jsonMapperBuilder -> { + SimpleModule module = new SimpleModule(); + + module.addSerializer(String.class, new ValueSerializer<>() { + @Override + public void serialize(String value, JsonGenerator gen, SerializationContext ctxt) + throws JacksonException { + if ("v1".equals(value)) { + value = "v2"; + } + gen.writeString(value); + } + }); + + jsonMapperBuilder.addModule(module); + }; + } + +} From 709a0c20fe857c6d485bbd451ce05f6a3be73c9b Mon Sep 17 00:00:00 2001 From: qnnn Date: Fri, 8 May 2026 18:05:41 +0800 Subject: [PATCH 4/6] fix 'Add codec customizer tests for body filters and function routing.' Signed-off-by: qnnn --- .../cloud/gateway/filter/FunctionRoutingFilterTests.java | 2 +- .../rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java index 75e162621..65fdcc5af 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilterTests.java @@ -70,7 +70,7 @@ public void codecCustomizerWorksWithFunctionRouting() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectBody(String.class) - .consumeWith(res -> assertThat(res.getResponseBody()).isEqualTo("{\"codec\":\"v3\"}")); + .consumeWith(res -> assertThat(res.getResponseBody()).isEqualTo("{\"codec\":\"v2\"}")); } @EnableAutoConfiguration diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java index c8e06554f..dfa12b245 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java @@ -44,7 +44,7 @@ /** * @author Junghoon Song */ -@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.http.codecs.max-in-memory-size=13") +@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.http.codecs.max-in-memory-size=40") @DirtiesContext public class ModifyRequestBodyGatewayFilterFactoryTests extends BaseWebClientTests { From a51657dd089041560de4e4f7167ba47f7806699b Mon Sep 17 00:00:00 2001 From: qnnn Date: Thu, 21 May 2026 12:02:06 +0800 Subject: [PATCH 5/6] polish ExchangeStrategies initialization. Signed-off-by: qnnn --- .../gateway/filter/FunctionRoutingFilter.java | 17 ++----------- ...ModifyRequestBodyGatewayFilterFactory.java | 24 +++---------------- ...odifyResponseBodyGatewayFilterFactory.java | 19 ++------------- .../gateway/support/BodyInserterContext.java | 16 +++++++++++++ 4 files changed, 23 insertions(+), 53 deletions(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java index 00e68da20..2d911245d 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/FunctionRoutingFilter.java @@ -75,18 +75,9 @@ public class FunctionRoutingFilter implements GlobalFilter, Ordered { private final ExchangeStrategies exchangeStrategies; - /** - * @deprecated Use {@link #FunctionRoutingFilter(FunctionCatalog, List, Set, List)} instead, - * which supports CodecCustomizer for consistent encoder/decoder. - */ - @Deprecated public FunctionRoutingFilter(FunctionCatalog functionCatalog, List> messageReaders, Set messageBodyEncoders) { - this.functionCatalog = functionCatalog; - this.messageReaders = messageReaders; - this.messageBodyEncoders = messageBodyEncoders.stream() - .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); - this.exchangeStrategies = ExchangeStrategies.withDefaults(); + this(functionCatalog, messageReaders, messageBodyEncoders, List.of()); } public FunctionRoutingFilter(FunctionCatalog functionCatalog, List> messageReaders, @@ -95,11 +86,7 @@ public FunctionRoutingFilter(FunctionCatalog functionCatalog, List codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); - this.exchangeStrategies = exchangeStrategiesBuilder.build(); + this.exchangeStrategies = BodyInserterContext.buildExchangeStrategies(codecCustomizers); } @Override diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java index 03446c249..310c2aade 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactory.java @@ -59,37 +59,19 @@ public class ModifyRequestBodyGatewayFilterFactory private final ExchangeStrategies exchangeStrategies; - /** - * @deprecated Use {@link #ModifyRequestBodyGatewayFilterFactory(List, List)} instead, - * which supports CodecCustomizer for consistent encoder/decoder. - */ - @Deprecated public ModifyRequestBodyGatewayFilterFactory() { - super(Config.class); - this.messageReaders = HandlerStrategies.withDefaults().messageReaders(); - this.exchangeStrategies = ExchangeStrategies.withDefaults(); + this(HandlerStrategies.withDefaults().messageReaders(), List.of()); } - /** - * @deprecated Use {@link #ModifyRequestBodyGatewayFilterFactory(List, List)} instead, - * which supports CodecCustomizer for consistent encoder/decoder. - */ - @Deprecated public ModifyRequestBodyGatewayFilterFactory(List> messageReaders) { - super(Config.class); - this.messageReaders = messageReaders; - this.exchangeStrategies = ExchangeStrategies.withDefaults(); + this(messageReaders, List.of()); } public ModifyRequestBodyGatewayFilterFactory(List> messageReaders, List codecCustomizers) { super(Config.class); this.messageReaders = messageReaders; - - ExchangeStrategies.Builder exchangeStrategiesBuilder = ExchangeStrategies.builder(); - exchangeStrategiesBuilder - .codecs((codecs) -> codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); - this.exchangeStrategies = exchangeStrategiesBuilder.build(); + this.exchangeStrategies = BodyInserterContext.buildExchangeStrategies(codecCustomizers); } @Override diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java index 8829d2316..1b98c81b8 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java @@ -72,20 +72,9 @@ public class ModifyResponseBodyGatewayFilterFactory private final ExchangeStrategies exchangeStrategies; - /** - * @deprecated Use {@link #ModifyResponseBodyGatewayFilterFactory(List, Set, Set, List)} instead, - * which supports CodecCustomizer for consistent encoder/decoder. - */ - @Deprecated public ModifyResponseBodyGatewayFilterFactory(List> messageReaders, Set messageBodyDecoders, Set messageBodyEncoders) { - super(Config.class); - this.messageReaders = messageReaders; - this.messageBodyDecoders = messageBodyDecoders.stream() - .collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity())); - this.messageBodyEncoders = messageBodyEncoders.stream() - .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); - this.exchangeStrategies = ExchangeStrategies.withDefaults(); + this(messageReaders, messageBodyDecoders, messageBodyEncoders, List.of()); } public ModifyResponseBodyGatewayFilterFactory(List> messageReaders, @@ -97,11 +86,7 @@ public ModifyResponseBodyGatewayFilterFactory(List> message .collect(Collectors.toMap(MessageBodyDecoder::encodingType, identity())); this.messageBodyEncoders = messageBodyEncoders.stream() .collect(Collectors.toMap(MessageBodyEncoder::encodingType, identity())); - - ExchangeStrategies.Builder exchangeStrategiesBuilder = ExchangeStrategies.builder(); - exchangeStrategiesBuilder - .codecs((codecs) -> codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); - this.exchangeStrategies = exchangeStrategiesBuilder.build(); + this.exchangeStrategies = BodyInserterContext.buildExchangeStrategies(codecCustomizers); } @Override diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java index fcea091fa..71e08abcc 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/support/BodyInserterContext.java @@ -21,8 +21,10 @@ import java.util.Map; import java.util.Optional; +import org.springframework.boot.http.codec.CodecCustomizer; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.client.ExchangeStrategies; @@ -38,6 +40,20 @@ public BodyInserterContext(ExchangeStrategies exchangeStrategies) { this.exchangeStrategies = exchangeStrategies; } + /** + * Build an {@link ExchangeStrategies} instance and apply all registered + * {@link CodecCustomizer CodecCustomizers}. + */ + public static ExchangeStrategies buildExchangeStrategies(List codecCustomizers) { + if (ObjectUtils.isEmpty(codecCustomizers)) { + return ExchangeStrategies.withDefaults(); + } + ExchangeStrategies.Builder exchangeStrategiesBuilder = ExchangeStrategies.builder(); + exchangeStrategiesBuilder + .codecs((codecs) -> codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); + return exchangeStrategiesBuilder.build(); + } + @Override public List> messageWriters() { return exchangeStrategies.messageWriters(); From cec10a4209cb8f6610a1cb1d293afd6cf8dff33f Mon Sep 17 00:00:00 2001 From: qnnn Date: Thu, 21 May 2026 23:30:51 +0800 Subject: [PATCH 6/6] polish 'Add codec customizer tests for body filters and function routing' Signed-off-by: qnnn --- .../rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java index dfa12b245..04bca1367 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyRequestBodyGatewayFilterFactoryTests.java @@ -44,7 +44,7 @@ /** * @author Junghoon Song */ -@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.http.codecs.max-in-memory-size=40") +@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.http.codecs.max-in-memory-size=15") @DirtiesContext public class ModifyRequestBodyGatewayFilterFactoryTests extends BaseWebClientTests {