1
0
mirror of https://github.com/django/django.git synced 2025-06-03 02:29:13 +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() raise RequestAborted()
# Add a body chunk from the message, if provided. # Add a body chunk from the message, if provided.
if "body" in message: 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. # Quit out if that's the end.
if not message.get("more_body", False): if not message.get("more_body", False):
break break

View File

@ -1,8 +1,10 @@
import asyncio import asyncio
import sys import sys
import tempfile
import threading import threading
import time import time
from pathlib import Path from pathlib import Path
from unittest.mock import patch
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from asgiref.testing import ApplicationCommunicator from asgiref.testing import ApplicationCommunicator
@ -659,3 +661,64 @@ class ASGITest(SimpleTestCase):
# 'last\n' isn't sent. # 'last\n' isn't sent.
with self.assertRaises(asyncio.TimeoutError): with self.assertRaises(asyncio.TimeoutError):
await communicator.receive_output(timeout=0.2) 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))