Skip to content

Commit 5abf24e

Browse files
committed
Expose maxInMemorySize via CodecConfigurer
Centralized maxInMemorySize exposed via CodecConfigurer along with ability to plug in an instance of MultipartHttpMessageWrite. Closes gh-23884
1 parent 00ead7a commit 5abf24e

File tree

9 files changed

+180
-16
lines changed

9 files changed

+180
-16
lines changed

spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.util.Collections;
2020
import java.util.function.Consumer;
2121

22-
import org.junit.jupiter.api.AfterEach;
2322
import org.junit.jupiter.api.Test;
2423
import org.reactivestreams.Subscription;
2524
import reactor.core.publisher.BaseSubscriber;
@@ -33,7 +32,6 @@
3332
import org.springframework.core.io.buffer.AbstractLeakCheckingTests;
3433
import org.springframework.core.io.buffer.DataBuffer;
3534
import org.springframework.core.io.buffer.DataBufferUtils;
36-
import org.springframework.core.io.buffer.LeakAwareDataBufferFactory;
3735
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
3836
import org.springframework.core.io.support.ResourceRegion;
3937
import org.springframework.util.MimeType;

spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -143,6 +143,20 @@ interface DefaultCodecs {
143143
*/
144144
void jaxb2Encoder(Encoder<?> encoder);
145145

146+
/**
147+
* Configure a limit on the number of bytes that can be buffered whenever
148+
* the input stream needs to be aggregated. This can be a result of
149+
* decoding to a single {@code DataBuffer},
150+
* {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]},
151+
* {@link org.springframework.core.io.Resource Resource}, {@code String}, etc.
152+
* It can also occur when splitting the input stream, e.g. delimited text,
153+
* in which case the limit applies to data buffered between delimiters.
154+
* <p>By default this is not set, in which case individual codec defaults
155+
* apply. All codecs are limited to 256K by default.
156+
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
157+
* @sine 5.1.11
158+
*/
159+
void maxInMemorySize(int byteCount);
146160
/**
147161
* Whether to log form data at DEBUG level, and headers at TRACE level.
148162
* Both may contain sensitive information.

spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ static ServerCodecConfigurer create() {
7676
*/
7777
interface ServerDefaultCodecs extends DefaultCodecs {
7878

79+
/**
80+
* Configure the {@code HttpMessageReader} to use for multipart requests.
81+
* <p>By default, if
82+
* <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
83+
* is present, this is set to
84+
* {@link org.springframework.http.codec.multipart.MultipartHttpMessageReader
85+
* MultipartHttpMessageReader} created with an instance of
86+
* {@link org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader
87+
* SynchronossPartHttpMessageReader}.
88+
* @param reader the message reader to use for multipart requests.
89+
* @since 5.1.11
90+
*/
91+
void multipartReader(HttpMessageReader<?> reader);
92+
7993
/**
8094
* Configure the {@code Encoder} to use for Server-Sent Events.
8195
* <p>By default if this is not set, and Jackson is available, the

spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageReader.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public MultipartHttpMessageReader(HttpMessageReader<Part> partReader) {
6565
}
6666

6767

68+
/**
69+
* Return the configured parts reader.
70+
* @since 5.1.11
71+
*/
72+
public HttpMessageReader<Part> getPartReader() {
73+
return this.partReader;
74+
}
75+
6876
@Override
6977
public List<MediaType> getReadableMediaTypes() {
7078
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);

spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufDecoder.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
public class ProtobufDecoder extends ProtobufCodecSupport implements Decoder<Message> {
7575

7676
/** The default max size for aggregating messages. */
77-
protected static final int DEFAULT_MESSAGE_MAX_SIZE = 64 * 1024;
77+
protected static final int DEFAULT_MESSAGE_MAX_SIZE = 256 * 1024;
7878

7979
private static final ConcurrentMap<Class<?>, Method> methodCache = new ConcurrentReferenceHashMap<>();
8080

@@ -102,10 +102,23 @@ public ProtobufDecoder(ExtensionRegistry extensionRegistry) {
102102
}
103103

104104

105+
/**
106+
* The max size allowed per message.
107+
* <p>By default, this is set to 256K.
108+
* @param maxMessageSize the max size per message, or -1 for unlimited
109+
*/
105110
public void setMaxMessageSize(int maxMessageSize) {
106111
this.maxMessageSize = maxMessageSize;
107112
}
108113

114+
/**
115+
* Return the {@link #setMaxMessageSize configured} message size limit.
116+
* @since 5.1.11
117+
*/
118+
public int getMaxMessageSize() {
119+
return this.maxMessageSize;
120+
}
121+
109122

110123
@Override
111124
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
@@ -205,7 +218,7 @@ public Iterable<? extends Message> apply(DataBuffer input) {
205218
if (!readMessageSize(input)) {
206219
return messages;
207220
}
208-
if (this.messageBytesToRead > this.maxMessageSize) {
221+
if (this.maxMessageSize > 0 && this.messageBytesToRead > this.maxMessageSize) {
209222
throw new DataBufferLimitException(
210223
"The number of bytes to read for message " +
211224
"(" + this.messageBytesToRead + ") exceeds " +

spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222

23+
import org.springframework.core.codec.AbstractDataBufferDecoder;
2324
import org.springframework.core.codec.ByteArrayDecoder;
2425
import org.springframework.core.codec.ByteArrayEncoder;
2526
import org.springframework.core.codec.ByteBufferDecoder;
@@ -29,6 +30,7 @@
2930
import org.springframework.core.codec.DataBufferEncoder;
3031
import org.springframework.core.codec.Decoder;
3132
import org.springframework.core.codec.Encoder;
33+
import org.springframework.core.codec.ResourceDecoder;
3234
import org.springframework.core.codec.StringDecoder;
3335
import org.springframework.http.codec.CodecConfigurer;
3436
import org.springframework.http.codec.DecoderHttpMessageReader;
@@ -38,6 +40,7 @@
3840
import org.springframework.http.codec.HttpMessageWriter;
3941
import org.springframework.http.codec.ResourceHttpMessageReader;
4042
import org.springframework.http.codec.ResourceHttpMessageWriter;
43+
import org.springframework.http.codec.json.AbstractJackson2Decoder;
4144
import org.springframework.http.codec.json.Jackson2JsonDecoder;
4245
import org.springframework.http.codec.json.Jackson2JsonEncoder;
4346
import org.springframework.http.codec.json.Jackson2SmileDecoder;
@@ -95,6 +98,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
9598
@Nullable
9699
private Encoder<?> jaxb2Encoder;
97100

101+
@Nullable
102+
private Integer maxInMemorySize;
103+
98104
private boolean enableLoggingRequestDetails = false;
99105

100106
private boolean registerDefaults = true;
@@ -130,6 +136,16 @@ public void jaxb2Encoder(Encoder<?> encoder) {
130136
this.jaxb2Encoder = encoder;
131137
}
132138

139+
@Override
140+
public void maxInMemorySize(int byteCount) {
141+
this.maxInMemorySize = byteCount;
142+
}
143+
144+
@Nullable
145+
protected Integer maxInMemorySize() {
146+
return this.maxInMemorySize;
147+
}
148+
133149
@Override
134150
public void enableLoggingRequestDetails(boolean enable) {
135151
this.enableLoggingRequestDetails = enable;
@@ -155,17 +171,20 @@ final List<HttpMessageReader<?>> getTypedReaders() {
155171
return Collections.emptyList();
156172
}
157173
List<HttpMessageReader<?>> readers = new ArrayList<>();
158-
readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
159-
readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
160-
readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder()));
161-
readers.add(new ResourceHttpMessageReader());
162-
readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
174+
readers.add(new DecoderHttpMessageReader<>(init(new ByteArrayDecoder())));
175+
readers.add(new DecoderHttpMessageReader<>(init(new ByteBufferDecoder())));
176+
readers.add(new DecoderHttpMessageReader<>(init(new DataBufferDecoder())));
177+
readers.add(new ResourceHttpMessageReader(init(new ResourceDecoder())));
178+
readers.add(new DecoderHttpMessageReader<>(init(StringDecoder.textPlainOnly())));
163179
if (protobufPresent) {
164-
Decoder<?> decoder = this.protobufDecoder != null ? this.protobufDecoder : new ProtobufDecoder();
180+
Decoder<?> decoder = this.protobufDecoder != null ? this.protobufDecoder : init(new ProtobufDecoder());
165181
readers.add(new DecoderHttpMessageReader<>(decoder));
166182
}
167183

168184
FormHttpMessageReader formReader = new FormHttpMessageReader();
185+
if (this.maxInMemorySize != null) {
186+
formReader.setMaxInMemorySize(this.maxInMemorySize);
187+
}
169188
formReader.setEnableLoggingRequestDetails(this.enableLoggingRequestDetails);
170189
readers.add(formReader);
171190

@@ -174,6 +193,28 @@ final List<HttpMessageReader<?>> getTypedReaders() {
174193
return readers;
175194
}
176195

196+
private <T extends Decoder<?>> T init(T decoder) {
197+
if (this.maxInMemorySize != null) {
198+
if (decoder instanceof AbstractDataBufferDecoder) {
199+
((AbstractDataBufferDecoder<?>) decoder).setMaxInMemorySize(this.maxInMemorySize);
200+
}
201+
if (decoder instanceof ProtobufDecoder) {
202+
((ProtobufDecoder) decoder).setMaxMessageSize(this.maxInMemorySize);
203+
}
204+
if (jackson2Present) {
205+
if (decoder instanceof AbstractJackson2Decoder) {
206+
((AbstractJackson2Decoder) decoder).setMaxInMemorySize(this.maxInMemorySize);
207+
}
208+
}
209+
if (jaxb2Present) {
210+
if (decoder instanceof Jaxb2XmlDecoder) {
211+
((Jaxb2XmlDecoder) decoder).setMaxInMemorySize(this.maxInMemorySize);
212+
}
213+
}
214+
}
215+
return decoder;
216+
}
217+
177218
/**
178219
* Hook for client or server specific typed readers.
179220
*/
@@ -189,13 +230,13 @@ final List<HttpMessageReader<?>> getObjectReaders() {
189230
}
190231
List<HttpMessageReader<?>> readers = new ArrayList<>();
191232
if (jackson2Present) {
192-
readers.add(new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
233+
readers.add(new DecoderHttpMessageReader<>(init(getJackson2JsonDecoder())));
193234
}
194235
if (jackson2SmilePresent) {
195-
readers.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
236+
readers.add(new DecoderHttpMessageReader<>(init(new Jackson2SmileDecoder())));
196237
}
197238
if (jaxb2Present) {
198-
Decoder<?> decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : new Jaxb2XmlDecoder();
239+
Decoder<?> decoder = this.jaxb2Decoder != null ? this.jaxb2Decoder : init(new Jaxb2XmlDecoder());
199240
readers.add(new DecoderHttpMessageReader<>(decoder));
200241
}
201242
extendObjectReaders(readers);
@@ -216,7 +257,7 @@ final List<HttpMessageReader<?>> getCatchAllReaders() {
216257
return Collections.emptyList();
217258
}
218259
List<HttpMessageReader<?>> result = new ArrayList<>();
219-
result.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
260+
result.add(new DecoderHttpMessageReader<>(init(StringDecoder.allMimeTypes())));
220261
return result;
221262
}
222263

spring-web/src/main/java/org/springframework/http/codec/support/ServerDefaultCodecsImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,18 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
3939
DefaultServerCodecConfigurer.class.getClassLoader());
4040

4141

42+
@Nullable
43+
private HttpMessageReader<?> multipartReader;
44+
4245
@Nullable
4346
private Encoder<?> sseEncoder;
4447

4548

49+
@Override
50+
public void multipartReader(HttpMessageReader<?> reader) {
51+
this.multipartReader = reader;
52+
}
53+
4654
@Override
4755
public void serverSentEventEncoder(Encoder<?> encoder) {
4856
this.sseEncoder = encoder;
@@ -51,10 +59,18 @@ public void serverSentEventEncoder(Encoder<?> encoder) {
5159

5260
@Override
5361
protected void extendTypedReaders(List<HttpMessageReader<?>> typedReaders) {
62+
if (this.multipartReader != null) {
63+
typedReaders.add(this.multipartReader);
64+
return;
65+
}
5466
if (synchronossMultipartPresent) {
5567
boolean enable = isEnableLoggingRequestDetails();
5668

5769
SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader();
70+
Integer size = maxInMemorySize();
71+
if (size != null) {
72+
partReader.setMaxInMemorySize(size);
73+
}
5874
partReader.setEnableLoggingRequestDetails(enable);
5975
typedReaders.add(partReader);
6076

spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.core.codec.DataBufferEncoder;
3737
import org.springframework.core.codec.Decoder;
3838
import org.springframework.core.codec.Encoder;
39+
import org.springframework.core.codec.ResourceDecoder;
3940
import org.springframework.core.codec.StringDecoder;
4041
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
4142
import org.springframework.http.MediaType;
@@ -124,13 +125,45 @@ public void jackson2EncoderOverride() {
124125
.filter(e -> e == encoder).orElse(null)).isSameAs(encoder);
125126
}
126127

128+
@Test
129+
public void maxInMemorySize() {
130+
int size = 99;
131+
this.configurer.defaultCodecs().maxInMemorySize(size);
132+
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
133+
assertThat(readers.size()).isEqualTo(13);
134+
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
135+
assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
136+
assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
137+
138+
ResourceHttpMessageReader resourceReader = (ResourceHttpMessageReader) nextReader(readers);
139+
ResourceDecoder decoder = (ResourceDecoder) resourceReader.getDecoder();
140+
assertThat(decoder.getMaxInMemorySize()).isEqualTo(size);
141+
142+
assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
143+
assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size);
144+
assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
145+
assertThat(((SynchronossPartHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
146+
147+
MultipartHttpMessageReader multipartReader = (MultipartHttpMessageReader) nextReader(readers);
148+
SynchronossPartHttpMessageReader reader = (SynchronossPartHttpMessageReader) multipartReader.getPartReader();
149+
assertThat((reader).getMaxInMemorySize()).isEqualTo(size);
150+
151+
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
152+
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
153+
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
154+
assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
155+
}
127156

128157
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
129-
HttpMessageReader<?> reader = readers.get(this.index.getAndIncrement());
158+
HttpMessageReader<?> reader = nextReader(readers);
130159
assertThat(reader.getClass()).isEqualTo(DecoderHttpMessageReader.class);
131160
return ((DecoderHttpMessageReader<?>) reader).getDecoder();
132161
}
133162

163+
private HttpMessageReader<?> nextReader(List<HttpMessageReader<?>> readers) {
164+
return readers.get(this.index.getAndIncrement());
165+
}
166+
134167
private Encoder<?> getNextEncoder(List<HttpMessageWriter<?>> writers) {
135168
HttpMessageWriter<?> writer = writers.get(this.index.getAndIncrement());
136169
assertThat(writer.getClass()).isEqualTo(EncoderHttpMessageWriter.class);

src/docs/asciidoc/web/webflux.adoc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,33 @@ for repeated, map-like access to parts, or otherwise rely on the
818818
`SynchronossPartHttpMessageReader` for a one-time access to `Flux<Part>`.
819819

820820

821+
[[webflux-codecs-limits]]
822+
==== Limits
823+
824+
`Decoder` and `HttpMessageReader` implementations that buffer some or all of the input
825+
stream can be configured with a limit on the maximum number of bytes to buffer in memory.
826+
In some cases buffering occurs because input is aggregated and represented as a single
827+
object, e.g. controller method with `@RequestBody byte[]`, `x-www-form-urlencoded` data,
828+
and so on. Buffering can also occurs with streaming, when splitting the input stream,
829+
e.g. delimited text, a stream of JSON objects, and so on. For those streaming cases, the
830+
limit applies to the number of bytes associted with one object in the stream.
831+
832+
To configure buffer sizes, you can check if a given `Decoder` or `HttpMessageReader`
833+
exposes a `maxInMemorySize` property and if so the Javadoc will have details about default
834+
values. In WebFlux, the `ServerCodecConfigurer` provides a
835+
<<webflux-config-message-codecs,single place>> from where to set all codecs, through the
836+
`maxInMemorySize` property for default codecs.
837+
838+
For <<webflux-codecs-multipart,Multipart parsing>> the `maxInMemorySize` property limits
839+
the size of non-file parts. For file parts it determines the threshold at which the part
840+
is written to disk. For file parts written to disk, there is an additional
841+
`maxDiskUsagePerPart` property to limit the amount of disk space per part. There is also
842+
a `maxParts` property to limit the overall number of parts in a multipart request.
843+
To configure all 3 in WebFlux, you'll need to supply a pre-configured instance of
844+
`MultipartHttpMessageReader` to `ServerCodecConfigurer`.
845+
846+
847+
821848
[[webflux-codecs-streaming]]
822849
==== Streaming
823850
[.small]#<<web.adoc#mvc-ann-async-http-streaming, Same as in Spring MVC>>#

0 commit comments

Comments
 (0)