From 5abf24e7d75e9f81746a92e7b3e1ae51204abbd2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 28 Oct 2019 21:32:15 +0000 Subject: [PATCH] Expose maxInMemorySize via CodecConfigurer Centralized maxInMemorySize exposed via CodecConfigurer along with ability to plug in an instance of MultipartHttpMessageWrite. Closes gh-23884 --- .../codec/ResourceRegionEncoderTests.java | 2 - .../http/codec/CodecConfigurer.java | 16 ++++- .../http/codec/ServerCodecConfigurer.java | 14 +++++ .../multipart/MultipartHttpMessageReader.java | 8 +++ .../http/codec/protobuf/ProtobufDecoder.java | 17 +++++- .../http/codec/support/BaseDefaultCodecs.java | 61 ++++++++++++++++--- .../support/ServerDefaultCodecsImpl.java | 16 +++++ .../support/ServerCodecConfigurerTests.java | 35 ++++++++++- src/docs/asciidoc/web/webflux.adoc | 27 ++++++++ 9 files changed, 180 insertions(+), 16 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java index 8d4a919fb8a..ce83d5bebdf 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java @@ -19,7 +19,6 @@ package org.springframework.core.codec; import java.util.Collections; import java.util.function.Consumer; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; @@ -33,7 +32,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.AbstractLeakCheckingTests; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.LeakAwareDataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.support.ResourceRegion; import org.springframework.util.MimeType; diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index 17b820a9309..7ea35b9c6a6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -143,6 +143,20 @@ public interface CodecConfigurer { */ void jaxb2Encoder(Encoder encoder); + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

By default this is not set, in which case individual codec defaults + * apply. All codecs are limited to 256K by default. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @sine 5.1.11 + */ + void maxInMemorySize(int byteCount); /** * Whether to log form data at DEBUG level, and headers at TRACE level. * Both may contain sensitive information. diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java index fde144e3cbe..1479390b525 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -76,6 +76,20 @@ public interface ServerCodecConfigurer extends CodecConfigurer { */ interface ServerDefaultCodecs extends DefaultCodecs { + /** + * Configure the {@code HttpMessageReader} to use for multipart requests. + *

By default, if + * Synchronoss NIO Multipart + * is present, this is set to + * {@link org.springframework.http.codec.multipart.MultipartHttpMessageReader + * MultipartHttpMessageReader} created with an instance of + * {@link org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader + * SynchronossPartHttpMessageReader}. + * @param reader the message reader to use for multipart requests. + * @since 5.1.11 + */ + void multipartReader(HttpMessageReader reader); + /** * Configure the {@code Encoder} to use for Server-Sent Events. *

By default if this is not set, and Jackson is available, the diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java index c2392921cdc..0d47dd6fcef 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java @@ -65,6 +65,14 @@ public class MultipartHttpMessageReader extends LoggingCodecSupport } + /** + * Return the configured parts reader. + * @since 5.1.11 + */ + public HttpMessageReader getPartReader() { + return this.partReader; + } + @Override public List getReadableMediaTypes() { return Collections.singletonList(MediaType.MULTIPART_FORM_DATA); diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java index bbe43ac65d7..ec530bcbf38 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java @@ -74,7 +74,7 @@ import org.springframework.util.MimeType; public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder { /** The default max size for aggregating messages. */ - protected static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024; + protected static final int DEFAULT_MESSAGE_MAX_SIZE = 256 * 1024; private static final ConcurrentMap, Method> methodCache = new ConcurrentReferenceHashMap<>(); @@ -102,10 +102,23 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements DecoderBy default, this is set to 256K. + * @param maxMessageSize the max size per message, or -1 for unlimited + */ public void setMaxMessageSize(int maxMessageSize) { this.maxMessageSize = maxMessageSize; } + /** + * Return the {@link #setMaxMessageSize configured} message size limit. + * @since 5.1.11 + */ + public int getMaxMessageSize() { + return this.maxMessageSize; + } + @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { @@ -205,7 +218,7 @@ public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder this.maxMessageSize) { + if (this.maxMessageSize > 0 && this.messageBytesToRead > this.maxMessageSize) { throw new DataBufferLimitException( "The number of bytes to read for message " + "(" + this.messageBytesToRead + ") exceeds " + diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 564db7f262a..39d76ad0ddb 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.springframework.core.codec.AbstractDataBufferDecoder; import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.ByteBufferDecoder; @@ -29,6 +30,7 @@ import org.springframework.core.codec.DataBufferDecoder; import org.springframework.core.codec.DataBufferEncoder; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.DecoderHttpMessageReader; @@ -38,6 +40,7 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageReader; import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.json.AbstractJackson2Decoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; @@ -95,6 +98,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { @Nullable private Encoder jaxb2Encoder; + @Nullable + private Integer maxInMemorySize; + private boolean enableLoggingRequestDetails = false; private boolean registerDefaults = true; @@ -130,6 +136,16 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { this.jaxb2Encoder = encoder; } + @Override + public void maxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + @Nullable + protected Integer maxInMemorySize() { + return this.maxInMemorySize; + } + @Override public void enableLoggingRequestDetails(boolean enable) { this.enableLoggingRequestDetails = enable; @@ -155,17 +171,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { return Collections.emptyList(); } List> readers = new ArrayList<>(); - readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); - readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); - readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder())); - readers.add(new ResourceHttpMessageReader()); - readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly())); + readers.add(new DecoderHttpMessageReader<>(init(new ByteArrayDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(new ByteBufferDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(new DataBufferDecoder()))); + readers.add(new ResourceHttpMessageReader(init(new ResourceDecoder()))); + readers.add(new DecoderHttpMessageReader<>(init(StringDecoder.textPlainOnly()))); if (protobufPresent) { - Decoder decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder(); + Decoder decoder = this.protobufDecoder != null ? this.protobufDecoder : init(new ProtobufDecoder()); readers.add(new DecoderHttpMessageReader<>(decoder)); } FormHttpMessageReader formReader = new FormHttpMessageReader(); + if (this.maxInMemorySize != null) { + formReader.setMaxInMemorySize(this.maxInMemorySize); + } formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails); readers.add(formReader); @@ -174,6 +193,28 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { return readers; } + private > T init(T decoder) { + if (this.maxInMemorySize != null) { + if (decoder instanceof AbstractDataBufferDecoder) { + ((AbstractDataBufferDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + } + if (decoder instanceof ProtobufDecoder) { + ((ProtobufDecoder) decoder).setMaxMessageSize(this.maxInMemorySize); + } + if (jackson2Present) { + if (decoder instanceof AbstractJackson2Decoder) { + ((AbstractJackson2Decoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + } + } + if (jaxb2Present) { + if (decoder instanceof Jaxb2XmlDecoder) { + ((Jaxb2XmlDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize); + } + } + } + return decoder; + } + /** * Hook for client or server specific typed readers. */ @@ -189,13 +230,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { } List> readers = new ArrayList<>(); if (jackson2Present) { - readers.add(new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); + readers.add(new DecoderHttpMessageReader<>(init(getJackson2JsonDecoder()))); } if (jackson2SmilePresent) { - readers.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder())); + readers.add(new DecoderHttpMessageReader<>(init(new Jackson2SmileDecoder()))); } if (jaxb2Present) { - Decoder decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder(); + Decoder decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : init(new Jaxb2XmlDecoder()); readers.add(new DecoderHttpMessageReader<>(decoder)); } extendObjectReaders(readers); @@ -216,7 +257,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs { return Collections.emptyList(); } List> result = new ArrayList<>(); - result.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + result.add(new DecoderHttpMessageReader<>(init(StringDecoder.allMimeTypes()))); return result; } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java index ef91fb7c369..37e924cd7e9 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java @@ -39,10 +39,18 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo DefaultServerCodecConfigurer.class.getClassLoader()); + @Nullable + private HttpMessageReader multipartReader; + @Nullable private Encoder sseEncoder; + @Override + public void multipartReader(HttpMessageReader reader) { + this.multipartReader = reader; + } + @Override public void serverSentEventEncoder(Encoder encoder) { this.sseEncoder = encoder; @@ -51,10 +59,18 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo @Override protected void extendTypedReaders(List> typedReaders) { + if (this.multipartReader != null) { + typedReaders.add(this.multipartReader); + return; + } if (synchronossMultipartPresent) { boolean enable = isEnableLoggingRequestDetails(); SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader(); + Integer size = maxInMemorySize(); + if (size != null) { + partReader.setMaxInMemorySize(size); + } partReader.setEnableLoggingRequestDetails(enable); typedReaders.add(partReader); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index 777f8f51bca..5698e154dd3 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -36,6 +36,7 @@ import org.springframework.core.codec.DataBufferDecoder; import org.springframework.core.codec.DataBufferEncoder; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; @@ -124,13 +125,45 @@ public class ServerCodecConfigurerTests { .filter(e -> e == encoder).orElse(null)).isSameAs(encoder); } + @Test + public void maxInMemorySize() { + int size = 99; + this.configurer.defaultCodecs().maxInMemorySize(size); + List> readers = this.configurer.getReaders(); + assertThat(readers.size()).isEqualTo(13); + assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + + ResourceHttpMessageReader resourceReader = (ResourceHttpMessageReader) nextReader(readers); + ResourceDecoder decoder = (ResourceDecoder) resourceReader.getDecoder(); + assertThat(decoder.getMaxInMemorySize()).isEqualTo(size); + + assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size); + assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((SynchronossPartHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); + + MultipartHttpMessageReader multipartReader = (MultipartHttpMessageReader) nextReader(readers); + SynchronossPartHttpMessageReader reader = (SynchronossPartHttpMessageReader) multipartReader.getPartReader(); + assertThat((reader).getMaxInMemorySize()).isEqualTo(size); + + assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + } private Decoder getNextDecoder(List> readers) { - HttpMessageReader reader = readers.get(this.index.getAndIncrement()); + HttpMessageReader reader = nextReader(readers); assertThat(reader.getClass()).isEqualTo(DecoderHttpMessageReader.class); return ((DecoderHttpMessageReader) reader).getDecoder(); } + private HttpMessageReader nextReader(List> readers) { + return readers.get(this.index.getAndIncrement()); + } + private Encoder getNextEncoder(List> writers) { HttpMessageWriter writer = writers.get(this.index.getAndIncrement()); assertThat(writer.getClass()).isEqualTo(EncoderHttpMessageWriter.class); diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 3af5baa28f9..b1ec0d8fcc7 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -818,6 +818,33 @@ for repeated, map-like access to parts, or otherwise rely on the `SynchronossPartHttpMessageReader` for a one-time access to `Flux`. +[[webflux-codecs-limits]] +==== Limits + +`Decoder` and `HttpMessageReader` implementations that buffer some or all of the input +stream can be configured with a limit on the maximum number of bytes to buffer in memory. +In some cases buffering occurs because input is aggregated and represented as a single +object, e.g. controller method with `@RequestBody byte[]`, `x-www-form-urlencoded` data, +and so on. Buffering can also occurs with streaming, when splitting the input stream, +e.g. delimited text, a stream of JSON objects, and so on. For those streaming cases, the +limit applies to the number of bytes associted with one object in the stream. + +To configure buffer sizes, you can check if a given `Decoder` or `HttpMessageReader` +exposes a `maxInMemorySize` property and if so the Javadoc will have details about default +values. In WebFlux, the `ServerCodecConfigurer` provides a +<> from where to set all codecs, through the +`maxInMemorySize` property for default codecs. + +For <> the `maxInMemorySize` property limits +the size of non-file parts. For file parts it determines the threshold at which the part +is written to disk. For file parts written to disk, there is an additional +`maxDiskUsagePerPart` property to limit the amount of disk space per part. There is also +a `maxParts` property to limit the overall number of parts in a multipart request. +To configure all 3 in WebFlux, you'll need to supply a pre-configured instance of +`MultipartHttpMessageReader` to `ServerCodecConfigurer`. + + + [[webflux-codecs-streaming]] ==== Streaming [.small]#<>#