🐛 Fix stale Content-Length in 205 Reset Content responses by subham-hq · Pull Request #16424 · fastapi/fastapi · GitHub
Skip to content

🐛 Fix stale Content-Length in 205 Reset Content responses - #16424

Open
subham-hq wants to merge 1 commit into
fastapi:masterfrom
subham-hq:fix/status-205-content-length
Open

subham-hq wants to merge 1 commit into
fastapi:masterfrom
subham-hq:fix/status-205-content-length

Conversation

@subham-hq

Copy link
Copy Markdown

Summary

A path operation that uses status 205 Reset Content and returns anything (even None) produces a response whose Content-Length header doesn't match its (empty) body. Under Uvicorn every such request fails and the client receives a truncated response.

This PR sets Content-Length: 0 when FastAPI drops the body of a 205 response. No other status code is affected.

Reproduction

from fastapi import FastAPI

app = FastAPI()


@app.post("/reset", status_code=205)
def reset():
    return None

Run it with uvicorn main:app and send a request:

$ curl -v -X POST http://127.0.0.1:8000/reset
< HTTP/1.1 205 Reset Content
< content-length: 4
* transfer closed with 4 bytes remaining to read

Uvicorn logs the following for every request:

RuntimeError: Response content shorter than Content-Length

The same happens when the status is set dynamically through the Response parameter:

from fastapi import FastAPI, Response

app = FastAPI()


@app.post("/reset")
def reset(response: Response):
    response.status_code = 205
    return {"message": "Form submitted"}

Here the header is content-length: 28 while the body is empty.

Expected: a valid 205 response with content-length: 0 and an empty body, so the connection can be reused.

Actual: content-length is the length of the serialized return value, the body is empty, the server errors and the connection is dropped.

Root cause

In get_request_handler() (fastapi/routing.py), the response is built first and the body is dropped afterwards for status codes that must not have one:

response = actual_response_class(content, **response_args)
if not is_body_allowed_for_status_code(response.status_code):
    response.body = b""

By the time response.body is cleared, the response class has already computed Content-Length from the original content (e.g. 4 for null), so the header goes stale.

This only causes a mismatch for 205. Starlette's Response.init_headers() already skips Content-Length for status codes below 200 and for 204 and 304, so for those there is no header to go stale. 205 is the only status FastAPI treats as "no body" (is_body_allowed_for_status_code() returns False for < 200, 204, 205 and 304) that Starlette still adds a Content-Length for.

The fix

if not is_body_allowed_for_status_code(response.status_code):
    response.body = b""
    # Unlike 1xx, 204, and 304, a 205 response is still
    # delimited by Content-Length, so it has to match the
    # now empty body
    # Ref: https://www.rfc-editor.org/rfc/rfc9112#section-6.3
    if response.status_code == 205:
        response.headers["content-length"] = "0"

The change runs before the headers from the Response parameter are appended, so it only corrects the value computed by the response class and never overrides a header set explicitly by the user.

Why only 205 (re: #16386)

#16386 tried to fix the same bug and was closed with the concern that the change might break something else, specifically 304 responses, which may legitimately carry a Content-Length. This PR is scoped to address that:

  • RFC 9112 §6.3 (message body length): any response with a 1xx, 204 or 304 status is always terminated by the first empty line after the header fields, regardless of the header fields present. So for those, Content-Length doesn't affect framing, and they are left untouched.
  • 205 is not in that list, so its body length is determined by Content-Length. If the header says 4 and no bytes follow, the message is malformed. That's exactly the error Uvicorn reports.
  • RFC 9110 §15.3.6: a server must not generate content in a 205 response. With Content-Length: 0 the response is correct and the connection stays reusable.
  • RFC 9110 §8.6: a server may send Content-Length in a 304 with the size of the representation that a 200 would have returned. This PR does not touch 304, and a test confirms that a Content-Length set by the user on a 304 is preserved.

Why 0 rather than removing the header: without Content-Length or Transfer-Encoding: chunked, an HTTP/1.1 client has to read until the connection closes to know where the response ends, which breaks keep-alive. An explicit Content-Length: 0 is the unambiguous way to frame an empty 205.

Tests

New file tests/test_response_code_no_body_content_length.py:

Test Before the fix After the fix
205 via status_code=205, returning a dict ❌ content-length: 28 ✅ 0
205 via status_code=205, returning None ❌ content-length: 4 ✅ 0
205 set through the Response parameter ❌ content-length: 28 ✅ 0
304 with a user-set Content-Length: 17 ✅ kept ✅ kept

Also checked:

  • The existing test suite passes, including test_response_code_no_body.py and the tutorial tests.
  • ruff check, ruff format --check and mypy pass on the changed files.
  • Manually verified with Uvicorn: the 205 response now has content-length: 0, no error is logged and the connection stays open.

Scope and compatibility

  • 6 lines in fastapi/routing.py, only on the code path where FastAPI already drops the body.
  • No public API changes.
  • Responses returned directly from the endpoint (a Response instance) are not affected, since they don't go through this path.
  • Behavior only changes for 205 responses, which were broken before.

When a path operation returns content with status 205 (set via
`status_code=205` or the `Response` parameter), FastAPI empties
`response.body`, but the response class had already computed
Content-Length from the original content (e.g. 4 for `null`). Servers
like Uvicorn then fail with "Response content shorter than
Content-Length" and the client gets a truncated response.

Set Content-Length to 0 when the body of a 205 response is dropped.
Only 205 is changed: per RFC 9112 section 6.3, 1xx, 204 and 304
responses are terminated after the header section regardless of
Content-Length (Starlette doesn't set it for them), while a 205 is
still delimited by it. A Content-Length set by the user on a 304,
allowed by RFC 9110 section 8.6, is left untouched.
@codspeed

codspeed Bot commented Oct 1, 2026

Copy link
Copy Markdown

This branch has not been deployed

No deployments
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.

1 participant