Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ExchangeStrategies customizations in WebClient #23961

Closed
totof3110 opened this issue Nov 9, 2019 · 21 comments
Closed

Allow ExchangeStrategies customizations in WebClient #23961

totof3110 opened this issue Nov 9, 2019 · 21 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: enhancement A general enhancement
Milestone

Comments

@totof3110
Copy link

totof3110 commented Nov 9, 2019

Seems caused by 89d053d / #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)
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 9, 2019
@bclozel
Copy link
Member

bclozel commented Nov 10, 2019

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();

@totof3110
Copy link
Author

totof3110 commented Nov 10, 2019

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?

@bclozel bclozel self-assigned this Nov 11, 2019
@bclozel bclozel added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Nov 11, 2019
@bclozel bclozel added this to the 5.2.2 milestone Nov 11, 2019
@bclozel
Copy link
Member

bclozel commented Nov 11, 2019

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

@bclozel bclozel changed the title "Exceeded limit on max bytes to buffer : 262144" with WebClient Allow ExchangeStrategies customizations in WebClient Nov 29, 2019
@bclozel bclozel added the for: backport-to-5.1.x Marks an issue as a candidate for backport to 5.1.x label Nov 29, 2019
@spring-projects-issues spring-projects-issues added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-5.1.x Marks an issue as a candidate for backport to 5.1.x labels Nov 29, 2019
@totof3110
Copy link
Author

Thanks @bclozel !

@jhoeller
Copy link
Contributor

@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?

@bclozel
Copy link
Member

bclozel commented Dec 1, 2019

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

@bclozel bclozel removed this from the 5.2.2 milestone Dec 2, 2019
@bclozel
Copy link
Member

bclozel commented Dec 2, 2019

This change is breaking integrations with other projects. Reverting for now and rescheduling to another version.

@bclozel bclozel reopened this Dec 2, 2019
@rstoyanchev rstoyanchev added this to the 5.2.2 milestone Dec 2, 2019
rstoyanchev added a commit that referenced this issue Dec 2, 2019
rstoyanchev pushed a commit that referenced this issue Dec 2, 2019
bclozel added a commit that referenced this issue Dec 2, 2019
As a follow-up of gh-23961, this change provides a way for custom codecs
to align with the default codecs' behavior on common features like
buffer size limits and logging request details.

Closes gh-24118
Co-authored-by: Rossen Stoyanchev <rstoyanchev@pivotal.io>
bclozel added a commit that referenced this issue Dec 2, 2019
As a follow-up of gh-23961, this change provides a way for custom codecs
to align with the default codecs' behavior on common features like
buffer size limits and logging request details.

Closes gh-24119
Co-authored-by: Rossen Stoyanchev <rstoyanchev@pivotal.io>
pacphi added a commit to cf-toolsuite/cf-butler that referenced this issue Dec 10, 2019
…naged version dependencies

* Fix issue with H2 by updating version to r2dbc-h2-0.8.1.RELEASE
* Adjust WebClientConfig.java as per spring-projects/spring-framework#23961 (comment) and increase spring.codec.max-in-memory-size property value in application.yml to 512000000
* Adjust SpaceUsersTask.java to flatMap on save
@chrisatrotter
Copy link

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?

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Feb 28, 2020

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.

@chrisatrotter
Copy link

@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 !

@datumgeek
Copy link

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

@bclozel
Copy link
Member

bclozel commented Mar 31, 2020

@datumgeek we've got this covered in the reference docs. Don't hesitate to share improvement ideas!

Thanks!

@datumgeek
Copy link

datumgeek commented Mar 31, 2020

@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 😉

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Mar 31, 2020

@datumgeek
Copy link

datumgeek commented Mar 31, 2020

@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);
    }
}

rstoyanchev added a commit that referenced this issue Mar 31, 2020
@rstoyanchev
Copy link
Contributor

No that's fine, I made an improvement.

@patanric
Copy link

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

@cfw
Copy link

cfw commented Sep 19, 2020

@Vity01
Copy link

Vity01 commented Jan 18, 2021

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.

@lackovic
Copy link

lackovic commented Jul 1, 2021

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();

@alex-albericio
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests