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.Modules 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.