Keep connection alive for body-less responses (204) to avoid ECONNRESET on keep-alive clients#10
Open
icebob wants to merge 1 commit into
Open
Conversation
…g it NettyWebResponse.end() closed the channel whenever no Content-Length header was present. This is the case for body-less responses such as "204 No Content" (or an action returning null). The close was performed without a "Connection: close" header, so HTTP clients that use keep-alive connection pools (browsers, Node's http.Agent / axios, Java HttpClient, ...) returned the socket to the pool and hit "ECONNRESET" / "socket hang up" on the next request that reused it. For a body-less response the message length is well-defined (zero), so emit an explicit "Content-Length: 0" before the headers are flushed. The existing logic then sees a Content-Length, keeps the connection alive, and the response is correctly framed. Responses that write a body are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
NettyWebResponse.end()closes the TCP connection whenever the response has noContent-Lengthheader — which is the case for body-less responses such as204 No Content(or any action returningnull):The close is done without sending a
Connection: closeheader (sendHeaders()never emits one). So from the client's point of view it is an ordinary HTTP/1.1 keep-alive response. Clients that use a keep-alive connection pool (browsers, Node'shttp.Agent/axios, JavaHttpClient, …) return the socket to the pool and reuse it for the next request, which then fails withECONNRESET/ "socket hang up".curlis unaffected because it transparently detects the closed socket and opens a new connection; pooled clients do not. Because the close is an asyncChannelFutureListener.CLOSE, whether the client notices the FIN before reusing the socket is a race — so this can appear JVM/timing/load dependent even with an unchanged library version.Reproduction
Any endpoint returning
204(ornullfrom the action), called by a keep-alive client, followed by another request on the same pooled connection:With
keepAlive: false(orcurl) both calls succeed.Fix
Emit an explicit
Content-Length: 0for body-less responses beforesendHeaders()runs. The existing logic then sees aContent-Length, keeps the connection alive, and the message stays correctly framed.send()only flipsfirstwhen it writesbytes.length > 0, sofirst.get() == trueat the top ofend()reliably means "no body was written"; multipart and bodied responses are left untouched../gradlew compileJavapasses. The same logic, applied as an application-levelHttpMiddlewarethat wraps theWebResponse, has been verified to resolve theECONNRESETfailures against a running server (204 → reused socket now returns 200 instead of resetting).Note (optional follow-up)
For genuinely close-delimited responses (a body written with no
Content-Length), the server still closes the socket without advertising it; adding aConnection: closeheader in that branch would make those fully HTTP/1.1-correct too. This PR only addresses the common body-less (204/empty) case.🤖 Generated with Claude Code