1
0
mirror of https://github.com/django/django.git synced 2025-06-01 09:39:12 +00:00

Fixed #36281 -- Used async-safe write in ASGIHandler.read_body().

Thanks Carlton Gibson for reviews.
This commit is contained in:
신우진 2025-04-08 16:20:37 +09:00 committed by Mariusz Felisiak
parent 9d93e35c20
commit 1fb3f57e81
2 changed files with 73 additions and 1 deletions

View File

@ -263,7 +263,16 @@ class ASGIHandler(base.BaseHandler):
raise RequestAborted()
# Add a body chunk from the message, if provided.
if "body" in message:
body_file.write(message["body"])
on_disk = getattr(body_file, "_rolled", False)
if on_disk:
async_write = sync_to_async(
body_file.write,
thread_sensitive=False,
)
await async_write(message["body"])
else:
body_file.write(message["body"])
# Quit out if that's the end.
if not message.get("more_body", False):
break

View File

@ -1,8 +1,10 @@
import asyncio
import sys
import tempfile
import threading
import time
from pathlib import Path
from unittest.mock import patch
from asgiref.sync import sync_to_async
from asgiref.testing import ApplicationCommunicator
@ -659,3 +661,64 @@ class ASGITest(SimpleTestCase):
# 'last\n' isn't sent.
with self.assertRaises(asyncio.TimeoutError):
await communicator.receive_output(timeout=0.2)
async def test_read_body_thread(self):
"""Write runs on correct thread depending on rollover."""
handler = ASGIHandler()
loop_thread = threading.current_thread()
called_threads = []
def write_wrapper(data):
called_threads.append(threading.current_thread())
return original_write(data)
# In-memory write (no rollover expected).
in_memory_chunks = [
{"type": "http.request", "body": b"small", "more_body": False}
]
async def receive():
return in_memory_chunks.pop(0)
with tempfile.SpooledTemporaryFile(max_size=1024, mode="w+b") as temp_file:
original_write = temp_file.write
with (
patch(
"django.core.handlers.asgi.tempfile.SpooledTemporaryFile",
return_value=temp_file,
),
patch.object(temp_file, "write", side_effect=write_wrapper),
):
await handler.read_body(receive)
# Write was called in the event loop thread.
self.assertIn(loop_thread, called_threads)
# Clear thread log before next test.
called_threads.clear()
# Rollover to disk (write should occur in a threadpool thread).
rolled_chunks = [
{"type": "http.request", "body": b"A" * 16, "more_body": True},
{"type": "http.request", "body": b"B" * 16, "more_body": False},
]
async def receive_rolled():
return rolled_chunks.pop(0)
with (
override_settings(FILE_UPLOAD_MAX_MEMORY_SIZE=10),
tempfile.SpooledTemporaryFile(max_size=10, mode="w+b") as temp_file,
):
original_write = temp_file.write
# roll_over force in handlers.
with (
patch(
"django.core.handlers.asgi.tempfile.SpooledTemporaryFile",
return_value=temp_file,
),
patch.object(temp_file, "write", side_effect=write_wrapper),
):
await handler.read_body(receive_rolled)
# The second write should have rolled over to disk.
self.assertTrue(any(t != loop_thread for t in called_threads))