Skip to content

Commit 1157370

Browse files
authored
Early Rejection of Large Requests Based on Content-Length Header (#6032)
Motivation: Currently, large requests are rejected only after the transferred bytes exceed the configured limit. This behavior is sub-optimal for requests that include a valid Content-Length header indicating the request is already too large. By rejecting such requests earlier—when the header is read—we can improve resource utilization and reduce unnecessary processing. Modifications: - Added a field to ContentTooLargeException to indicate when the exception is raised during header processing. - Implemented content-length-based early rejection in Http1ObjectDecoder and Http2ObjectDecoder. Result: Result: - Closes #5880 - Requests with a Content-Length header exceeding the allowed limit can now be rejected early in the request flow, reducing wasted resources and improving efficiency. <!-- Visit this URL to learn more about how to write a pull request description: https://armeria.dev/community/developer-guide#how-to-write-pull-request-description -->
1 parent f9f949d commit 1157370

9 files changed

+161
-75
lines changed

core/src/main/java/com/linecorp/armeria/common/ContentTooLargeException.java

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -50,6 +50,7 @@ public static ContentTooLargeExceptionBuilder builder() {
5050
private final long maxContentLength;
5151
private final long contentLength;
5252
private final long transferred;
53+
private final boolean earlyRejection;
5354

5455
private ContentTooLargeException() {
5556
this(false);
@@ -62,16 +63,18 @@ private ContentTooLargeException(boolean neverSample) {
6263
maxContentLength = -1;
6364
transferred = -1;
6465
contentLength = -1;
66+
earlyRejection = false;
6567
}
6668

6769
ContentTooLargeException(long maxContentLength, long contentLength, long transferred,
68-
@Nullable Throwable cause) {
69-
super(toString(maxContentLength, contentLength, transferred), cause);
70+
boolean earlyRejection, @Nullable Throwable cause) {
71+
super(toString(maxContentLength, contentLength, transferred, earlyRejection), cause);
7072

7173
neverSample = false;
7274
this.transferred = transferred;
7375
this.contentLength = contentLength;
7476
this.maxContentLength = maxContentLength;
77+
this.earlyRejection = earlyRejection;
7578
}
7679

7780
/**
@@ -96,6 +99,13 @@ public long maxContentLength() {
9699
return maxContentLength;
97100
}
98101

102+
/**
103+
* Returns whether the exception is raised when reading content-length header.
104+
*/
105+
public boolean earlyRejection() {
106+
return earlyRejection;
107+
}
108+
99109
@Override
100110
public Throwable fillInStackTrace() {
101111
if (!neverSample && Flags.verboseExceptionSampler().isSampled(getClass())) {
@@ -105,7 +115,8 @@ public Throwable fillInStackTrace() {
105115
}
106116

107117
@Nullable
108-
private static String toString(long maxContentLength, long contentLength, long transferred) {
118+
private static String toString(long maxContentLength, long contentLength, long transferred,
119+
boolean earlyRejection) {
109120
try (TemporaryThreadLocals ttl = TemporaryThreadLocals.acquire()) {
110121
final StringBuilder buf = ttl.stringBuilder();
111122
if (maxContentLength >= 0) {
@@ -117,6 +128,9 @@ private static String toString(long maxContentLength, long contentLength, long t
117128
if (transferred >= 0) {
118129
buf.append(", transferred: ").append(transferred);
119130
}
131+
if (earlyRejection) {
132+
buf.append(", earlyRejection: ").append("true");
133+
}
120134
return buf.length() != 0 ? buf.substring(2) : null;
121135
}
122136
}

core/src/main/java/com/linecorp/armeria/common/ContentTooLargeExceptionBuilder.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -30,6 +30,8 @@ public final class ContentTooLargeExceptionBuilder {
3030
private long maxContentLength = -1;
3131
private long contentLength = -1;
3232
private long transferred = -1;
33+
private boolean earlyRejection;
34+
3335
@Nullable
3436
private Throwable cause;
3537

@@ -83,13 +85,24 @@ public ContentTooLargeExceptionBuilder cause(Throwable cause) {
8385
return this;
8486
}
8587

88+
/**
89+
* Sets the exception as early rejection.
90+
*/
91+
@UnstableApi
92+
public ContentTooLargeExceptionBuilder earlyRejection(boolean isEarlyRejection) {
93+
this.earlyRejection = isEarlyRejection;
94+
return this;
95+
}
96+
8697
/**
8798
* Returns a new instance of {@link ContentTooLargeException}.
8899
*/
89100
public ContentTooLargeException build() {
90-
if (maxContentLength < 0 && contentLength < 0 && transferred < 0 && cause == null) {
101+
if (maxContentLength < 0 && contentLength < 0 &&
102+
transferred < 0 && !earlyRejection && cause == null) {
91103
return ContentTooLargeException.get();
92104
}
93-
return new ContentTooLargeException(maxContentLength, contentLength, transferred, cause);
105+
return new ContentTooLargeException(maxContentLength, contentLength,
106+
transferred, earlyRejection, cause);
94107
}
95108
}

core/src/main/java/com/linecorp/armeria/server/DecodedHttpRequest.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -143,6 +143,16 @@ default CompletableFuture<Void> whenAggregated() {
143143
*/
144144
long requestStartTimeMicros();
145145

146+
/**
147+
* Returns the maximum allowed length of the content of the request.
148+
*/
149+
long maxRequestLength();
150+
151+
/**
152+
* Returns the transferred bytes of the request.
153+
*/
154+
long transferredBytes();
155+
146156
/**
147157
* Returns whether the request is an HTTP/1.1 webSocket request.
148158
*/

core/src/main/java/com/linecorp/armeria/server/DecodedHttpRequestWriter.java

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -19,10 +19,5 @@
1919
import com.linecorp.armeria.common.HttpRequestWriter;
2020

2121
interface DecodedHttpRequestWriter extends DecodedHttpRequest, HttpRequestWriter {
22-
23-
long maxRequestLength();
24-
25-
long transferredBytes();
26-
2722
void increaseTransferredBytes(long delta);
2823
}

core/src/main/java/com/linecorp/armeria/server/EmptyContentDecodedHttpRequest.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -260,4 +260,14 @@ public long requestStartTimeNanos() {
260260
public long requestStartTimeMicros() {
261261
return requestStartTimeMicros;
262262
}
263+
264+
@Override
265+
public long maxRequestLength() {
266+
return 0;
267+
}
268+
269+
@Override
270+
public long transferredBytes() {
271+
return 0;
272+
}
263273
}

core/src/main/java/com/linecorp/armeria/server/Http1RequestDecoder.java

+40-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -208,8 +208,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
208208
// Validate the 'content-length' header.
209209
final String contentLengthStr = headers.get(HttpHeaderNames.CONTENT_LENGTH);
210210
final boolean contentEmpty;
211+
long contentLength = 0;
211212
if (contentLengthStr != null) {
212-
long contentLength;
213213
try {
214214
contentLength = Long.parseLong(contentLengthStr);
215215
} catch (NumberFormatException ignored) {
@@ -280,6 +280,10 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
280280
final boolean endOfStream = contentEmpty && !transferEncodingChunked;
281281
this.req = req = DecodedHttpRequest.of(endOfStream, eventLoop, id, 1, headers,
282282
keepAlive, inboundTrafficController, routingCtx);
283+
final long maxRequestLength = req.maxRequestLength();
284+
if (maxRequestLength > 0 && contentLength > maxRequestLength) {
285+
abortLargeRequest(ctx, req, id, endOfStream, keepAliveHandler, true);
286+
}
283287
cfg.serverMetrics().increasePendingHttp1Requests();
284288
ctx.fireChannelRead(req);
285289
} else {
@@ -313,33 +317,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
313317
final long maxContentLength = decodedReq.maxRequestLength();
314318
final long transferredLength = decodedReq.transferredBytes();
315319
if (maxContentLength > 0 && transferredLength > maxContentLength) {
316-
final ContentTooLargeException cause =
317-
ContentTooLargeException.builder()
318-
.maxContentLength(maxContentLength)
319-
.contentLength(req.headers())
320-
.transferred(transferredLength)
321-
.build();
322-
discarding = true;
323-
req = null;
324-
final boolean shouldReset;
325-
if (encoder instanceof ServerHttp1ObjectEncoder) {
326-
if (encoder.isResponseHeadersSent(id, 1)) {
327-
ctx.channel().close();
328-
} else {
329-
keepAliveHandler.disconnectWhenFinished();
330-
}
331-
shouldReset = false;
332-
} else {
333-
// Upgraded to HTTP/2. Reset only if the remote peer is still open.
334-
shouldReset = !endOfStream;
335-
}
336-
337-
// Wrap the cause with the returned status to let LoggingService correctly log the
338-
// status.
339-
final HttpStatusException httpStatusException =
340-
HttpStatusException.of(HttpStatus.REQUEST_ENTITY_TOO_LARGE, cause);
341-
decodedReq.setShouldResetOnlyIfRemoteIsOpen(shouldReset);
342-
decodedReq.abortResponse(httpStatusException, true);
320+
abortLargeRequest(ctx, decodedReq, id, endOfStream, keepAliveHandler, false);
343321
return;
344322
}
345323

@@ -377,6 +355,39 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
377355
}
378356
}
379357

358+
private void abortLargeRequest(ChannelHandlerContext ctx, DecodedHttpRequest decodedReq, int id,
359+
boolean endOfStream, KeepAliveHandler keepAliveHandler,
360+
boolean isEarlyRejection) {
361+
final ContentTooLargeException cause =
362+
ContentTooLargeException.builder()
363+
.maxContentLength(decodedReq.maxRequestLength())
364+
.transferred(decodedReq.transferredBytes())
365+
.contentLength(decodedReq.headers())
366+
.earlyRejection(isEarlyRejection)
367+
.build();
368+
discarding = true;
369+
req = null;
370+
final boolean shouldReset;
371+
if (encoder instanceof ServerHttp1ObjectEncoder) {
372+
if (encoder.isResponseHeadersSent(id, 1)) {
373+
ctx.channel().close();
374+
} else {
375+
keepAliveHandler.disconnectWhenFinished();
376+
}
377+
shouldReset = false;
378+
} else {
379+
// Upgraded to HTTP/2. Reset only if the remote peer is still open.
380+
shouldReset = !endOfStream;
381+
}
382+
383+
// Wrap the cause with the returned status to let LoggingService correctly log the
384+
// status.
385+
final HttpStatusException httpStatusException =
386+
HttpStatusException.of(HttpStatus.REQUEST_ENTITY_TOO_LARGE, cause);
387+
decodedReq.setShouldResetOnlyIfRemoteIsOpen(shouldReset);
388+
decodedReq.abortResponse(httpStatusException, true);
389+
}
390+
380391
private void removeFromPipelineIfUpgraded(ChannelHandlerContext ctx, boolean endOfStream) {
381392
if (endOfStream && encoder instanceof ServerHttp2ObjectEncoder) {
382393
// An HTTP/1 connection has been upgraded to HTTP/2.

core/src/main/java/com/linecorp/armeria/server/Http2RequestDecoder.java

+26-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 LINE Corporation
2+
* Copyright 2024 LINE Corporation
33
*
44
* LINE Corporation licenses this file to you under the Apache License,
55
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -165,8 +165,8 @@ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers
165165

166166
// Validate the 'content-length' header if exists.
167167
final String contentLengthStr = headers.get(HttpHeaderNames.CONTENT_LENGTH);
168+
long contentLength = 0;
168169
if (contentLengthStr != null) {
169-
long contentLength;
170170
try {
171171
contentLength = Long.parseLong(contentLengthStr);
172172
} catch (NumberFormatException ignored) {
@@ -206,6 +206,10 @@ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers
206206
final EventLoop eventLoop = ctx.channel().eventLoop();
207207
req = DecodedHttpRequest.of(endOfStream, eventLoop, id, streamId, headers, true,
208208
inboundTrafficController, routingCtx);
209+
final long maxRequestLength = req.maxRequestLength();
210+
if (maxRequestLength > 0 && contentLength > maxRequestLength) {
211+
abortLargeRequest(req, endOfStream, true);
212+
}
209213
requests.put(streamId, req);
210214
cfg.serverMetrics().increasePendingHttp2Requests();
211215
ctx.fireChannelRead(req);
@@ -321,20 +325,7 @@ public int onDataRead(
321325
final long maxContentLength = decodedReq.maxRequestLength();
322326
final long transferredLength = decodedReq.transferredBytes();
323327
if (maxContentLength > 0 && transferredLength > maxContentLength) {
324-
assert encoder != null;
325-
final ContentTooLargeException cause =
326-
ContentTooLargeException.builder()
327-
.maxContentLength(maxContentLength)
328-
.contentLength(decodedReq.headers())
329-
.transferred(transferredLength)
330-
.build();
331-
332-
final boolean shouldReset = !endOfStream;
333-
334-
final HttpStatusException httpStatusException =
335-
HttpStatusException.of(HttpStatus.REQUEST_ENTITY_TOO_LARGE, cause);
336-
decodedReq.setShouldResetOnlyIfRemoteIsOpen(shouldReset);
337-
decodedReq.abortResponse(httpStatusException, true);
328+
abortLargeRequest(decodedReq, endOfStream, false);
338329
} else if (decodedReq.isOpen()) {
339330
try {
340331
// The decodedReq will be automatically closed if endOfStream is true.
@@ -350,6 +341,25 @@ public int onDataRead(
350341
return dataLength + padding;
351342
}
352343

344+
private void abortLargeRequest(DecodedHttpRequest decodedReq, boolean endOfStream,
345+
boolean isEarlyRejection) {
346+
assert encoder != null;
347+
final ContentTooLargeException cause =
348+
ContentTooLargeException.builder()
349+
.maxContentLength(decodedReq.maxRequestLength())
350+
.contentLength(decodedReq.headers())
351+
.transferred(decodedReq.transferredBytes())
352+
.earlyRejection(isEarlyRejection)
353+
.build();
354+
355+
final boolean shouldReset = !endOfStream;
356+
357+
final HttpStatusException httpStatusException =
358+
HttpStatusException.of(HttpStatus.REQUEST_ENTITY_TOO_LARGE, cause);
359+
decodedReq.setShouldResetOnlyIfRemoteIsOpen(shouldReset);
360+
decodedReq.abortResponse(httpStatusException, true);
361+
}
362+
353363
private void writeInvalidRequestPathResponse(int streamId, @Nullable RequestHeaders headers) {
354364
writeErrorResponse(streamId, headers, HttpStatus.BAD_REQUEST,
355365
"Invalid request path", null);

0 commit comments

Comments
 (0)