{"id":"f29dad22d642c240","repo":"aio-libs/aiohttp","slug":"directory-is-not-a-directory","errorCode":null,"errorMessage":"'{directory}' is not a directory","messagePattern":"'(.+?)' is not a directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"aiohttp/web_urldispatcher.py","lineNumber":521,"sourceCode":"    def __init__(\n        self,\n        prefix: str,\n        directory: PathLike,\n        *,\n        name: str | None = None,\n        expect_handler: _ExpectHandler | None = None,\n        chunk_size: int = DEFAULT_CHUNK_SIZE,\n        show_index: bool = False,\n        break_symlink_sandbox: bool = False,\n        append_version: bool = False,\n    ) -> None:\n        super().__init__(prefix, name=name)\n        try:\n            directory = Path(directory).expanduser().resolve(strict=True)\n        except FileNotFoundError as error:\n            raise ValueError(f\"'{directory}' does not exist\") from error\n        if not directory.is_dir():\n            raise ValueError(f\"'{directory}' is not a directory\")\n        self._directory = directory\n        self._show_index = show_index\n        self._chunk_size = chunk_size\n        self._break_symlink_sandbox = break_symlink_sandbox\n        self._expect_handler = expect_handler\n        self._append_version = append_version\n\n        self._routes = {\n            \"GET\": ResourceRoute(\n                \"GET\", self._handle, self, expect_handler=expect_handler\n            ),\n            \"HEAD\": ResourceRoute(\n                \"HEAD\", self._handle, self, expect_handler=expect_handler\n            ),\n        }\n        self._allowed_methods = set(self._routes)\n\n    def url_for(  # type: ignore[override]","sourceCodeStart":503,"sourceCodeEnd":539,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_urldispatcher.py#L503-L539","documentation":"Raised as ValueError by StaticResource.__init__ when the resolved path exists but is not a directory (e.g. it points to a regular file). StaticResource can only serve from a directory, so passing a file path is rejected after the existence check passes.","triggerScenarios":"Calling app.router.add_static('/static', '/path/to/index.html') — pointing at a file rather than its containing directory. Also pointing at a device node or socket.","commonSituations":"Confusing the directory path with a file path; copy-paste of a file path from config; intending to serve a single file but using add_static instead of a route returning FileResponse.","solutions":["Pass the containing directory: app.router.add_static('/static', Path(file).parent).","To serve a single file, register a route that returns web.FileResponse(path) instead.","Double-check the path with Path(p).is_dir() before add_static."],"exampleFix":"# before\napp.router.add_static('/static', '/srv/app/index.html')\n\n# after\napp.router.add_static('/static', '/srv/app')","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef ensure_is_dir(directory):\n    p = Path(directory).expanduser().resolve()\n    if not p.is_dir():\n        raise ValueError(f\"not a directory: {p}\")\n    return p","typeGuard":"from pathlib import Path\n\ndef is_directory_path(directory) -> bool:\n    return Path(directory).expanduser().resolve().is_dir()","tryCatchPattern":"try:\n    app.router.add_static('/static', directory)\nexcept ValueError as e:\n    if 'not a directory' in str(e):\n        directory = Path(directory).parent  # fall back to parent dir\n    app.router.add_static('/static', directory)","preventionTips":["Pass directories, not files, to add_static.","Use FileResponse in a route to serve a single file.","Validate Path(p).is_dir() in config loaders."],"tags":["aiohttp","static-files","filesystem","value-error","configuration"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}