aio-libs/aiohttp · warning · HTTPForbidden

403: Forbidden

Error message

403: Forbidden

What it means

When the resolved path inside StaticResource is a directory and show_index is False (the default), aiohttp raises HTTPForbidden (403) rather than listing the directory contents. This prevents unintentional directory listing, which can leak sensitive filenames. The same 403 is also raised if a PermissionError occurs while checking is_dir() (e.g. the segment is not readable).

Source

Thrown at aiohttp/web_urldispatcher.py:665

            else:
                file_path = unresolved_path.resolve()
                file_path.relative_to(self._directory)
        except (ValueError, *CIRCULAR_SYMLINK_ERROR) as error:
            # ValueError is raised for the relative check. Circular symlinks
            # raise here on resolving for python < 3.13.
            raise HTTPNotFound() from error

        # if path is a directory, return the contents if permitted. Note the
        # directory check will raise if a segment is not readable.
        try:
            if file_path.is_dir():
                if self._show_index:
                    return Response(
                        text=self._directory_as_html(file_path),
                        content_type="text/html",
                    )
                else:
                    raise HTTPForbidden()
        except PermissionError as error:
            raise HTTPForbidden() from error

        # Return the file response, which handles all other checks.
        return FileResponse(file_path, chunk_size=self._chunk_size)

    def _directory_as_html(self, dir_path: Path) -> str:
        """returns directory's index as html."""
        assert dir_path.is_dir()

        relative_path_to_dir = dir_path.relative_to(self._directory).as_posix()
        index_of = f"Index of /{html_escape(relative_path_to_dir)}"
        h1 = f"<h1>{index_of}</h1>"

        index_list = []
        dir_index = dir_path.iterdir()
        for _file in sorted(dir_index):
            # show file url as relative to static path

View on GitHub (pinned to d041d4d0fd)

Solutions

  1. If you want directory listings, pass show_index=True to add_static.
  2. Otherwise, this 403 is expected — ensure clients request specific files, not directories.
  3. Fix filesystem permissions if PermissionError is the cause.

Example fix

// before
app.router.add_static('/static', '/app/static')  # show_index defaults to False
// after (enable listings)
app.router.add_static('/static', '/app/static', show_index=True)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def require_file_or_listing(path: Path, show_index: bool):
    if path.is_dir() and not show_index:
        raise PermissionError('Directory listing disabled')

Prevention

When it happens

Trigger: A GET request to a static URL that maps to a directory (e.g. GET /static/subdir/ where subdir is a folder) when add_static was configured with show_index=False (default). Also when the OS denies read permission on a path segment, causing PermissionError during the is_dir() check.

Common situations: Default static configuration that does not enable directory listings; a request for a folder URL without a trailing index file; permission misconfiguration on the served directory; security probing of folder URLs.

Understand the failure class

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/9b70ad79a4cd9292. Report an issue: GitHub.