111

While sending a file I receive an array of bytes. I always have a problem with webflux to receive an array. the error thrown as below :

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

Do you now how to resolve that in webflux ?

Kiran Bhagwat
  • 574
  • 1
  • 6
  • 13
taveced
  • 1,133
  • 2
  • 7
  • 6
  • 2
    https://github.com/spring-projects/spring-framework/issues/23961 – Martin Tarjányi Jan 14 '20 at 19:15
  • You can also refer: https://www.baeldung.com/spring-webflux-databufferlimitexception – user2173372 Dec 19 '22 at 12:09
  • Most (all?) of the answers suggest ways to configure the size of the buffer. I presume there is no alternative. This feels like an WebClient API failure. The implementation should grow the backing buffer when required much like an ArrayList will grow the backing array when required. – JohnC Dec 22 '22 at 16:37

14 Answers14

151

This worked for me:

  1. Create a @Bean in one of your configuration classes or the main SpringBootApplication class:

    @Bean
    public WebClient webClient() {
        final int size = 16 * 1024 * 1024;
        final ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(size))
            .build();
        return WebClient.builder()
            .exchangeStrategies(strategies)
            .build();
    }
    
  2. Next, go to your desired class where you want to use the WebClient:

    @Service
    public class TestService {
    
        @Autowired
        private WebClient webClient;
    
        public void test() {
            String out = webClient
                .get()
                .uri("/my/api/endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .block();
    
            System.out.println(out);
        }
    }
    
blacktide
  • 10,654
  • 8
  • 33
  • 53
Guru Cse
  • 2,805
  • 2
  • 18
  • 15
  • 6
    Don't know why, this the only solution that worked on WebFlux 2.3.2.RELEASE – zerologiko Sep 28 '20 at 23:10
  • 1
    If you have declared a custom web configuration (ex. by implementing WebMvcConfigurer), you override the option in properties.yaml. Thus, if you set the `maxInMemorySize` property, you configure directly your webclient, bypassing the Web Configuration of spring-boot. – Georgios Syngouroglou Dec 17 '21 at 01:00
82

I suppose this issue is about adding a new spring.codec.max-in-memory-size configuration property in Spring Boot. Add it to the application.yml file like:

spring:
  codec:
    max-in-memory-size: 10MB
blacktide
  • 10,654
  • 8
  • 33
  • 53
David
  • 827
  • 5
  • 6
  • 7
    I'm using this in my spring boot app config, however it doesn't help. – mareck_ste Jun 17 '20 at 16:29
  • 11
    @mareck_ste Hi! Maybe you are using some custom configuration that overrides this option. E.g. you have WebClient configuration, so just set this 'maxInMemorySize' property in that WebClientBuilder.exchangeStrategies() – David Jun 18 '20 at 17:14
  • 3
    @mareck_ste indeed, I have the same for spring-boot-starter-webflux 2.3.5-RELEASE . Check out this [excelent answer](https://stackoverflow.com/a/59392022/1610553) – bernard paulus Nov 12 '20 at 23:16
  • Thank you Bernard, modifying the codecs worked. – Bobin May 25 '22 at 07:50
  • 3
    If the config-based solution above does not work - the following might explain why - you need to be using the pre-configured Spring WebClient.Builder and not creating your own https://github.com/spring-projects/spring-boot/issues/27836 – kellyfj Jul 07 '22 at 20:57
  • In my case, I needed to put this setting in the API gateway settings rather than the endpoint service settings. – PowerAktar Oct 06 '22 at 12:54
30

Set the maximum bytes (in megabytes) in your Spring Boot application.properties configuration file like below:

spring.codec.max-in-memory-size=20MB
blacktide
  • 10,654
  • 8
  • 33
  • 53
Nicodemus Ojwee
  • 744
  • 6
  • 12
  • 1
    If that doesn't work this might be why https://github.com/spring-projects/spring-boot/issues/27836 – kellyfj Jul 07 '22 at 20:58
19

worked for me

webTestClient.mutate()
  .codecs(configurer -> configurer
          .defaultCodecs()
          .maxInMemorySize(16 * 1024 * 1024))
  .build().get()
  .uri("/u/r/l")
  .exchange()
  .expectStatus()
  .isOk()
Younes El Ouarti
  • 2,200
  • 2
  • 20
  • 31
Jayshree
  • 201
  • 2
  • 5
17

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

Datum Geek
  • 1,358
  • 18
  • 23
  • 4
    Why are you showing the addResourceHandlers method? Is it related to the question somehow? – Zucca Jul 26 '21 at 19:35
16

Instead of retrieving data at once, you can stream:

Mono<String> string = webClient.get()
    .uri("end point of an API")
    .retrieve()
    .bodyToFlux(DataBuffer.class)
    .map(buffer -> {
        String string = buffer.toString(Charset.forName("UTF-8"));
        DataBufferUtils.release(buffer);
        return string;
    });

Alternatively convert to stream:

    .map(b -> b.asInputStream(true))
    .reduce(SequenceInputStream::new)
    .map(stream -> {
        // consume stream
        stream.close();
        return string;
    });

In most cases you don't want to really aggregate the stream, rather than processing it directly. The need to load huge amount of data in memory is mostly a sign to change the approach to more reactive one. JSON- and XML-Parsers have streaming interfaces.

Tires
  • 1,529
  • 16
  • 27
  • 2
    Where is `buffer` defined? – Ashok Koyi Sep 02 '20 at 16:16
  • 1
    @AshokKoyi I mixed up two variants before (fixed) – Tires Sep 02 '20 at 16:34
  • 2
    Did you actually measure the overall memory footprint? i.e even after releasing the data buffer, unless you consume the final stream, the memory will still piled up since you are using reduction till you receive last byte. So, I am not sure whether you will have any advantage by using this approach – Ashok Koyi Sep 02 '20 at 16:40
  • Option (1) is problematic as you are not guaranteed that you received all the data before you can convert it the bytes to string. Its possible that we only read 1 byte of a 4 byte UTF-8 character during the map operation – Ashok Koyi Sep 02 '20 at 16:43
  • If the partial data can be utilised & the buffer be freed, that's the best thing that can be done. Using reductions is a bad idea as we are filling the memory till complete reduction is done, which defeats the point of using buffers as and when they come. Option (1) is valid, but it only works as a byte array, not as string – Ashok Koyi Sep 02 '20 at 17:18
  • Looks like Jackson bombs when it comes to streamed parsing based on this https://stackoverflow.com/questions/38416158/how-to-properly-parse-streaming-json-with-jackson – Ashok Koyi Sep 02 '20 at 17:21
  • I absolutely aggree, as told: "In most cases you don't want to really aggregate the stream" – Tires Sep 03 '20 at 17:14
  • 1
    The idea is the best one among all answers here. The only thing I don't like in the presented example code is this call to `DataBufferUtils.release(buffer)`. Smells like manual memory management which is so against general Java. – Krzysztof Tomaszewski Mar 14 '23 at 16:01
7

This worked for me

val exchangeStrategies = ExchangeStrategies.builder()
                .codecs { configurer: ClientCodecConfigurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024) }.build()
        return WebClient.builder().exchangeStrategies(exchangeStrategies).build()
Mohnish
  • 1,010
  • 1
  • 12
  • 20
koushik v
  • 71
  • 1
  • 1
3

Another alternative could be creating a custom CodecCustomizer, which is going to be applied to both WebFlux and WebClient at the same time:

@Configuration
class MyAppConfiguration {

    companion object {
        private const val MAX_MEMORY_SIZE = 50 * 1024 * 1024 // 50 MB
    }

    @Bean
    fun codecCustomizer(): CodecCustomizer {
        return CodecCustomizer {
            it.defaultCodecs()
                .maxInMemorySize(MAX_MEMORY_SIZE)
        }
    }
}
walrus03
  • 151
  • 1
  • 6
1

As of Spring Boot 2.3.0, there is now a dedicated configuration property for the Reactive Elasticsearch REST client.

You can use the following configuration property to set a specific memory limit for the client.

spring.data.elasticsearch.client.reactive.max-in-memory-size= The already existing spring.codec.max-in-memory-size property is separate and only affects other WebClient instances in the application.

  • This is replaces with spring.elasticsearch.webclient.max-in-memory-size=512MB [see](https://stackoverflow.com/a/73921961/5557885) – rajadilipkolli Oct 01 '22 at 21:47
0

For those who had no luck with the myriad of beans, customizers, and properties that could be added to solve this problem, check whether you have defined a bean extending WebFluxConfigurationSupport. If you have, it will disable the autoconfiguration version of the same bean (my personal experience, Boot 2.7.2), somewhere under which Spring loads properties such as the suggested spring.codec.max-in-memory-size. For this solution to work you need to have also configured this property correctly.

To test if this is the cause of your problems, remove your WebFluxConfigurationSupport implementation temporarily. The long term fix that worked for me was to use configuration beans to override attributes for the autoconfigured bean. In my case, WebFluxConfigurer had all of the same methods available and was a drop-in replacement for WebFluxConfigurationSupport. Large JSON messages are now decoding for me as configured.

Bit Fracture
  • 651
  • 1
  • 9
  • 24
0

If you dont want to change the default settings for webClient globally, you can use the following approach to manually merge multiple DataBuffers

webClient
        .method(GET)
        .uri("<uri>")
        .exchangeToMono(response -> {
          return response.bodyToFlux(DataBuffer.class)
              .switchOnFirst((firstBufferSignal, responseBody$) -> {
                assert firstBufferSignal.isOnNext();
                return responseBody$
                    .collect(() -> requireNonNull(firstBufferSignal.get()).factory().allocateBuffer(), (accumulator, curr) -> {
                      accumulator.ensureCapacity(curr.readableByteCount());
                      accumulator.write(curr);
                      DataBufferUtils.release(curr);
                    })
                    .map(accumulator -> {
                      final var responseBodyAsStr = accumulator.toString(UTF_8);
                      DataBufferUtils.release(accumulator);
                      return responseBodyAsStr;
                    });
              })
              .single();
        });

The above code aggregates all the DataBuffers into a single DataBuffer & then converts the final DataBuffer into a string. Please note that this answer wont work as DataBuffers received might not have all the bytes to construct a character (incase of UTF-8 characters, each character can take upto 4 bytes). So we cant convert intermediate DataBuffers into String as the bytes towards the end of buffer might have only part of the bytes required to construct a valid character

Note that this loads all the response DataBuffers into memory but unlike changing global settings for the webClient across the whole application. You can use this option to read complete response only where you want i.e you can narrow down & pick this option only where you expect large responses.

Ashok Koyi
  • 5,327
  • 8
  • 41
  • 50
0

As of Spring boot 2.7.x we should use below property to set the memory size to webclient which is used internally in reactive ElasticSearch

spring.elasticsearch.webclient.max-in-memory-size=512MB

rajadilipkolli
  • 3,475
  • 2
  • 26
  • 49
0

Just add below code in your springboot main class.

@Bean
public WebClient getWebClient() {
    return WebClient.builder()
            .baseUrl("Your_SERVICE_URL")
            .codecs(configurer -> configurer
                      .defaultCodecs()
                      .maxInMemorySize(16 * 1024 * 1024))
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
}

This work for me.

0

Note that if you're using custom decoder, you need to set the maximum in-memory size in there, for instance:

Integer CODEC_20_MB_SIZE = 20 * 1024 * 1024;
WebClient.builder()
    .codecs(clientCodecConfigurer -> {
        var codec = new Jackson2JsonDecoder();
        codec.setMaxInMemorySize(CODEC_20_MB_SIZE);
        clientCodecConfigurer.customCodecs().register(codec);
        clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
    });
mpartan
  • 1,296
  • 1
  • 14
  • 30