aio-libs/aiohttp · error · ValueError
path should be started with / or be empty
Error message
path should be started with / or be empty
What it means
add_resource() requires the path argument to be either empty or start with '/'. aiohttp matches paths against request.rel_url.path, which always begins with '/', so a path without a leading slash can never match. Empty string is allowed (used for root-level domain-matched resources). This is the first check in add_resource, before any name validation.
Solutions
- Prefix every path with '/': app.router.add_get('/users', h).
- Normalize before registering: path = '/' + path.lstrip('/') if path else path.
- Lint route tables in config-driven apps to enforce the leading slash.
Example fix
// before
app.router.add_get('users/{id}', handler)
// after
app.router.add_get('/users/{id}', handler) Defensive patterns
Strategy: validation
Validate before calling
def safe_path(path: str) -> str:
if not path:
return path
if not path.startswith('/'):
path = '/' + path.lstrip('/')
return path
app.router.add_get(safe_path(raw_path), h) Type guard
def is_valid_route_path(path: str) -> bool:
return isinstance(path, str) and (path == '' or path.startswith('/')) Try / catch
try:
app.router.add_get(raw_path, h)
except ValueError as e:
if 'should be started with /' in str(e):
app.router.add_get('/' + raw_path.lstrip('/'), h)
else:
raise Prevention
- Always prefix route paths with '/'.
- Normalise joined path fragments with a leading slash before add_route.
- Lint route tables in config-driven apps.
When it happens
Trigger: Calling app.router.add_route('GET', 'users', h) (missing leading slash); app.add_get('api/items', h); constructing a path from joined parts without a leading slash: path = 'api/' + resource.
Common situations: Building paths by joining fragments and forgetting the leading slash; porting from a framework that doesn't require it (Flask supports 'users' implicitly); config-driven route tables with a missing '/' prefix.
Related errors
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/3d973522e53542fd.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_urldispatcher.py:1105
index_key = index_key.partition("{")[0].rpartition("/")[0]
return index_key.rstrip("/") or "/"
def index_resource(self, resource: AbstractResource) -> None:
"""Add a resource to the resource index."""
resource_key = self._get_resource_index_key(resource)
# There may be multiple resources for a canonical path
# so we keep them in a list to ensure that registration
# order is respected.
self._resource_index.setdefault(resource_key, []).append(resource)
def unindex_resource(self, resource: AbstractResource) -> None:
"""Remove a resource from the resource index."""
resource_key = self._get_resource_index_key(resource)
self._resource_index[resource_key].remove(resource)
def add_resource(self, path: str, *, name: str | None = None) -> Resource:
if path and not path.startswith("/"):
raise ValueError("path should be started with / or be empty")
# Reuse last added resource if path and name are the same
if self._resources:
resource = self._resources[-1]
if resource.name == name and resource.raw_match(path):
return cast(Resource, resource)
if not ("{" in path or "}" in path or ROUTE_RE.search(path)):
resource = PlainResource(path, name=name)
self.register_resource(resource)
return resource
resource = DynamicResource(path, name=name)
self.register_resource(resource)
return resource
def add_route(
self,
method: str,
path: str,
handler: Handler | type[AbstractView],View on GitHub (pinned to d041d4d0fd)