Bug: HTTP proxy incorrectly keeps close-delimited HTTP/1.0 responses alive
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 131
- Forks
- 109
- PR merge metrics
- No merged PRs in 30d
Description
Bug: HTTP proxy incorrectly keeps close-delimited HTTP/1.0 responses alive
Summary
The HTTP proxy implementation in github.com/sagernet/sing/protocol/http/handshake.go appears to convert upstream close-delimited responses into downstream keep-alive responses without ensuring the response body is self-delimited.
This breaks old HTTP/1.0 web servers that send a response without Content-Length and rely on Connection: close to mark the end of the body.
In NekoBox for Android this is triggered by enabling Append HTTP Proxy to VPN, because supported browsers then use the mixed HTTP proxy directly.
Affected Scenario
- NekoBox for Android enables Android VPN HTTP proxy via
VpnService.Builder.setHttpProxy(...). - Browser sends requests through the local mixed proxy, e.g.
127.0.0.1:9080. - Target server returns:
HTTP/1.0 200 OK
Content-Type: text/html
Connection: close
<!doctype html><html><body>loaded</body></html>
This response is valid: without Content-Length or chunked encoding, the body is delimited by closing the connection.
Actual Behavior
The proxy rewrites the downstream response to keep-alive:
HTTP/1.0 200 OK
Connection: keep-alive
Proxy-Connection: keep-alive
Keep-Alive: timeout=4
Content-Type: text/html
The response body is received, but the browser cannot determine the end of the response because there is no Content-Length and the downstream connection is kept alive.
Observed in Chromium DevTools/CDP:
- Main document remains
readyState=loading. performance.getEntriesByType("navigation")[0].responseEndstays0.- The request is served from the local proxy address, e.g.
127.0.0.1:9080.
If the same upstream response includes Content-Length, the page loads normally.
Expected Behavior
The proxy should not keep the downstream connection alive unless the response can be safely delimited on a persistent connection.
For close-delimited responses without Content-Length or chunked transfer encoding, the proxy should either:
- close the downstream connection after writing the response, or
- buffer/rewrite the response with a valid
Content-Length.
Minimal Reproduction
Run a simple TCP server that returns:
HTTP/1.0 200 OK
Server: test
Content-Type: text/html
Connection: close
<!doctype html><html><body>loaded</body></html>
Then access it through the sing mixed HTTP proxy:
curl -v --proxy http://127.0.0.1:9080 http://HOST:PORT/
The body is received, but the response does not complete correctly because the proxy changes the downstream response to keep-alive without adding a body length.
Likely Cause
In github.com/sagernet/sing/protocol/http/handshake.go, handleHTTPConnection decides downstream keep-alive only from the client request:
keepAlive := !(request.ProtoMajor == 1 && request.ProtoMinor == 0) &&
strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive"
Then it unconditionally rewrites the response when keepAlive is true:
response.Header.Set("Proxy-Connection", "keep-alive")
response.Header.Set("Connection", "keep-alive")
response.Header.Set("Keep-Alive", "timeout=4")
response.Close = !keepAlive
This ignores whether the upstream response has a safe message length for a persistent downstream connection.
Suggested Fix
Only allow downstream keep-alive when the response is self-delimited, such as:
Content-Length >= 0- chunked transfer encoding is used
- the response has no body by definition, such as
HEAD,204,304, or1xx
Otherwise, force Connection: close for the downstream response.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in protocol/http/handshake.go at handleHTTPConnection and reproduce the issue with the provided TCP server and curl command. Trace how the upstream response body is delimited before downstream keep-alive headers are set. Done means close-delimited responses use Connection: close, while self-delimited or bodyless responses can remain persistent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100