Skip to content

Commit 7af2b8f

Browse files
authored
fix(request): content-length check incompatible with decompression middleware (#4690)
## Summary - Remove the content-length integrity check in `Request.stream()` that raised `ClientException("Malformed request")` when streamed bytes exceeded the `Content-Length` header - Replace with unconditional `request_max_body_size` enforcement on all requests - Update docs to reflect that content-length validation is delegated to the ASGI server Closes #4125
1 parent d172c73 commit 7af2b8f

3 files changed

Lines changed: 35 additions & 27 deletions

File tree

docs/usage/requests.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ To disable this limit for a specific handler / router / controller, it can be se
304304
For requests that define a ``Content-Length`` header, Litestar will not attempt to
305305
read the request body should the header value exceed the ``request_max_body_size``.
306306

307-
If the header value is within the allowed bounds, Litestar will verify during the
308-
streaming of the request body that it does not exceed the size specified in the
309-
header. Should the request exceed this size, it will abort the request with a
310-
``400 - Bad Request``.
307+
Validation that the actual request body matches the ``Content-Length`` header is
308+
delegated to the ASGI server (e.g. uvicorn). This ensures compatibility with
309+
request decompression middleware, where the decompressed body may legitimately
310+
exceed the wire-format ``Content-Length``.

litestar/connection/request.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -208,23 +208,12 @@ async def stream(self) -> AsyncGenerator[bytes, None]:
208208
if body:
209209
total_bytes_streamed += len(body)
210210

211-
# if a 'content-length' header was set, check if we have
212-
# received more bytes than specified. in most cases this should
213-
# be caught before it hits the application layer and an ASGI
214-
# server (e.g. uvicorn) will not allow this, but since it's not
215-
# forbidden according to the HTTP or ASGI spec, we err on the
216-
# side of caution and still perform this check.
217-
#
218-
# uvicorn documented behaviour for this case:
219-
# https://github.com/encode/uvicorn/blob/fe3910083e3990695bc19c2ef671dd447262ae18/docs/server-behavior.md?plain=1#L11
220-
if announced_content_length:
221-
if total_bytes_streamed > announced_content_length:
222-
raise ClientException("Malformed request")
223-
224-
# we don't have a 'content-length' header, likely a chunked
225-
# transfer. we don't really care and simply check if we have
226-
# received more bytes than allowed
227-
elif total_bytes_streamed > max_content_length:
211+
# enforce the request body size limit on actual bytes
212+
# received. content-length validation is intentionally left to
213+
# the ASGI server (e.g. uvicorn) since middleware such as
214+
# request decompression can legitimately cause the streamed
215+
# body to exceed the wire-format content-length.
216+
if total_bytes_streamed > max_content_length:
228217
raise RequestEntityTooLarge
229218

230219
yield body

tests/unit/test_connection/test_request.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -538,15 +538,34 @@ async def get_state(request: Request[Any, Any, State]) -> dict[str, str]:
538538
assert response.json() == {"state": 2}
539539

540540

541-
def test_request_body_exceeds_content_length() -> None:
542-
@post("/")
543-
def handler(body: bytes) -> None:
544-
pass
541+
def test_request_body_exceeding_content_length_is_allowed() -> None:
542+
"""Body larger than Content-Length but within request_max_body_size succeeds.
543+
544+
Decompression middleware can legitimately produce a body larger than the
545+
wire-format Content-Length. Content-length enforcement is delegated to the
546+
ASGI server (e.g. uvicorn).
547+
"""
548+
549+
@post("/", request_max_body_size=10)
550+
def handler(body: bytes) -> bytes:
551+
return body
545552

546553
with create_test_client([handler]) as client:
547554
response = client.post("/", headers={"content-length": "1"}, content=b"ab")
548-
assert response.status_code == HTTP_400_BAD_REQUEST
549-
assert response.json() == {"status_code": 400, "detail": "Malformed request"}
555+
assert response.status_code == 201
556+
assert response.content == b"ab"
557+
558+
559+
def test_request_body_exceeding_content_length_still_enforces_max_body_size() -> None:
560+
"""Content-length mismatch is allowed, but request_max_body_size is still enforced."""
561+
562+
@post("/", request_max_body_size=1)
563+
def handler(body: bytes) -> bytes:
564+
return body
565+
566+
with create_test_client([handler]) as client:
567+
response = client.post("/", headers={"content-length": "1"}, content=b"ab")
568+
assert response.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE
550569

551570

552571
def test_request_body_exceeds_max_request_body_size() -> None:

0 commit comments

Comments
 (0)