Opened 7 weeks ago

Last modified 29 hours ago

#37319 assigned Bug

GZipMiddleware should skip responses without body.

Reported by: Vladimir Nani Owned by: Vimal Sahani
Component: HTTP handling Version: 6.1
Severity: Normal Keywords: GZipMiddleware
Cc: Vaibhav Pant Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

GZipMiddleware compresses responses with status 304 that are not allowed to have content body.

Negative effect:

  • streaming response with 304 status code and empty body is passed to GZipMiddleware
  • Middleware check doesnt apply for streaming response and proceeds with compression
  • In situation where connections are pooled, for example with Traefik, the body is not read (according to rfc 9112).
  • The empty body compressed is left as a garbage in the sockets buffer.
  • When the socket is read for the next time instead of starting with "HTTP" it reads "\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\xffaaaa"
  • The next unrelated request that reuses the socket fails with malformed http:
{"level":"debug","error":"net/http: HTTP/1.x transport connection broken: malformed HTTP version \"\\x1f\\x8b\\b\\b\\x00\\x00\\x00\\x00\\x00\\xffaaaaaaaaaaaaaaaa\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00HTTP/1.1\"","time":"2026-09-04T12:33:17+02:00","caller":"github.com/traefik/traefik/v3/pkg/proxy/httputil/proxy.go:117","message":"500 Internal Server Error"}

Proposal:
Check for 204 and 304 and do not compress these responses.

​https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.1

Change History (15)

comment:1 by Md. Saikat Islam, 7 weeks ago

You are right. The middleware currently handles 204, 304, and short responses under 200 bytes only in the non-streaming case. This is the guard in GZipMiddleware:

if not response.streaming and len(response.content) < 200:
    return response

For streaming responses, this guard does not apply, and the middleware compresses them anyway — which it should not do.

I tested this to verify for 204, 304, short response, and long response.

Non-streaming (test.py):

204 204 None b''
304 304 None b''
short 200 200 None b'short body'
long 200 200 gzip b'\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\x03aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x00\xab\xa8\x18\x05#\r\x00\x00Q\xce\xb2\n\xf4\x01\x00\x00'

Streaming (test_streaming.py):

streaming 304 empty 304 gzip b'\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\xffaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00'
streaming 204 empty 204 gzip b'\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\xffaaaaaaa\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00'
streaming short 200 gzip b'\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\xffaa\x00*\xce\xc8/*QH\xcaO\xa9\x04\x00\x00\x00\xff\xff\x03\x009\xb2\xed\xc0\n\x00\x00\x00'
streaming 200 long 200 gzip b'\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\xffaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x00JL\x1c\x05#\r\x00\x00\x00\x00\xff\xff\x03\x00\x11A\x92\x05\xf4\x01\x00\x00'

In the streaming case, even the 204 and 304 responses with empty content still get Content-Encoding: gzip and non-empty compressed bytes — gzip-compressing an empty sequence still produces gzip header/footer framing, so it's never truly empty. This matches the bug behavior described in the report.

I can provide the test files here if needed.

I would like to work on this issue.

comment:2 by Yassin Bahri, 7 weeks ago

I reproduced this on current Django main through the WSGI handler.

A StreamingHttpResponse([], status=304) passed through GZipMiddleware with Accept-Encoding: gzip is returned with Content-Encoding: gzip and a non-empty gzip payload. The same behavior occurs with status 204.

RFC 9112 section 6.3 states that responses with 1xx, 204, or 304 status codes cannot contain a message body:

​https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3

The issue therefore appears valid and actionable. I suggest classifying it as:

  • Type: Bug
  • Component: HTTP handling
  • Severity: Normal
  • Triage stage: Accepted

I would also suggest narrowing the summary to:

GZipMiddleware compresses streaming 204 and 304 responses.

“Responses without body” is slightly ambiguous because an empty 200 response may legally be gzip encoded. The problem here is responses whose status code prohibits content.

Before implementing the fix, the complete scope should be considered. RFC 9112 also covers 1xx responses, while RFC 9110 prohibits content in 205 responses. HEAD responses have separate semantics and may still include representation metadata, so they should not automatically be treated identically.

This does not appear to be a Django 6.1 regression; the relevant middleware behavior is also present in older supported branches.

comment:3 by Md. Saikat Islam, 7 weeks ago

I will wait untill a maintainer set the triage stage to accepted. After that I will assign myself to this issue.

comment:4 by Vladimir Nani, 7 weeks ago

It looks like gunicorn has added the checks for 304 and 204 as well. ​https://github.com/benoitc/gunicorn/commit/9bc5891b4b06f25a8ce0e707053dcb2fb9bf638c#diff-0b3107decc367439e32c2eb51c3a8270d4399fa51c45e89f6a5e740e3c631188R353

I wouldn't have spotted the problem on the later version of gunicorn.

Last edited 7 weeks ago by Vladimir Nani (previous) (diff)

comment:6 by Vladimir Nani, 7 weeks ago

Component: Uncategorized → HTTP handling
Has patch: set
Type: Uncategorized → Bug
Version: 6.1

comment:7 by Md. Saikat Islam, 7 weeks ago

According to the triaging guidelines, another community member should review and reproduce the issue before it’s marked as “Accepted”. Reference: ​https://docs.djangoproject.com/en/dev/internals/contributing/triaging-tickets/

Since Yassin Bahri independently reproduced the issue, and I was also able to reproduce it, I’m marking this ticket as accepted.

comment:8 by Md. Saikat Islam, 7 weeks ago

Owner: set to Md. Saikat Islam
Status: new → assigned
Triage Stage: Unreviewed → Accepted
Version: → 6.1

comment:9 by Md. Saikat Islam, 3 weeks ago

Owner: Md. Saikat Islam removed
Status: assigned → new

comment:10 by OsmnvAslan, 3 weeks ago

Needs documentation: set
Patch needs improvement: set

comment:11 by Vaibhav Pant, 10 days ago

Owner: set to Vaibhav Pant
Status: new → assigned

in reply to:  5 comment:12 by Vaibhav Pant, 10 days ago

Replying to Vladimir Nani:

​https://github.com/django/django/commit/324434140cf30b4e1923f3bb251107afcdafc6db have a patch to fix this

Hi @vladimirnani, I’ve claimed the ticket on Trac, so the missing-owner check that closed this PR shouldn’t be a problem anymore. Would you like to reopen it and push the remaining review changes? I’ll wait a little while, and if it’s still closed after that, I’ll open a new PR based on yours and make sure you’re credited. Thanks for the work on this!

comment:13 by Vaibhav Pant, 6 days ago

Cc: Vaibhav Pant added
Owner: Vaibhav Pant removed
Status: assigned → new

comment:14 by Vimal Sahani, 40 hours ago

Owner: set to Vimal Sahani
Status: new → assigned

in reply to:  14 comment:15 by Vaibhav Pant, 29 hours ago

Replying to Vimal Sahani:

Hi, please check the existing comments before claiming an issue or opening a PR. I had already started working on this and was waiting for a response regarding the existing PR. It would have been better to leave a comment and allow some time for a response. Thanks for understanding!

Note: See TracTickets for help on using tickets.
Back to Top