Skip to content

Commit 6715eb4

Browse files
authored
Deprecate FileResponse(method=...) parameter (#2366)
1 parent 1ed1737 commit 6715eb4

4 files changed

Lines changed: 44 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ filterwarnings = [
8888
"ignore: The `allow_redirects` argument is deprecated. Use `follow_redirects` instead.:DeprecationWarning",
8989
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
9090
"ignore: You seem to already have a custom sys.excepthook handler installed. I'll skip installing Trio's custom handler, but this means MultiErrors will not show full tracebacks.:RuntimeWarning",
91+
"ignore: The 'method' parameter is not used, and it will be removed.:DeprecationWarning:starlette",
9192
]
9293

9394
[tool.coverage.run]

starlette/responses.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import os
44
import stat
55
import typing
6+
import warnings
67
from datetime import datetime
78
from email.utils import format_datetime, formatdate
89
from functools import partial
910
from mimetypes import guess_type
1011
from urllib.parse import quote
1112

1213
import anyio
14+
import anyio.to_thread
1315

1416
from starlette._compat import md5_hexdigest
1517
from starlette.background import BackgroundTask
@@ -280,7 +282,11 @@ def __init__(
280282
self.path = path
281283
self.status_code = status_code
282284
self.filename = filename
283-
self.send_header_only = method is not None and method.upper() == "HEAD"
285+
if method is not None:
286+
warnings.warn(
287+
"The 'method' parameter is not used, and it will be removed.",
288+
DeprecationWarning,
289+
)
284290
if media_type is None:
285291
media_type = guess_type(filename or path)[0] or "text/plain"
286292
self.media_type = media_type
@@ -329,7 +335,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
329335
"headers": self.raw_headers,
330336
}
331337
)
332-
if self.send_header_only:
338+
if scope["method"].upper() == "HEAD":
333339
await send({"type": "http.response.body", "body": b"", "more_body": False})
334340
else:
335341
async with await anyio.open_file(self.path, mode="rb") as file:

starlette/staticfiles.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from email.utils import parsedate
77

88
import anyio
9+
import anyio.to_thread
910

1011
from starlette.datastructures import URL, Headers
1112
from starlette.exceptions import HTTPException
@@ -154,12 +155,7 @@ async def get_response(self, path: str, scope: Scope) -> Response:
154155
self.lookup_path, "404.html"
155156
)
156157
if stat_result and stat.S_ISREG(stat_result.st_mode):
157-
return FileResponse(
158-
full_path,
159-
stat_result=stat_result,
160-
method=scope["method"],
161-
status_code=404,
162-
)
158+
return FileResponse(full_path, stat_result=stat_result, status_code=404)
163159
raise HTTPException(status_code=404)
164160

165161
def lookup_path(

tests/test_responses.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import os
33
import time
44
from http.cookies import SimpleCookie
5+
from pathlib import Path
56

67
import anyio
78
import pytest
89

910
from starlette import status
1011
from starlette.background import BackgroundTask
12+
from starlette.datastructures import Headers
1113
from starlette.requests import Request
1214
from starlette.responses import (
1315
FileResponse,
@@ -17,6 +19,7 @@
1719
StreamingResponse,
1820
)
1921
from starlette.testclient import TestClient
22+
from starlette.types import Message
2023

2124

2225
def test_text_response(test_client_factory):
@@ -244,6 +247,36 @@ async def app(scope, receive, send):
244247
assert filled_by_bg_task == "6, 7, 8, 9"
245248

246249

250+
@pytest.mark.anyio
251+
async def test_file_response_on_head_method(tmpdir: Path):
252+
path = os.path.join(tmpdir, "xyz")
253+
content = b"<file content>" * 1000
254+
with open(path, "wb") as file:
255+
file.write(content)
256+
257+
app = FileResponse(path=path, filename="example.png")
258+
259+
async def receive() -> Message: # type: ignore[empty-body]
260+
... # pragma: no cover
261+
262+
async def send(message: Message) -> None:
263+
if message["type"] == "http.response.start":
264+
assert message["status"] == status.HTTP_200_OK
265+
headers = Headers(raw=message["headers"])
266+
assert headers["content-type"] == "image/png"
267+
assert "content-length" in headers
268+
assert "content-disposition" in headers
269+
assert "last-modified" in headers
270+
assert "etag" in headers
271+
elif message["type"] == "http.response.body":
272+
assert message["body"] == b""
273+
assert message["more_body"] is False
274+
275+
# Since the TestClient drops the response body on HEAD requests, we need to test
276+
# this directly.
277+
await app({"type": "http", "method": "head"}, receive, send)
278+
279+
247280
def test_file_response_with_directory_raises_error(tmpdir, test_client_factory):
248281
app = FileResponse(path=tmpdir, filename="example.png")
249282
client = test_client_factory(app)

0 commit comments

Comments
 (0)