Skip to content

Commit a6d0394

Browse files
committed
Detect blocking calls in coroutines using BlockBuster
1 parent 1695942 commit a6d0394

File tree

8 files changed

+25
-11
lines changed

8 files changed

+25
-11
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ venv*/
1111
.python-version
1212
build/
1313
dist/
14+
.idea/

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ types-PyYAML==6.0.12.20241230
1212
types-dataclasses==0.6.6
1313
pytest==8.3.4
1414
trio==0.28.0
15+
blockbuster==1.5.13
1516

1617
# Documentation
1718
black==25.1.0

starlette/datastructures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import sys
34
import typing
45
from shlex import shlex
56
from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit
@@ -438,7 +439,7 @@ async def write(self, data: bytes) -> None:
438439
if self.size is not None:
439440
self.size += len(data)
440441

441-
if self._in_memory:
442+
if self._in_memory and self.file.tell() + len(data) <= getattr(self.file, "_max_size", sys.maxsize):
442443
self.file.write(data)
443444
else:
444445
await run_in_threadpool(self.file.write, data)

starlette/middleware/errors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import traceback
77
import typing
88

9+
import anyio
10+
911
from starlette._utils import is_async_callable
1012
from starlette.concurrency import run_in_threadpool
1113
from starlette.requests import Request
@@ -167,7 +169,7 @@ async def _send(message: Message) -> None:
167169
request = Request(scope)
168170
if self.debug:
169171
# In debug mode, return traceback responses.
170-
response = self.debug_response(request, exc)
172+
response = await anyio.to_thread.run_sync(self.debug_response, request, exc)
171173
elif self.handler is None:
172174
# Use our default 500 error handler.
173175
response = self.error_response(request, exc)

starlette/testclient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ async def receive() -> Message:
288288
await response_complete.wait()
289289
return {"type": "http.disconnect"}
290290

291-
body = request.read()
291+
body = await anyio.to_thread.run_sync(request.read)
292292
if isinstance(body, str):
293293
body_bytes: bytes = body.encode("utf-8") # pragma: no cover
294294
elif body is None:

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
from __future__ import annotations
22

33
import functools
4+
from collections.abc import Iterator
45
from typing import Any, Literal
56

67
import pytest
8+
from blockbuster import blockbuster_ctx
79

810
from starlette.testclient import TestClient
911
from tests.types import TestClientFactory
1012

1113

14+
@pytest.fixture(autouse=True)
15+
def blockbuster() -> Iterator[None]:
16+
with blockbuster_ctx("starlette") as bb:
17+
bb.functions["os.stat"].can_block_in("/mimetypes.py", "init")
18+
yield
19+
20+
1221
@pytest.fixture
1322
def test_client_factory(
1423
anyio_backend_name: Literal["asyncio", "trio"],

tests/middleware/test_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,9 @@ async def cancel_on_disconnect(
466466
# before we start returning the body
467467
await task_group.start(cancel_on_disconnect)
468468

469-
# A timeout is set for 0.1 second in order to ensure that
469+
# A timeout is set for 0.2 second in order to ensure that
470470
# we never deadlock the test run in an infinite loop
471-
with anyio.move_on_after(0.1):
471+
with anyio.move_on_after(0.2):
472472
while True:
473473
await send(
474474
{

tests/test_templates.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_templates(tmpdir: Path, test_client_factory: TestClientFactory) -> None
2323
with open(path, "w") as file:
2424
file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>")
2525

26-
async def homepage(request: Request) -> Response:
26+
def homepage(request: Request) -> Response:
2727
return templates.TemplateResponse(request, "index.html")
2828

2929
app = Starlette(debug=True, routes=[Route("/", endpoint=homepage)])
@@ -40,7 +40,7 @@ def test_calls_context_processors(tmp_path: Path, test_client_factory: TestClien
4040
path = tmp_path / "index.html"
4141
path.write_text("<html>Hello {{ username }}</html>")
4242

43-
async def homepage(request: Request) -> Response:
43+
def homepage(request: Request) -> Response:
4444
return templates.TemplateResponse(request, "index.html")
4545

4646
def hello_world_processor(request: Request) -> dict[str, str]:
@@ -69,7 +69,7 @@ def test_template_with_middleware(tmpdir: Path, test_client_factory: TestClientF
6969
with open(path, "w") as file:
7070
file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>")
7171

72-
async def homepage(request: Request) -> Response:
72+
def homepage(request: Request) -> Response:
7373
return templates.TemplateResponse(request, "index.html")
7474

7575
class CustomMiddleware(BaseHTTPMiddleware):
@@ -96,15 +96,15 @@ def test_templates_with_directories(tmp_path: Path, test_client_factory: TestCli
9696
template_a = dir_a / "template_a.html"
9797
template_a.write_text("<html><a href='{{ url_for('page_a') }}'></a> a</html>")
9898

99-
async def page_a(request: Request) -> Response:
99+
def page_a(request: Request) -> Response:
100100
return templates.TemplateResponse(request, "template_a.html")
101101

102102
dir_b = tmp_path.resolve() / "b"
103103
dir_b.mkdir()
104104
template_b = dir_b / "template_b.html"
105105
template_b.write_text("<html><a href='{{ url_for('page_b') }}'></a> b</html>")
106106

107-
async def page_b(request: Request) -> Response:
107+
def page_b(request: Request) -> Response:
108108
return templates.TemplateResponse(request, "template_b.html")
109109

110110
app = Starlette(
@@ -150,7 +150,7 @@ def test_templates_with_environment(tmpdir: Path, test_client_factory: TestClien
150150
with open(path, "w") as file:
151151
file.write("<html>Hello, <a href='{{ url_for('homepage') }}'>world</a></html>")
152152

153-
async def homepage(request: Request) -> Response:
153+
def homepage(request: Request) -> Response:
154154
return templates.TemplateResponse(request, "index.html")
155155

156156
env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(tmpdir)))

0 commit comments

Comments
 (0)