Seems caused by https://github.com/spring-projects/spring-framework/commit/89d053d7f45fb1886b044be5e3276927d7a7798e / https://github.com/spring-projects/spring-framework/issues/23884.
After upgrading from Spring Boot 2.2.0 to 2.2.1, WebClient started throwing org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
when calling a JSON REST API (that has a large response size).
From the linked documentation (https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/webflux.adoc#limits), it's not clear what configuration should be changed to make our API calls work again.
Is this expected behavior?
Full stack trace:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:101)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Body from POST <redacted> [DefaultClientResponse]
Stack trace:
at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:101)
at org.springframework.core.io.buffer.LimitedDataBufferList.updateCount(LimitedDataBufferList.java:94)
at org.springframework.core.io.buffer.LimitedDataBufferList.add(LimitedDataBufferList.java:59)
at reactor.core.publisher.MonoCollect$CollectSubscriber.onNext(MonoCollect.java:119)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:192)
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:192)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:213)
at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:346)
at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:348)
at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:572)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:102)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:438)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:326)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:300)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1478)
at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1227)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1274)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:503)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:442)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:281)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1422)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:931)
at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:792)
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:502)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:407)
at io.netty.util.concurrent.SingleThreadEventExecutor$6.run(SingleThreadEventExecutor.java:1050)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:834)
Comment From: bclozel
If you are using Spring Boot and creating your WebClient using a WebClient.Builder configured by Spring Boot (you can get it injected), you can use the new configuration property spring.codec.max-in-memory-size, see spring-projects/spring-boot#18828.
Otherwise if you’re creating that client from scratch you need to configure the codecs like:
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 10)).build();
WebClient webClient = WebClient.builder().exchangeStrategies(exchangeStrategies).build();
Comment From: totof3110
Thanks @bclozel . Since I use the autowired WebClient.Builder
, I've tried setting spring.codec.max-in-memory-size
to a large size (500MB) but am still getting that error.
We happen to be using Spring HATEOAS and I wonder if something there is overriding this configuration. In particular there's class org.springframework.hateoas.config.WebClientConfigurer
which transforms the WebClients with:
webClient.mutate().exchangeStrategies(hypermediaExchangeStrategies()).build()
I've already noticed that this seems to mess up the ObjectMapper
used in the WebClient
and drop all the com.fasterxml.jackson.databind.Module
s that were previously autoconfigured.
Could that be the reason?
Comment From: bclozel
We need to improve the documentation for the client side of things. This should help non Spring Boot users and developers building their own WebClient
instances.
Now for HATEOAS, we're probably missing an extension point to allow it to customize the codecs without replacing them - we should provide ClientCodecConfigurer
with an additional method (like we did for ServerCodecConfigurer
).
Comment From: totof3110
Thanks @bclozel !
Comment From: jhoeller
@bclozel Is it intentional that we're introducing an ExchangeStrategies.mutate()
method which is deprecated from day one? Is this meant to be a hint for other API designers?
Comment From: bclozel
@jhoeller yes this is intentional. I’ve added a new method on the WebClient.Builder
that takes itself an ExchangeStrategies.Builder
to customize those. It is hard to go back from ExchangeStrategies
to its builder form because of the nature of the underlying infrastructure.
We’ve added clone and mutate methods to solve the current problem first without breaking the contract and we’ll remove all that in a future release.
I’ve added comments along those lines in the commit message itself.
Comment From: bclozel
This change is breaking integrations with other projects. Reverting for now and rescheduling to another version.
Comment From: chrisatrotter
Hi, This is most likely the wrong place to ask such a question, but on the topic of customising the memory size I was wondering how would you do a unit test, boundary test, to check that the webclient does a request when it is within the memory limit and breaks when the requests exceeds the memory limit?
Comment From: rstoyanchev
We have such tests for each codec (e.g. for Jackson) so you shouldn't have to test that. All you should test is how your app is configured. That aside you can probably use WebTestClient
without a server and pass the exact chunks.
Comment From: chrisatrotter
@rstoyanchev, you're right. I just wanted to create a simple unit test to ensure that the the custom memory size was set correctly. This is a snippet of the solution I managed to do with some Kotlin code:
@SpringBootTest
class MyTest(@Value("100") private val maxMemorySize: Int) {
...
@Test
fun `DataBufferLimitException is thrown when 'memoryBufferData' is greater than the custom 'maxMemorySize (100)'`() {
// Given:
val memoryBufferData = "A".repeat(maxMemorySize)
val toJson = "\"$memoryBufferData\""
val mockResponse = MockResponse().setBody(toJson)
val webClient = MockWebServer()
.also { it.enqueue(mockResponse) }
.also { it.start() }
// Then:
assertThrows<DataBufferLimitException> {
webClient.get(path = "/test", entityType = Any::class)
}
}
@Test
fun `Response is the same as 'memoryBufferData' when memoryBuffer is within the custom 'maxMemorySize (100)'`() {
// Given:
val belowMaxMemorySize = maxMemorySize - 2
val memoryBufferData = "A".repeat(belowMaxMemorySize)
val toJson = "\"$memoryBufferData\""
val mockResponse = MockResponse().setBody(toJson)
val webClient = MockWebServer()
.also { it.enqueue(mockResponse) }
.also { it.start() }
// When:
val response = webClient.get(path = "/test", entityType = Any::class)
// Then:
assertEquals(response, memoryBufferData)
...
}
Thank you for taking the time @rstoyanchev !
Comment From: datumgeek
i was getting this error for a simple RestController (i post a large json string).
here is how i successfully changed the maxInMemorySize
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.ResourceHandlerRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class WebfluxConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui.html**")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024);
}
}
this was surprisingly hard to find
Comment From: bclozel
@datumgeek we've got this covered in the reference docs. Don't hesitate to share improvement ideas!
Thanks!
Comment From: datumgeek
@bclozel - thank you for the reference !!
i did see that part of the doc... but from that description, i wasn't able to figure out how to fix the error in the rest controller... maybe it needs a reference to a code sample? i'm guessing this is a fairly common issue for folks developing rest controllers...
the answer was also not present in the stackoverflow questions i found when searching. i tried to update a few of them 😄
once you have the recipe, it is very easy 😉
Comment From: rstoyanchev
maybe it needs a reference to a code sample?
There is a code sample in the config section. There are links to it from the referenced part of the doc?
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web-reactive.html#webflux-client-builder-maxinmemorysize
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web-reactive.html#webflux-config-message-codecs
Comment From: datumgeek
@rstoyanchev - maybe i'm just being dense 😉
i was trying to configure the maxInMemoryBytes
for the rest controller
i was thinking it would be good if there was a code sample for how to do this via WebFluxConfigurer
like maybe if it said specifically, if you are trying to change the maxInMemoryBytes for a RestController do this:
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class WebfluxConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024);
}
}
Comment From: rstoyanchev
No that's fine, I made an improvement.
Comment From: patanric
In my case it was jensgram's answer on Stackoverflow that helped for the original problem:
WebClient.builder()
.…
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer
.defaultCodecs()
.maxInMemorySize(16 * 1024 * 1024))
.build())
.build();
I hope this helps the next engineer that comes across here...
Comment From: cfw
refer https://github.com/spring-cloud/spring-cloud-gateway/issues/1658#issuecomment-695196754
Comment From: Vity01
I still don't understand, why there is no simple configuration property to set the maxInMemory size for all webclient client instances globally. Using exchange strategies is too clumsy to setup such simple thing.
Comment From: lackovic
Can anyone confirm the default limit is still 256KB?
In my Spring Boot 2.4.1
application the error message is:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 1048576
which seems to indicate the default limit is actually 1MB.
Doubling the limit by adding the following filter (as indicated in this section of the documentation) did not fix the issue:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024);
}
}
Doubling the limit while building the WebClient
(as shown in this other section of the documentation) fixed the issue:
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
Comment From: alex-albericio
I think there's some confusion here because there are two parts to be configured, the client and the server. Depending on your use-cases you might need to configure just the client (if you are making requests), the server (if your are receiving requests as a resource server) or both.
The server can be configured by overriding WebFluxConfigurer.configureHttpMessageCodecs where you get a ServerCodecConfigurer.
The client can be configured with the WebClient.Builder.codecs where you get a ClientCodecConfigurer.