aio-libs/aiohttp · error · RuntimeError

Cannot change apps stack after .freeze() call

Error message

Cannot change apps stack after .freeze() call

What it means

Raised as RuntimeError by UrlMappingMatchInfo.add_app when an application is pushed onto the match-info's app stack after freeze() was called. The app stack records the chain of sub-applications traversed during routing; once frozen (after startup), it is immutable, so late additions indicate routing happening after the app lifecycle entered its frozen phase.

Source

Thrown at aiohttp/web_urldispatcher.py:249

    @property
    def expect_handler(self) -> _ExpectHandler:
        return self._route.handle_expect_header

    @property
    def http_exception(self) -> HTTPException | None:
        return None

    def get_info(self) -> _InfoDict:  # type: ignore[override]
        return self._route.get_info()

    @property
    def apps(self) -> tuple["Application", ...]:
        return tuple(self._apps)

    def add_app(self, app: "Application") -> None:
        if self._frozen:
            raise RuntimeError("Cannot change apps stack after .freeze() call")
        if self._current_app is None:
            self._current_app = app
        self._apps.insert(0, app)

    @property
    def current_app(self) -> "Application":
        app = self._current_app
        assert app is not None
        return app

    @current_app.setter
    def current_app(self, app: "Application") -> None:
        if DEBUG:
            if app not in self._apps:
                raise RuntimeError(
                    f"Expected one of the following apps {self._apps!r}, got {app!r}"
                )
        self._current_app = app

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Ensure sub-applications are added via app.add_sub_app(prefix, sub_app) BEFORE app.freeze()/startup, not during request handling.
  2. Do not call match_info.add_app(...) in custom resource resolve() implementations after the app is frozen.
  3. If using a custom AbstractResource, return a fresh UrlMappingMatchInfo rather than reusing a frozen one.
  4. Verify you are not re-freezing or re-starting an already-started Application.

Example fix

# before (adding sub-app after startup)
runner = web.AppRunner(app)
await runner.setup()
app.add_sub_app('/sub', sub_app)  # too late

# after
app.add_sub_app('/sub', sub_app)
runner = web.AppRunner(app)
await runner.setup()
Defensive patterns

Strategy: validation

Validate before calling

# Prevent by structuring setup: never add sub-apps after runner.setup()
def setup_app(app, sub_apps):
    for prefix, sub in sub_apps:
        app.add_sub_app(prefix, sub)  # before any freeze/startup
    return app

Try / catch

try:
    app.add_sub_app(prefix, sub_app)
except RuntimeError as e:
    if 'freeze' in str(e):
        raise RuntimeError("App already frozen; add sub-apps before startup") from e
    raise

Prevention

When it happens

Trigger: Internally triggered when a sub-application resource tries to add_app after the match info is frozen — typically a sign of routing against an Application whose cleanup/startup re-entered resolution, or a custom AbstractResource that resolves after freeze. Not normally raised by user code directly.

Common situations: Custom router/resource subclasses that call add_app outside the normal request routing window; using add_sub_app after app.freeze()/startup; re-using a frozen Application's match info; bugs in middleware that re-resolve routes.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/8f94012d3a59e87e.json. Report an issue: GitHub.