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

  1. Prefix every path with '/': app.router.add_get('/users', h).
  2. Normalize before registering: path = '/' + path.lstrip('/') if path else path.
  3. 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

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)