#36655: GZipMiddleware buffers streaming responses
-------------------------------+----------------------------------------
     Reporter:  Adam Johnson   |                    Owner:  Adam Johnson
         Type:  Bug            |                   Status:  assigned
    Component:  HTTP handling  |                  Version:  dev
     Severity:  Normal         |               Resolution:
     Keywords:                 |             Triage Stage:  Unreviewed
    Has patch:  0              |      Needs documentation:  0
  Needs tests:  0              |  Patch needs improvement:  0
Easy pickings:  0              |                    UI/UX:  0
-------------------------------+----------------------------------------
Changes (by Adam Johnson):

 * summary:  GzipMiddleware buffers streaming responses => GZipMiddleware
     buffers streaming responses


Old description:

> Currently, `GzipMiddleware`, via `compress_sequence()`, buffers the
> entire response before sending it to the client. This can cause issues
> for clients that expect to receive data in chunks, such as those using
> Server-Sent Events (SSE) or WebSockets.
>
> This issue was reported to me in the django-browser-reload project back
> in [https://github.com/adamchainz/django-browser-reload/pull/161 Issue
> #161 (2023)], where a contributor fixed it with a workaround, and I
> didn't think to investigate. Now, while implementing
> [https://github.com/adamchainz/django-http-compression django-http-
> compression], I have realized that it’s a proper bug that can be fixed by
> adding a call to `zfile.flush()`, as done in its
> [https://github.com/adamchainz/django-http-compression/pull/8 PR #8].
>
> To reproduce the issue, use the below app, which can be run with `uv run
> --script`. If you comment out `GzipMiddleware` and load the page in a
> browser, you will see the numbers incrementing every second. If you
> include `GzipMiddleware`, the page will never load. Adding the
> `zfile.flush()` call in `compress_sequence()` fixes the issue.
>
> {{{#!python
> #!/usr/bin/env uv run --script
> # /// script
> # requires-python = ">=3.14"
> # dependencies = [
> #     "django",
> # ]
> # ///
> from __future__ import annotations
>
> import os
> import sys
> import time
>
> from django.conf import settings
> from django.core.wsgi import get_wsgi_application
> from django.http import StreamingHttpResponse
> from django.urls import path
>
> settings.configure(
>     # Dangerous: disable host header validation
>     ALLOWED_HOSTS=["*"],
>     # Use DEBUG=1 to enable debug mode
>     DEBUG=(os.environ.get("DEBUG", "") == "1"),
>     # Make this module the urlconf
>     ROOT_URLCONF=__name__,
>     # Only gzip middleware
>     MIDDLEWARE=[
>         "django.middleware.gzip.GZipMiddleware",
>     ],
> )
>

> def clock(request):
>     def stream():
>         yield "<h1>Clock</h1>\n"
>         count = 1
>         while True:
>             yield f"<p>{count}</p>\n"
>             count += 1
>             time.sleep(1)
>
>     return StreamingHttpResponse(stream())
>

> urlpatterns = [
>     path("", clock),
> ]
>
> app = get_wsgi_application()
>
> if __name__ == "__main__":
>     from django.core.management import execute_from_command_line
>
>     execute_from_command_line(sys.argv)
> }}}

New description:

 Currently, `GZipMiddleware`, via `compress_sequence()`, buffers the entire
 response before sending it to the client. This can cause issues for
 clients that expect to receive data in chunks, such as those using Server-
 Sent Events (SSE) or WebSockets.

 This issue was reported to me in the django-browser-reload project back in
 [https://github.com/adamchainz/django-browser-reload/pull/161 Issue #161
 (2023)], where a contributor fixed it with a workaround, and I didn't
 think to investigate. Now, while implementing
 [https://github.com/adamchainz/django-http-compression django-http-
 compression], I have realized that it’s a proper bug that can be fixed by
 adding a call to `zfile.flush()`, as done in its
 [https://github.com/adamchainz/django-http-compression/pull/8 PR #8].

 To reproduce the issue, use the app below, which can be run with `uv run
 --script`. If you comment out `GzipMiddleware` and load the page in a
 browser, you will see the numbers incrementing every second. If you
 include `GzipMiddleware`, the page will never load. Adding the
 `zfile.flush()` call in `compress_sequence()` fixes the issue.

 {{{#!python
 #!/usr/bin/env uv run --script
 # /// script
 # requires-python = ">=3.14"
 # dependencies = [
 #     "django",
 # ]
 # ///
 from __future__ import annotations

 import os
 import sys
 import time

 from django.conf import settings
 from django.core.wsgi import get_wsgi_application
 from django.http import StreamingHttpResponse
 from django.urls import path

 settings.configure(
     # Dangerous: disable host header validation
     ALLOWED_HOSTS=["*"],
     # Use DEBUG=1 to enable debug mode
     DEBUG=(os.environ.get("DEBUG", "") == "1"),
     # Make this module the urlconf
     ROOT_URLCONF=__name__,
     # Only gzip middleware
     MIDDLEWARE=[
         "django.middleware.gzip.GZipMiddleware",
     ],
 )


 def clock(request):
     def stream():
         yield "<h1>Clock</h1>\n"
         count = 1
         while True:
             yield f"<p>{count}</p>\n"
             count += 1
             time.sleep(1)

     return StreamingHttpResponse(stream())


 urlpatterns = [
     path("", clock),
 ]

 app = get_wsgi_application()

 if __name__ == "__main__":
     from django.core.management import execute_from_command_line

     execute_from_command_line(sys.argv)
 }}}

--
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36655#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/01070199cee3ba32-cbfa8cfe-45d2-4668-b39d-cb0565094a03-000000%40eu-central-1.amazonses.com.

Reply via email to