Skip to content

Commit 8c4099a

Browse files
authored
feat: add configurable max request body size (#24866) (CP: 24.10) (#24872)
Add maxRequestBodySize property to limit UIDL/RPC and push request body size (-1 disables); the default is 10 MB; does not affect uploads.
1 parent 0c3b8ab commit 8c4099a

14 files changed

Lines changed: 369 additions & 20 deletions

flow-server/src/main/java/com/vaadin/flow/function/DeploymentConfiguration.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.vaadin.flow.server.AbstractConfiguration;
1919
import com.vaadin.flow.server.Constants;
20+
import com.vaadin.flow.server.DefaultDeploymentConfiguration;
2021
import com.vaadin.flow.server.InitParameters;
2122
import com.vaadin.flow.server.SessionLockCheckStrategy;
2223
import com.vaadin.flow.server.WrappedSession;
@@ -60,6 +61,24 @@ public interface DeploymentConfiguration
6061
*/
6162
int getHeartbeatInterval();
6263

64+
/**
65+
* Returns the maximum size, in characters, that Flow reads from a
66+
* client-to-server UIDL/RPC or push request body before rejecting the
67+
* request with HTTP 413 (Request Entity Too Large).
68+
* <p>
69+
* The limit does not apply to file uploads, which are streamed in chunks
70+
* and have their own separate size limits.
71+
*
72+
* @return the maximum request body size in characters, or a negative number
73+
* if the limit is disabled
74+
*/
75+
default long getMaxRequestBodySize() {
76+
return getApplicationOrSystemProperty(
77+
InitParameters.SERVLET_PARAMETER_MAX_REQUEST_BODY_SIZE,
78+
DefaultDeploymentConfiguration.DEFAULT_MAX_REQUEST_BODY_SIZE,
79+
Long::parseLong);
80+
}
81+
6382
/**
6483
* In certain cases, such as when combining XmlHttpRequests and push over
6584
* low bandwidth connections, messages may be received out of order by the

flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ public class DefaultDeploymentConfiguration
7373
*/
7474
public static final int DEFAULT_MAX_MESSAGE_SUSPEND_TIMEOUT = 5000;
7575

76+
/**
77+
* Default value for {@link #getMaxRequestBodySize()} = {@value} characters
78+
* (10&nbsp;MB).
79+
*/
80+
public static final long DEFAULT_MAX_REQUEST_BODY_SIZE = 10L * 1024 * 1024;
81+
7682
/**
7783
* Default value for {@link #getWebComponentDisconnect()} = {@value}.
7884
*
@@ -95,6 +101,7 @@ public class DefaultDeploymentConfiguration
95101
private boolean productionMode;
96102
private boolean xsrfProtectionEnabled;
97103
private int heartbeatInterval;
104+
private long maxRequestBodySize;
98105
private int maxMessageSuspendTimeout;
99106
private int webComponentDisconnect;
100107
private boolean closeIdleSessions;
@@ -134,6 +141,7 @@ public DefaultDeploymentConfiguration(ApplicationConfiguration parentConfig,
134141
checkRequestTiming();
135142
checkXsrfProtection(log);
136143
checkHeartbeatInterval();
144+
checkMaxRequestBodySize();
137145
checkMaxMessageSuspendTimeout();
138146
checkWebComponentDisconnectTimeout();
139147
checkCloseIdleSessions();
@@ -205,6 +213,16 @@ public int getHeartbeatInterval() {
205213
return heartbeatInterval;
206214
}
207215

216+
/**
217+
* {@inheritDoc}
218+
* <p>
219+
* The default is 10&nbsp;MB.
220+
*/
221+
@Override
222+
public long getMaxRequestBodySize() {
223+
return maxRequestBodySize;
224+
}
225+
208226
/**
209227
* {@inheritDoc}
210228
* <p>
@@ -359,6 +377,20 @@ private void checkHeartbeatInterval() {
359377
}
360378
}
361379

380+
private void checkMaxRequestBodySize() {
381+
try {
382+
maxRequestBodySize = getApplicationOrSystemProperty(
383+
InitParameters.SERVLET_PARAMETER_MAX_REQUEST_BODY_SIZE,
384+
DEFAULT_MAX_REQUEST_BODY_SIZE, Long::parseLong);
385+
} catch (NumberFormatException e) {
386+
String warning = "WARNING: maxRequestBodySize has been set to an illegal value."
387+
+ " The default of " + DEFAULT_MAX_REQUEST_BODY_SIZE
388+
+ " characters will be used.";
389+
warnings.add(warning);
390+
maxRequestBodySize = DEFAULT_MAX_REQUEST_BODY_SIZE;
391+
}
392+
}
393+
362394
private void checkMaxMessageSuspendTimeout() {
363395
try {
364396
maxMessageSuspendTimeout = getApplicationOrSystemProperty(

flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,38 @@ public class InitParameters implements Serializable {
5454
public static final String SERVLET_PARAMETER_SEND_URLS_AS_PARAMETERS = "sendUrlsAsParameters";
5555
public static final String SERVLET_PARAMETER_PUSH_SUSPEND_TIMEOUT_LONGPOLLING = "pushLongPollingSuspendTimeout";
5656
public static final String SERVLET_PARAMETER_MAX_MESSAGE_SUSPEND_TIMEOUT = "maxMessageSuspendTimeout";
57+
58+
/**
59+
* Configuration parameter name for the maximum size, in characters, that
60+
* Flow reads from a client-to-server UIDL/RPC or push request body before
61+
* rejecting the request with HTTP 413 (Request Entity Too Large). The
62+
* default is 10&nbsp;MB; set the value to {@code -1} to disable the limit.
63+
* <p>
64+
* This limit does not apply to file uploads, which are streamed in chunks
65+
* and have their own separate size limits (request size, file size and file
66+
* count).
67+
* <p>
68+
* The body is read incrementally and the limit is enforced on the running
69+
* total, so the request is rejected mid-stream once the limit is exceeded;
70+
* even chunked ({@code Transfer-Encoding: chunked}) requests without a
71+
* {@code Content-Length} header cannot bypass it. For this to hold, Flow
72+
* must be the first component to read the body: if a filter or request
73+
* wrapper in front of Flow buffers the whole body first, that memory cost
74+
* is incurred before this check runs. As defense-in-depth, also configure a
75+
* request body size limit and a request read timeout at the servlet
76+
* container or reverse proxy (for example Tomcat {@code connectionTimeout}
77+
* and {@code maxSwallowSize}, or nginx {@code client_max_body_size} and
78+
* {@code client_body_timeout}).
79+
* <p>
80+
* Be careful not to set the limit too low: every Flow interaction adds a
81+
* small protocol overhead (roughly 100&nbsp;bytes per request on top of the
82+
* actual payload), and request bodies also grow with the size of component
83+
* state changes and RPC arguments sent from the browser. A limit that is
84+
* too low will reject legitimate interactions and make the application
85+
* unusable. Choose a value that comfortably accommodates the largest
86+
* expected UIDL/RPC payload produced by the application.
87+
*/
88+
public static final String SERVLET_PARAMETER_MAX_REQUEST_BODY_SIZE = "maxRequestBodySize";
5789
public static final String SERVLET_PARAMETER_JSBUNDLE = "module.bundle";
5890
public static final String SERVLET_PARAMETER_POLYFILLS = "module.polyfills";
5991
public static final String NODE_VERSION = "node.version";

flow-server/src/main/java/com/vaadin/flow/server/PropertyDeploymentConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ public int getHeartbeatInterval() {
237237
return DefaultDeploymentConfiguration.DEFAULT_HEARTBEAT_INTERVAL;
238238
}
239239

240+
@Override
241+
public long getMaxRequestBodySize() {
242+
return getApplicationOrSystemProperty(
243+
InitParameters.SERVLET_PARAMETER_MAX_REQUEST_BODY_SIZE,
244+
DefaultDeploymentConfiguration.DEFAULT_MAX_REQUEST_BODY_SIZE,
245+
Long::parseLong);
246+
}
247+
240248
@Override
241249
public int getMaxMessageSuspendTimeout() {
242250
return DefaultDeploymentConfiguration.DEFAULT_MAX_MESSAGE_SUSPEND_TIMEOUT;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server;
17+
18+
import java.io.IOException;
19+
20+
/**
21+
* Thrown when a client-to-server UIDL/RPC or push request body exceeds the
22+
* configured maximum size while it is being read.
23+
* <p>
24+
* Extends {@link IOException} so that it propagates through the existing
25+
* request-handling signatures. Request handlers translate this into an HTTP 413
26+
* (Request Entity Too Large) response, or, for push messages, into a refresh
27+
* and disconnect.
28+
*
29+
* @see com.vaadin.flow.function.DeploymentConfiguration#getMaxRequestBodySize()
30+
* @see SynchronizedRequestHandler#getRequestBody(java.io.Reader, long)
31+
*/
32+
public class RequestBodyTooLargeException extends IOException {
33+
34+
private final long maxBodySize;
35+
36+
/**
37+
* Creates a new exception for a request body that exceeded the given
38+
* maximum size.
39+
*
40+
* @param maxBodySize
41+
* the configured maximum request body size, in characters
42+
*/
43+
public RequestBodyTooLargeException(long maxBodySize) {
44+
super("Request body exceeds the maximum allowed size of " + maxBodySize
45+
+ " characters. The limit can be changed with the '"
46+
+ InitParameters.SERVLET_PARAMETER_MAX_REQUEST_BODY_SIZE
47+
+ "' configuration property (-1 disables it).");
48+
this.maxBodySize = maxBodySize;
49+
}
50+
51+
/**
52+
* Gets the configured maximum request body size, in characters, that was
53+
* exceeded.
54+
*
55+
* @return the maximum request body size in characters
56+
*/
57+
public long getMaxBodySize() {
58+
return maxBodySize;
59+
}
60+
}

flow-server/src/main/java/com/vaadin/flow/server/SynchronizedRequestHandler.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.io.Serializable;
1515
import java.util.Optional;
1616

17+
import org.slf4j.LoggerFactory;
18+
1719
/**
1820
* RequestHandler which takes care of locking and unlocking of the VaadinSession
1921
* automatically. The session is locked before
@@ -52,8 +54,14 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
5254
try {
5355
if (isReadAndWriteOutsideSessionLock()) {
5456
BufferedReader reader = request.getReader();
55-
String requestBody = reader == null ? null
56-
: getRequestBody(reader);
57+
String requestBody;
58+
try {
59+
requestBody = reader == null ? null
60+
: getRequestBody(reader,
61+
getMaxRequestBodySize(request));
62+
} catch (RequestBodyTooLargeException e) {
63+
return rejectOversizedRequest(response, e);
64+
}
5765
session.lock();
5866
Optional<ResponseWriter> responseWriter = synchronizedHandleRequest(
5967
session, request, response, requestBody);
@@ -162,18 +170,85 @@ protected boolean canHandleRequest(VaadinRequest request) {
162170
return true;
163171
}
164172

173+
/**
174+
* Reads the entire request body from the given reader without any size
175+
* limit.
176+
* <p>
177+
* Prefer {@link #getRequestBody(Reader, long)} on request-handling code
178+
* paths so that the configured maximum request body size is enforced.
179+
*
180+
* @param reader
181+
* the reader to read the request body from
182+
* @return the request body as a string
183+
* @throws IOException
184+
* if reading fails
185+
*/
165186
public static String getRequestBody(Reader reader) throws IOException {
187+
return getRequestBody(reader, -1L);
188+
}
189+
190+
/**
191+
* Reads the request body from the given reader, rejecting it once it
192+
* exceeds {@code maxBodySize} characters.
193+
* <p>
194+
* The limit is enforced on the number of characters read from the reader.
195+
* For typical ASCII/UTF-8 JSON payloads this equals the number of bytes;
196+
* multi-byte content makes the effective byte limit larger.
197+
*
198+
* @param reader
199+
* the reader to read the request body from
200+
* @param maxBodySize
201+
* the maximum number of characters to read, or a negative number
202+
* to read without any limit
203+
* @return the request body as a string
204+
* @throws IOException
205+
* if reading fails
206+
* @throws RequestBodyTooLargeException
207+
* if the body exceeds {@code maxBodySize} characters
208+
*/
209+
public static String getRequestBody(Reader reader, long maxBodySize)
210+
throws IOException {
166211
StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
167212
char[] buffer = new char[MAX_BUFFER_SIZE];
213+
long total = 0;
168214

169215
while (true) {
170216
int read = reader.read(buffer);
171217
if (read == -1) {
172218
break;
173219
}
220+
total += read;
221+
if (maxBodySize >= 0 && total > maxBodySize) {
222+
throw new RequestBodyTooLargeException(maxBodySize);
223+
}
174224
sb.append(buffer, 0, read);
175225
}
176226

177227
return sb.toString();
178228
}
229+
230+
/**
231+
* Gets the configured maximum request body size for the given request, in
232+
* characters.
233+
*
234+
* @param request
235+
* the request being handled
236+
* @return the maximum request body size in characters, or a negative number
237+
* if the limit is disabled
238+
*/
239+
public static long getMaxRequestBodySize(VaadinRequest request) {
240+
return request.getService().getDeploymentConfiguration()
241+
.getMaxRequestBodySize();
242+
}
243+
244+
private static boolean rejectOversizedRequest(VaadinResponse response,
245+
RequestBodyTooLargeException e) throws IOException {
246+
LoggerFactory.getLogger(SynchronizedRequestHandler.class)
247+
.warn("Rejected a request with a body larger than the "
248+
+ "configured maximum of {} characters",
249+
e.getMaxBodySize());
250+
response.sendError(HttpStatusCode.REQUEST_ENTITY_TOO_LARGE.getCode(),
251+
e.getMessage());
252+
return true;
253+
}
179254
}

flow-server/src/main/java/com/vaadin/flow/server/communication/PushHandler.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.vaadin.flow.internal.CurrentInstance;
3434
import com.vaadin.flow.server.ErrorEvent;
3535
import com.vaadin.flow.server.HandlerHelper;
36+
import com.vaadin.flow.server.RequestBodyTooLargeException;
3637
import com.vaadin.flow.server.SessionExpiredException;
3738
import com.vaadin.flow.server.SynchronizedRequestHandler;
3839
import com.vaadin.flow.server.SystemMessages;
@@ -157,9 +158,17 @@ interface PushEventCallback {
157158

158159
try {
159160
new ServerRpcHandler().handleRpc(ui,
160-
SynchronizedRequestHandler.getRequestBody(reader),
161+
SynchronizedRequestHandler.getRequestBody(reader,
162+
SynchronizedRequestHandler
163+
.getMaxRequestBodySize(vaadinRequest)),
161164
vaadinRequest);
162165
connection.push(false);
166+
} catch (RequestBodyTooLargeException e) {
167+
getLogger().warn(
168+
"Rejected a push message with a body larger than the "
169+
+ "configured maximum of {} characters",
170+
e.getMaxBodySize());
171+
sendRefreshAndDisconnect(resource);
163172
} catch (JsonException e) {
164173
getLogger().error("Error writing JSON to response", e);
165174
// Refresh on client side

0 commit comments

Comments
 (0)