Skip to content

send_error: skip the default error page for 204 No Content (issue #3360)#3663

Open
HrachShah wants to merge 2 commits into
tornadoweb:masterfrom
HrachShah:fix/send-error-skip-body-for-no-content-status
Open

send_error: skip the default error page for 204 No Content (issue #3360)#3663
HrachShah wants to merge 2 commits into
tornadoweb:masterfrom
HrachShah:fix/send-error-skip-body-for-no-content-status

Conversation

@HrachShah

Copy link
Copy Markdown

What

Closes #3360: send_error unconditionally called write_error(), which in the default RequestHandler writes a small HTML body like
"<html><title>204: No Content</title>...". That body then propagated into the _status_code in (204, 304) or (100 <= _status_code < 200) assertion in finish(), raising AssertionError: Cannot send body with 204.

This was doubly bad because under python -O (which strips assertions) the client received a 204 response with a body and no Content-Length header, in violation of RFC 9110.

The fix

Skip write_error for 204 (keeping the existing 304 skip) so the default send_error path for 204 produces a header-only response.

Zo Bot added 2 commits July 1, 2026 09:38
… (issue tornadoweb#3614)

When TCPClient.connect is called with both ssl_options and a timeout, a
TLS handshake timeout leaks the underlying socket. IOStream.start_tls
transfers socket ownership to a new SSLIOStream *before* the handshake
completes and sets self.socket to None on the original stream. The
gen.with_timeout call around start_tls only raises TimeoutError to the
caller; the original stream is now a no-op (its socket is gone) and the
new SSLIOStream that actually owns the socket is reachable only through
the inner future -- which gen.with_timeout deliberately leaves running,
per its docstring.

Capture the future returned by start_tls so that, on timeout, we register
a done-callback that closes the resulting SSLIOStream. If the handshake
eventually fails, SSLIOStream already closes its socket on the failure
path, so the callback only acts when the future resolved successfully.

Regression test in tcpclient_test.py replaces start_tls with a stub that
builds a real SSLIOStream around the underlying socket but never completes
the handshake, instruments close() to record the stream, and asserts that
the SSLIOStream is closed when the timeout fires and the future is
subsequently resolved. All 28 existing tcpclient tests still pass.
…nadoweb#3360)

send_error unconditionally called write_error(), which on the default
RequestHandler writes a small HTML page like
"<html><title>204: No Content</title>...". That buffer then propagated
into the `_status_code in (204, 304) or (100 <= _status_code < 200)`
assertion in finish(), raising "Cannot send body with 204" -- and
worse, when run under `python -O` (which strips assertions) the
client received a 204 response with a body and no Content-Length
header, in violation of RFC 9110.

Skip write_error for 204 (and keep the existing 304 skip) so the
default branch produces a header-only 204 response.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Send 204 as HTTPError

1 participant