1010import warnings
1111from collections .abc import AsyncIterable , Awaitable , Iterable , Mapping , Sequence
1212from datetime import datetime
13- from email .utils import format_datetime , formatdate
13+ from email .utils import format_datetime , formatdate , parsedate
1414from functools import partial
1515from mimetypes import guess_type
1616from secrets import token_hex
@@ -297,6 +297,15 @@ def __init__(self, max_size: int) -> None:
297297class FileResponse (Response ):
298298 chunk_size = 64 * 1024
299299
300+ NOT_MODIFIED_HEADERS = {
301+ b"cache-control" ,
302+ b"content-location" ,
303+ b"date" ,
304+ b"etag" ,
305+ b"expires" ,
306+ b"vary" ,
307+ }
308+
300309 def __init__ (
301310 self ,
302311 path : str | os .PathLike [str ],
@@ -362,12 +371,14 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
362371 stat_result = self .stat_result
363372
364373 headers = Headers (scope = scope )
374+ http_if_none_match = headers .get ("if-none-match" )
375+ http_if_modified_since = headers .get ("if-modified-since" )
365376 http_range = headers .get ("range" )
366377 http_if_range = headers .get ("if-range" )
367378
368- if http_range is None or ( http_if_range is not None and not self ._should_use_range ( http_if_range ) ):
369- await self ._handle_simple (send , send_header_only , send_pathsend )
370- else :
379+ if self . status_code == 200 and self ._is_not_modified ( http_if_none_match , http_if_modified_since ):
380+ await self ._handle_not_modified (send )
381+ elif self . status_code == 200 and http_range is not None and self . _should_use_range ( http_if_range ) :
371382 try :
372383 ranges = self ._parse_range_header (http_range , stat_result .st_size )
373384 except MalformedRangeHeader as exc :
@@ -381,6 +392,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
381392 await self ._handle_single_range (send , start , end , stat_result .st_size , send_header_only )
382393 else :
383394 await self ._handle_multiple_ranges (send , ranges , stat_result .st_size , send_header_only )
395+ else :
396+ await self ._handle_simple (send , send_header_only , send_pathsend )
384397
385398 if self .background is not None :
386399 await self .background ()
@@ -399,6 +412,11 @@ async def _handle_simple(self, send: Send, send_header_only: bool, send_pathsend
399412 more_body = len (chunk ) == self .chunk_size
400413 await send ({"type" : "http.response.body" , "body" : chunk , "more_body" : more_body })
401414
415+ async def _handle_not_modified (self , send : Send ) -> None :
416+ headers = [(k , v ) for k , v in self .raw_headers if k in FileResponse .NOT_MODIFIED_HEADERS ]
417+ await send ({"type" : "http.response.start" , "status" : 304 , "headers" : headers })
418+ await send ({"type" : "http.response.body" , "body" : b"" , "more_body" : False })
419+
402420 async def _handle_single_range (
403421 self , send : Send , start : int , end : int , file_size : int , send_header_only : bool
404422 ) -> None :
@@ -452,8 +470,36 @@ async def _handle_multiple_ranges(
452470 }
453471 )
454472
455- def _should_use_range (self , http_if_range : str ) -> bool :
456- return http_if_range == self .headers ["last-modified" ] or http_if_range == self .headers ["etag" ]
473+ def _is_not_modified (self , http_if_none_match : str | None , http_if_modified_since : str | None ) -> bool :
474+ """
475+ Given the request and response headers, return `True` if an HTTP
476+ "Not Modified" response could be returned instead.
477+ """
478+ if http_if_none_match is not None :
479+ try :
480+ match = [tag .strip (" W/" ) for tag in http_if_none_match .split ("," )]
481+ etag = self .headers ["etag" ]
482+ return etag in match # Client already has the version with current tag
483+ except KeyError :
484+ pass
485+
486+ if http_if_modified_since :
487+ try :
488+ since = parsedate (http_if_modified_since )
489+ last_modified = parsedate (self .headers ["last-modified" ])
490+ if since is not None and last_modified is not None :
491+ return since >= last_modified
492+ except KeyError :
493+ pass
494+
495+ return False
496+
497+ def _should_use_range (self , http_if_range : str | None ) -> bool :
498+ return http_if_range in (
499+ None ,
500+ self .headers ["last-modified" ],
501+ self .headers ["etag" ],
502+ )
457503
458504 @staticmethod
459505 def _parse_range_header (http_range : str , file_size : int ) -> list [tuple [int , int ]]:
0 commit comments