1
0
mirror of https://github.com/django/django.git synced 2025-11-07 07:15:35 +00:00

Fixed #36656 -- Avoided truncating async streaming responses in GZipMiddleware.

This commit is contained in:
Adam Johnson
2025-10-11 00:10:35 +01:00
committed by Jacob Walls
parent 9bb83925d6
commit a0323a0c44
3 changed files with 24 additions and 15 deletions

View File

@@ -1,7 +1,7 @@
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
from django.utils.text import compress_sequence, compress_string from django.utils.text import acompress_sequence, compress_sequence, compress_string
re_accepts_gzip = _lazy_re_compile(r"\bgzip\b") re_accepts_gzip = _lazy_re_compile(r"\bgzip\b")
@@ -32,18 +32,10 @@ class GZipMiddleware(MiddlewareMixin):
if response.streaming: if response.streaming:
if response.is_async: if response.is_async:
# pull to lexical scope to capture fixed reference in case response.streaming_content = acompress_sequence(
# streaming_content is set again later. response.streaming_content,
original_iterator = response.streaming_content max_random_bytes=self.max_random_bytes,
)
async def gzip_wrapper():
async for chunk in original_iterator:
yield compress_string(
chunk,
max_random_bytes=self.max_random_bytes,
)
response.streaming_content = gzip_wrapper()
else: else:
response.streaming_content = compress_sequence( response.streaming_content = compress_sequence(
response.streaming_content, response.streaming_content,

View File

@@ -393,6 +393,22 @@ def compress_sequence(sequence, *, max_random_bytes=None):
yield buf.read() yield buf.read()
async def acompress_sequence(sequence, *, max_random_bytes=None):
buf = StreamingBuffer()
filename = _get_random_filename(max_random_bytes) if max_random_bytes else None
with GzipFile(
filename=filename, mode="wb", compresslevel=6, fileobj=buf, mtime=0
) as zfile:
# Output headers...
yield buf.read()
async for item in sequence:
zfile.write(item)
data = buf.read()
if data:
yield data
yield buf.read()
# Expression to match some_token and some_token="with spaces" (and similarly # Expression to match some_token and some_token="with spaces" (and similarly
# for single-quoted strings). # for single-quoted strings).
smart_split_re = _lazy_re_compile( smart_split_re = _lazy_re_compile(

View File

@@ -2,6 +2,7 @@ import gzip
import random import random
import re import re
import struct import struct
import zlib
from io import BytesIO from io import BytesIO
from unittest import mock from unittest import mock
from urllib.parse import quote from urllib.parse import quote
@@ -880,8 +881,8 @@ class GZipMiddlewareTest(SimpleTestCase):
@staticmethod @staticmethod
def decompress(gzipped_string): def decompress(gzipped_string):
with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f: # Use zlib to ensure gzipped_string contains exactly one gzip stream.
return f.read() return zlib.decompress(gzipped_string, zlib.MAX_WBITS | 16)
@staticmethod @staticmethod
def get_mtime(gzipped_string): def get_mtime(gzipped_string):