Respond with 408 status code if not all request body bytes were received - #5466
Respond with 408 status code if not all request body bytes were received#5466rwalerow wants to merge 38 commits into
Conversation
|
Is there a related issue? |
| override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = { | ||
| msg match { | ||
| case _: LastHttpContent => ctx.channel().attr(RequestBodyCompletedTracker.BodyComplete).set(true) | ||
| case _: HttpRequest => ctx.channel().attr(RequestBodyCompletedTracker.BodyComplete).set(false) |
There was a problem hiding this comment.
are we using the explicit-false case anywhere? Maybe we should just use a () as a marker? So that we have two states: RequestBodyCompletedTracker.BodyComplete is either set, or not. Now we have three (true, false, unset), with overlapping semantics
There was a problem hiding this comment.
I understand the incentive to use only double, not the triple values.
I would stay with a given boolean version though. If we would change this to Unit type and try to make it work we would be forced to use deprecated Attribute.remove() api
I've tested this with the java doc suggestion of set(null) instead but the fix stopped working at this point.
I personally prefer to pay a fee of additional non set state than to use deprecated api
There was a problem hiding this comment.
But is it at all possible to first observe LastHttpContent and then a HttpRequest? Or would it be a bug?
There was a problem hiding this comment.
It would be a bug since even headers generate HttpRequest
Even option should generate those before LastHttpContent
adamw
left a comment
There was a problem hiding this comment.
Automated Claude code review. 10 inline comments: 3 confirmed correctness issues, 1 likely ordering problem, plus smaller cleanups.
| override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = { | ||
| msg match { | ||
| case _: LastHttpContent => ctx.channel().attr(RequestBodyCompletedTracker.BodyComplete).set(true) | ||
| case _: HttpRequest => ctx.channel().attr(RequestBodyCompletedTracker.BodyComplete).set(false) |
There was a problem hiding this comment.
🤖 automated Claude review
The flag is per-channel, but the timeout is per-request. A keep-alive client can fully send request 1 (flag = true), pipeline request 2's headers — this line resets the flag to false, since the tracker sits before HttpStreamsServerHandler and sees them immediately — and then stall. Request 1's reader-idle then reads false and aborts a valid, fully-received request with a 400. With a chunked/streaming response for request 1 this is a new failure: before this PR the exchange completed.
There was a problem hiding this comment.
I'm not sure multiple requests are allowed in http apart from websockets.
2 requests and than response starts?
| def runRoute(req: HttpRequest, releaseReq: () => Any = () => ()): Unit = { | ||
| val requestTimeoutHandler = config.requestTimeout.map { requestTimeout => | ||
| new IdleStateHandler(0, requestTimeout.toMillis.toInt, 0, TimeUnit.MILLISECONDS) | ||
| new IdleStateHandler(requestTimeout.toMillis.toInt, requestTimeout.toMillis.toInt, 0, TimeUnit.MILLISECONDS) |
There was a problem hiding this comment.
🤖 automated Claude review
toMillis.toInt overflows for timeouts above ~24.8 days; a negative value disables IdleStateHandler entirely, silently dropping both the 503 and the new 400. The constructor accepts long, so .toInt can just be dropped. (Pre-existing in the writer slot; this change copies it into the reader slot.)
There was a problem hiding this comment.
I don't think that's relevant for this PR, it also applies for other timeouts
Yes, here #4169 I've adjusted description accordingly |
1eb9e0e to
b89805a
Compare
|
🤖 automated Claude review — re-review after the latest changes. Replies marked as addressed, where the code says otherwise:
Open discussions:
New findings:
|
cdd9a83 to
546d06e
Compare
Adjusted, I've fixed faulty setting in my intellij
I've rephrased the log message
Reintroduced
I've introduced socket closing as a resource code
Those were left open on purpose
This will spawn a new PR
Changed return status
I've removed
I've introduced socket closing as a resource code
I've introduced this approach |
|
🤖 automated Claude review — re-review of Everything else from the previous round looks addressed: scalafmt is clean now (checked with scalafmt 3.11.5 on all 4 files), the unified Main finding: the 408 only happens when the whole partial request arrives in one readI built the branch and probed it with
Suggested fix, which is also simpler than what's in the PR: drop the reader-idle timer (go back to if (e.state() == IdleState.WRITER_IDLE) {
if (wasRequestBodyFullyReceived(ctx)) {
logger.error(...)
writeErrorThenClose(ctx, HttpResponseStatus.SERVICE_UNAVAILABLE)
} else {
logger.debug(...)
writeErrorThenClose(ctx, HttpResponseStatus.REQUEST_TIMEOUT)
}
}I applied exactly this to the branch and re-ran the probe: both stalled-upload shapes give 408, and a complete request with a 2s handler still gives 503. As a bonus, with a single timer the "408 followed by a 503 on the same connection" problem goes away on its own, without having to remove the handler. The test should then write the head and the body fragment separately — that is the case that is currently broken. Other points
|
|
🤖 automated Claude review — final review of The design is now right: one writer-idle timer, with the status picked by the body-completion flag. I checked out the branch and ran it:
Everything actionable from the previous rounds is addressed: Nothing blocking. Four things to decide on before merge. 1. Test duplicationThe two new tests are ~45 near-identical lines each, differing only in whether the body fragment goes out in the same Worth knowing while doing that: the two cases may not actually differ. The decoder emits 2. The flag is per connection, the timeout is per requestKnown and accepted, but worth one sentence in the tracker scaladoc so the next reader doesn't assume otherwise: on a keep-alive connection the headers of request N+1 can be decoded while request N is still being handled, which flips the flag to 3. Writer-idle can fire more than once
4. DocsCorrect, but long for what they say. Concretely:
The same explanation currently appears in five places ( Smaller things
Unrelated, pre-existingRequests with |
…tion close header
… Resource to close a socket
…dTracker be placed
… double notification
- document RequestBodyCompletionTracker's absent-tracker fallback and its initial state; move the companion below the class it describes - make both request-timeout log messages name the same timeout and state the status they send - qualify the tracker reference in NettyConfig's scaladoc, and explain the requestTimeout guard and the Web Socket removal - assert on the exact sequence of response status lines, pin the test charset, and widen the timing margins for slow CI - extract the socket-level test data & helpers into TimingOutRequestSpecData Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e691257 to
0a8e8dc
Compare
Why I did it?
In order to have a test which might confirm an issue
with an incompletely send request
Issue reported here: #4169
How I did it:
I prepared a new test case in NettyCatsRequestTimeoutTest as following:
set request timeout to 1s
declare 10000 bytes as request body size(content length)
send request with a tiny request body
Note: This suppose to partial fix for case where server should return 408 on partially sent body without client closing the connection