Skip to content

Commit 405847e

Browse files
committed
server: Allow to configure max_form_parameters across servers
- fix #3783
1 parent a36e7bd commit 405847e

File tree

14 files changed

+157
-35
lines changed

14 files changed

+157
-35
lines changed

jooby/src/main/java/io/jooby/Route.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@
2020
import edu.umd.cs.findbugs.annotations.NonNull;
2121
import edu.umd.cs.findbugs.annotations.Nullable;
2222
import io.jooby.annotation.Transactional;
23-
import io.jooby.exception.MethodNotAllowedException;
24-
import io.jooby.exception.NotAcceptableException;
25-
import io.jooby.exception.NotFoundException;
26-
import io.jooby.exception.StatusCodeException;
27-
import io.jooby.exception.UnsupportedMediaType;
23+
import io.jooby.exception.*;
2824
import io.jooby.internal.RouterImpl;
2925

3026
/**
@@ -358,6 +354,19 @@ public record Location(String filename, int line) {}
358354
}
359355
};
360356

357+
/** Handler for body error decoder responses. */
358+
public static final Handler FORM_DECODER_HANDLER =
359+
ctx -> {
360+
var tooManyFields = (Throwable) ctx.getAttributes().remove("__too_many_fields");
361+
BadRequestException cause;
362+
if (tooManyFields != null) {
363+
cause = new BadRequestException("Too many form fields", tooManyFields);
364+
} else {
365+
cause = new BadRequestException("Failed to decode HTTP body");
366+
}
367+
return ctx.sendError(cause);
368+
};
369+
361370
/** Handler for {@link StatusCode#REQUEST_ENTITY_TOO_LARGE} responses. */
362371
public static final Handler REQUEST_ENTITY_TOO_LARGE =
363372
ctx ->

jooby/src/main/java/io/jooby/ServerOptions.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ public class ServerOptions {
118118
*/
119119
private int maxRequestSize = _10MB;
120120

121+
/** Max number of form fields. Default: <code>1000</code>. */
122+
private int maxFormFields = 1000;
123+
121124
/** The maximum size in bytes of a http request header. Default is <code>8kb</code> */
122125
private int maxHeaderSize = _8KB;
123126

@@ -177,6 +180,9 @@ public static Optional<ServerOptions> from(@NonNull Config conf) {
177180
if (conf.hasPath("server.maxRequestSize")) {
178181
options.setMaxRequestSize((int) conf.getMemorySize("server.maxRequestSize").toBytes());
179182
}
183+
if (conf.hasPath("server.maxFormFields")) {
184+
options.setMaxFormFields(conf.getInt("server.maxFormFields"));
185+
}
180186
if (conf.hasPath("server.workerThreads")) {
181187
options.setWorkerThreads(conf.getInt("server.workerThreads"));
182188
}
@@ -423,11 +429,31 @@ public int getMaxRequestSize() {
423429
* @param maxRequestSize Max request size in bytes.
424430
* @return This options.
425431
*/
426-
public @NonNull ServerOptions setMaxRequestSize(int maxRequestSize) {
432+
public ServerOptions setMaxRequestSize(int maxRequestSize) {
427433
this.maxRequestSize = maxRequestSize;
428434
return this;
429435
}
430436

437+
/**
438+
* Max number of form fields. Default: <code>1000</code>.
439+
*
440+
* @return Max number of form fields. Default: <code>1000</code>.
441+
*/
442+
public int getMaxFormFields() {
443+
return maxFormFields;
444+
}
445+
446+
/**
447+
* Set max number of form fields. Default: <code>1000</code>.
448+
*
449+
* @param maxFormFields Max number of form fields.
450+
* @return Max number of form fields. Default: <code>1000</code>.
451+
*/
452+
public ServerOptions setMaxFormFields(int maxFormFields) {
453+
this.maxFormFields = maxFormFields;
454+
return this;
455+
}
456+
431457
/**
432458
* The maximum size in bytes of an http request header. Exceeding the size generates a different
433459
* response across server implementations.

jooby/src/main/java/io/jooby/internal/RouterMatch.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ public RouterMatch missing(String method, String path, MessageEncoder encoder) {
109109
}
110110
this.route = new Route(method, path, h);
111111
this.route.setEncoder(encoder);
112-
// this.route.setReturnType(Context.class);
113112
return this;
114113
}
115114
}

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ private void register(DeleteFileTask deleteFileTask) {
729729
files.add(deleteFileTask);
730730
}
731731

732-
private static void formParam(Request request, Formdata form) {
732+
private void formParam(Request request, Formdata form) {
733733
try {
734734
var params = Request.getParameters(request);
735735
for (Fields.Field param : params) {
@@ -742,7 +742,19 @@ private static void formParam(Request request, Formdata form) {
742742
}
743743
}
744744
} catch (Exception ex) {
745-
throw SneakyThrows.propagate(ex);
745+
if (ex instanceof IllegalStateException
746+
&& ex.getMessage() != null
747+
&& ex.getMessage().startsWith("form with too many")) {
748+
this.setAttribute("__too_many_fields", ex);
749+
try {
750+
Route.FORM_DECODER_HANDLER.apply(this);
751+
} catch (Exception cause) {
752+
throw SneakyThrows.propagate(cause);
753+
}
754+
throw new JettyStopPipeline();
755+
} else {
756+
throw SneakyThrows.propagate(ex);
757+
}
746758
}
747759
}
748760
}

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyHandler.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ public boolean handle(Request request, Response response, Callback callback) {
3939
if (defaultHeaders) {
4040
responseHeaders.put(JettyHeaders.SERVER);
4141
}
42-
var context =
43-
new JettyContext(
44-
getInvocationType(), request, response, callback, router, bufferSize, maxRequestSize);
45-
router.match(context).execute(context);
42+
try {
43+
var context =
44+
new JettyContext(
45+
getInvocationType(), request, response, callback, router, bufferSize, maxRequestSize);
46+
router.match(context).execute(context);
47+
} catch (JettyStopPipeline ignored) {
48+
// handled already,
49+
}
4650
return true;
4751
}
4852
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jetty;
7+
8+
public class JettyStopPipeline extends RuntimeException {}

modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,6 @@ public io.jooby.Server start(@NonNull Jooby... application) {
110110
var portInUse = options.getPort();
111111
try {
112112
this.applications = List.of(application);
113-
/* Set max request size attribute: */
114-
System.setProperty(
115-
"org.eclipse.jetty.server.Request.maxFormContentSize",
116-
Long.toString(options.getMaxRequestSize()));
117113

118114
addShutdownHook();
119115

@@ -126,7 +122,7 @@ public io.jooby.Server start(@NonNull Jooby... application) {
126122

127123
var acceptors = 1;
128124
var selectors = options.getIoThreads();
129-
this.server = new Server(threadPool);
125+
server = new Server(threadPool);
130126
server.setStopAtShutdown(false);
131127

132128
JettyHttp2Configurer http2 =
@@ -212,7 +208,8 @@ public io.jooby.Server start(@NonNull Jooby... application) {
212208
}
213209

214210
var context = new ContextHandler();
215-
211+
context.setAttribute(FormFields.MAX_FIELDS_ATTRIBUTE, options.getMaxFormFields());
212+
context.setAttribute(FormFields.MAX_LENGTH_ATTRIBUTE, options.getMaxRequestSize());
216213
boolean webSockets =
217214
application[0].getRoutes().stream().anyMatch(it -> it.getMethod().equals(Router.WS));
218215

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class NettyHandler extends ChannelInboundHandlerAdapter {
3030
private final int bufferSize;
3131
private final boolean defaultHeaders;
3232
private final long maxRequestSize;
33+
private final int maxFormFields;
3334
private long contentLength;
3435
private long chunkSize;
3536
private final boolean http2;
@@ -42,12 +43,14 @@ public NettyHandler(
4243
NettyDateService serverDate,
4344
Context.Selector contextSelector,
4445
long maxRequestSize,
46+
int maxFormFields,
4547
int bufferSize,
4648
boolean defaultHeaders,
4749
boolean http2) {
4850
this.serverDate = serverDate;
4951
this.contextSelector = contextSelector;
5052
this.maxRequestSize = maxRequestSize;
53+
this.maxFormFields = maxFormFields;
5154
this.bufferSize = bufferSize;
5255
this.defaultHeaders = defaultHeaders;
5356
this.http2 = http2;
@@ -84,7 +87,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
8487
if (contentLength > 0 || isTransferEncodingChunked(req)) {
8588
context.httpDataFactory = new DefaultHttpDataFactory(bufferSize);
8689
context.httpDataFactory.setBaseDir(app.getTmpdir().toString());
87-
context.setDecoder(newDecoder(req, context.httpDataFactory));
90+
context.setDecoder(newDecoder(req, context.httpDataFactory, maxFormFields));
8891
} else {
8992
// no body, move on
9093
router.match(context).execute(context);
@@ -95,10 +98,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
9598
try {
9699
// when decoder == null, chunk is always a LastHttpContent.EMPTY, ignore it
97100
if (context.decoder != null) {
98-
offer(context, chunk);
99-
Router.Match route = router.match(context);
100-
resetDecoderState(context, !route.matches());
101-
route.execute(context);
101+
if (offer(context, chunk)) {
102+
Router.Match route = router.match(context);
103+
resetDecoderState(context, !route.matches());
104+
route.execute(context);
105+
}
102106
}
103107
} finally {
104108
release(chunk);
@@ -232,14 +236,16 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
232236
}
233237
}
234238

235-
private void offer(NettyContext context, HttpContent chunk) {
239+
private boolean offer(NettyContext context, HttpContent chunk) {
236240
try {
237241
context.decoder.offer(chunk);
238-
} catch (HttpPostRequestDecoder.ErrorDataDecoderException
239-
| HttpPostRequestDecoder.TooLongFormFieldException
240-
| HttpPostRequestDecoder.TooManyFormFieldsException x) {
241-
resetDecoderState(context, true);
242-
context.sendError(x, StatusCode.BAD_REQUEST);
242+
return true;
243+
} catch (Exception x) {
244+
if (x instanceof HttpPostRequestDecoder.TooManyFormFieldsException) {
245+
context.setAttribute("__too_many_fields", x);
246+
}
247+
router.match(context).execute(context, Route.FORM_DECODER_HANDLER);
248+
return false;
243249
}
244250
}
245251

@@ -257,14 +263,16 @@ private void resetDecoderState(NettyContext context, boolean destroy) {
257263
}
258264

259265
private static InterfaceHttpPostRequestDecoder newDecoder(
260-
HttpRequest request, HttpDataFactory factory) {
266+
HttpRequest request, HttpDataFactory factory, int maxFormFields) {
261267
String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
262268
if (contentType != null) {
263269
String lowerContentType = contentType.toLowerCase();
264270
if (lowerContentType.startsWith(MediaType.MULTIPART_FORMDATA)) {
265-
return new HttpPostMultipartRequestDecoder(factory, request, StandardCharsets.UTF_8);
271+
return new HttpPostMultipartRequestDecoder(
272+
factory, request, StandardCharsets.UTF_8, maxFormFields, -1);
266273
} else if (lowerContentType.startsWith(MediaType.FORM_URLENCODED)) {
267-
return new HttpPostStandardRequestDecoder(factory, request, StandardCharsets.UTF_8);
274+
return new HttpPostStandardRequestDecoder(
275+
factory, request, StandardCharsets.UTF_8, maxFormFields, -1);
268276
}
269277
}
270278
return new HttpRawPostRequestDecoder(factory, request);

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class NettyPipeline extends ChannelInitializer<SocketChannel> {
2323
private final HttpDecoderConfig decoderConfig;
2424
private final Context.Selector contextSelector;
2525
private final long maxRequestSize;
26+
private final int maxFormFields;
2627
private final int bufferSize;
2728
private final boolean defaultHeaders;
2829
private final boolean http2;
@@ -34,6 +35,7 @@ public NettyPipeline(
3435
HttpDecoderConfig decoderConfig,
3536
Context.Selector contextSelector,
3637
long maxRequestSize,
38+
int maxFormFields,
3739
int bufferSize,
3840
boolean defaultHeaders,
3941
boolean http2,
@@ -43,6 +45,7 @@ public NettyPipeline(
4345
this.decoderConfig = decoderConfig;
4446
this.contextSelector = contextSelector;
4547
this.maxRequestSize = maxRequestSize;
48+
this.maxFormFields = maxFormFields;
4649
this.bufferSize = bufferSize;
4750
this.defaultHeaders = defaultHeaders;
4851
this.http2 = http2;
@@ -55,6 +58,7 @@ private NettyHandler createHandler(ScheduledExecutorService executor) {
5558
new NettyDateService(executor),
5659
contextSelector,
5760
maxRequestSize,
61+
maxFormFields,
5862
bufferSize,
5963
defaultHeaders,
6064
http2);

modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ private NettyPipeline newPipeline(ServerOptions options, SslContext sslContext,
223223
decoderConfig,
224224
Context.Selector.create(applications),
225225
options.getMaxRequestSize(),
226+
options.getMaxFormFields(),
226227
options.getOutput().getSize(),
227228
options.getDefaultHeaders(),
228229
http2,

0 commit comments

Comments
 (0)