aio-libs/aiohttp · error · RuntimeError

OPTIONS route was set already

Error message

OPTIONS route was set already

What it means

StaticResource.set_options_route registers an OPTIONS handler for the static resource and refuses to do so twice. The first call adds 'OPTIONS' to self._routes; a second call detects the existing key and raises RuntimeError. This prevents silently replacing the OPTIONS handler, which would hide a configuration mistake (e.g. CORS middleware double-registering).

Solutions

  1. Ensure set_options_route is called at most once per StaticResource — coordinate between middleware.
  2. If you use a CORS library, let it own OPTIONS handling and do not also call set_options_route manually.
  3. Recreate the resource/router if you need to reconfigure OPTIONS handling.
Defensive patterns

Strategy: validation

Validate before calling

def safe_set_options(resource, handler):
    if 'OPTIONS' in getattr(resource, '_routes', {}):
        raise RuntimeError('OPTIONS route already set')
    return resource.set_options_route(handler)

Prevention

When it happens

Trigger: Calling set_options_route(handler) twice on the same StaticResource. In practice this is usually invoked automatically by aiohttp's CORS or OPTIONS handling machinery; user code rarely calls it directly. The duplicate call indicates two components both trying to set the OPTIONS route.

Common situations: Two CORS/OPTIONS middleware both attempting to register an OPTIONS route on the same static resource; a plugin and user code both calling set_options_route; re-initialization during hot reload without recreating the resource.

Related errors


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

Appendix: source

Thrown at aiohttp/web_urldispatcher.py:593

        return url

    @staticmethod
    def _get_file_hash(byte_array: bytes) -> str:
        m = hashlib.sha256()  # todo sha256 can be configurable param
        m.update(byte_array)
        b64 = base64.urlsafe_b64encode(m.digest())
        return b64.decode("ascii")

    def get_info(self) -> _InfoDict:
        return {
            "directory": self._directory,
            "prefix": self._prefix,
            "routes": self._routes,
        }

    def set_options_route(self, handler: Handler) -> "ResourceRoute":
        if "OPTIONS" in self._routes:
            raise RuntimeError("OPTIONS route was set already")
        route = ResourceRoute(
            "OPTIONS", handler, self, expect_handler=self._expect_handler
        )
        self._routes["OPTIONS"] = route
        self._allowed_methods.add("OPTIONS")
        return route

    async def resolve(self, request: Request) -> _Resolve:
        path = request.rel_url.path_safe
        method = request.method
        # We normalise here to avoid matches that traverse below the static root.
        # e.g. /static/../../../../home/user/webapp/static/
        norm_path = os.path.normpath(path)
        if IS_WINDOWS:
            norm_path = norm_path.replace("\\", "/")
        if not norm_path.startswith(self._prefix2) and norm_path != self._prefix:
            return None, set()

View on GitHub (pinned to d041d4d0fd)