From c0ffe93c38e498753ae9cf50e377896cfcd8a8cf Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 19 Jul 2025 19:30:06 +0500 Subject: [PATCH 1/2] feat: in static route disable methods other than GET & POST. Do not open/stat file for HEAD requests. --- falcon/routing/static.py | 64 ++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/falcon/routing/static.py b/falcon/routing/static.py index bc7096e96..f7623704f 100644 --- a/falcon/routing/static.py +++ b/falcon/routing/static.py @@ -231,6 +231,9 @@ def __call__(self, req: Request, resp: Response, **kw: Any) -> None: resp.set_header('Content-Length', '0') return + if req.method not in ('GET', 'HEAD'): + raise falcon.HTTPMethodNotAllowed(('GET', 'HEAD')) + without_prefix = req.path[len(self._prefix) :] # NOTE(kgriffs): Check surrounding whitespace and strip trailing @@ -259,38 +262,41 @@ def __call__(self, req: Request, resp: Response, **kw: Any) -> None: if '..' in file_path or not file_path.startswith(self._directory): raise falcon.HTTPNotFound() - if self._fallback_filename is None: - fh, st = _open_file(file_path) - else: - try: + if req.method == 'GET': + # for HEAD requests, we won't do operations like open/stat + if self._fallback_filename is None: fh, st = _open_file(file_path) - except falcon.HTTPNotFound: - fh, st = _open_file(self._fallback_filename) - file_path = self._fallback_filename - - etag = f'{int(st.st_mtime):x}-{st.st_size:x}' - resp.etag = etag - - last_modified = datetime.fromtimestamp(st.st_mtime, timezone.utc) - # NOTE(vytas): Strip the microsecond part because that is not reflected - # in HTTP date, and when the client passes a previous value via - # If-Modified-Since, it will look as if our copy is ostensibly newer. - last_modified = last_modified.replace(microsecond=0) - resp.last_modified = last_modified - - if _is_not_modified(req, etag, last_modified): - fh.close() - resp.status = falcon.HTTP_304 - return + else: + try: + fh, st = _open_file(file_path) + except falcon.HTTPNotFound: + fh, st = _open_file(self._fallback_filename) + file_path = self._fallback_filename + + etag = f'{int(st.st_mtime):x}-{st.st_size:x}' + resp.etag = etag + + last_modified = datetime.fromtimestamp(st.st_mtime, timezone.utc) + # NOTE(vytas): Strip the microsecond part because that is not reflected + # in HTTP date, and when the client passes a previous value via + # If-Modified-Since, it will look as if our copy is ostensibly newer. + last_modified = last_modified.replace(microsecond=0) + resp.last_modified = last_modified + + if _is_not_modified(req, etag, last_modified): + fh.close() + resp.status = falcon.HTTP_304 + return + + req_range = req.range if req.range_unit == 'bytes' else None + try: + stream, length, content_range = _set_range(fh, st, req_range) + except IOError: + fh.close() + raise falcon.HTTPNotFound() - req_range = req.range if req.range_unit == 'bytes' else None - try: - stream, length, content_range = _set_range(fh, st, req_range) - except IOError: - fh.close() - raise falcon.HTTPNotFound() + resp.set_stream(stream, length) - resp.set_stream(stream, length) suffix = os.path.splitext(file_path)[1] resp.content_type = resp.options.static_media_types.get( suffix, 'application/octet-stream' From 4b3551cbc53dcf2b168caac83f8041ffc5bf5f4a Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sat, 27 Dec 2025 20:04:30 +0500 Subject: [PATCH 2/2] feat: adjust _open_file function to do only stat for HEAD requests --- falcon/routing/static.py | 78 ++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/falcon/routing/static.py b/falcon/routing/static.py index f86f28099..58cef827a 100644 --- a/falcon/routing/static.py +++ b/falcon/routing/static.py @@ -20,8 +20,10 @@ from falcon import Response -def _open_file(file_path: str | Path) -> tuple[io.BufferedReader, os.stat_result]: - """Open a file for a static file request and read file stat. +def _open_and_stat_file( + file_path: str | Path, only_stat: bool = False +) -> tuple[io.BufferedReader, os.stat_result]: + """Open(optionally) a file for a static file request and read file stat. Args: file_path (Union[str, Path]): Path to the file to open. @@ -29,13 +31,17 @@ def _open_file(file_path: str | Path) -> tuple[io.BufferedReader, os.stat_result tuple: Tuple of (BufferedReader, stat_result). """ fh: io.BufferedReader | None = None - try: - fh = io.open(file_path, 'rb') # noqa: UP020 - st = os.fstat(fh.fileno()) - except OSError: - if fh is not None: - fh.close() - raise falcon.HTTPNotFound() + + if only_stat: + st, fh = os.stat(file_path), io.BufferedReader(io.BytesIO()) + else: + try: + fh = io.open(file_path, 'rb') # noqa: UP020 + st = os.fstat(fh.fileno()) + except OSError: + if fh is not None: + fh.close() + raise falcon.HTTPNotFound() return fh, st @@ -263,38 +269,31 @@ def __call__(self, req: Request, resp: Response, **kw: Any) -> None: if '..' in file_path or not file_path.startswith(self._directory): raise falcon.HTTPNotFound() - if req.method == 'GET': - # for HEAD requests, we won't do operations like open/stat - if self._fallback_filename is None: - fh, st = _open_file(file_path) - else: - try: - fh, st = _open_file(file_path) - except falcon.HTTPNotFound: - fh, st = _open_file(self._fallback_filename) - file_path = self._fallback_filename - - etag = f'{int(st.st_mtime):x}-{st.st_size:x}' - resp.etag = etag - - last_modified = datetime.fromtimestamp(st.st_mtime, timezone.utc) - # NOTE(vytas): Strip the microsecond part because that is not reflected - # in HTTP date, and when the client passes a previous value via - # If-Modified-Since, it will look as if our copy is ostensibly newer. - last_modified = last_modified.replace(microsecond=0) - resp.last_modified = last_modified - - if _is_not_modified(req, etag, last_modified): - fh.close() - resp.status = falcon.HTTP_304 - return + only_stat = req.method == 'HEAD' - req_range = req.range if req.range_unit == 'bytes' else None + if self._fallback_filename is None: + fh, st = _open_and_stat_file(file_path, only_stat) + else: try: - stream, length, content_range = _set_range(fh, st, req_range) - except IOError: - fh.close() - raise falcon.HTTPNotFound() + fh, st = _open_and_stat_file(file_path, only_stat) + except falcon.HTTPNotFound: + fh, st = _open_and_stat_file(self._fallback_filename, only_stat) + file_path = self._fallback_filename + + etag = f'{int(st.st_mtime):x}-{st.st_size:x}' + resp.etag = etag + + last_modified = datetime.fromtimestamp(st.st_mtime, timezone.utc) + # NOTE(vytas): Strip the microsecond part because that is not reflected + # in HTTP date, and when the client passes a previous value via + # If-Modified-Since, it will look as if our copy is ostensibly newer. + last_modified = last_modified.replace(microsecond=0) + resp.last_modified = last_modified + + if _is_not_modified(req, etag, last_modified): + fh.close() + resp.status = falcon.HTTP_304 + return req_range = req.range if req.range_unit == 'bytes' else None try: @@ -303,6 +302,7 @@ def __call__(self, req: Request, resp: Response, **kw: Any) -> None: fh.close() raise falcon.HTTPNotFound() + resp.set_stream(stream, length) suffix = os.path.splitext(file_path)[1] resp.content_type = resp.options.static_media_types.get( suffix, 'application/octet-stream'