Skip to content

TenantMiddleware does not say whether the tenant came from the user or the header, so a cacheable anonymous read cannot set Vary correctly #367

Description

@antosubash

Summary

TenantMiddleware resolves the tenant from the signed-in user's tenant_id first and falls back to the configured header, then stores only the result in request.state.tenant_id. A module serving a cacheable read (a public API with ETag and Cache-Control: public) has to declare that the response varies on the tenant header. But when the tenant came from the user's account, the response depends on the session cookie, not the header, and Vary: <header> is wrong: a shared cache keyed on the header would serve a signed-in user's tenant to an anonymous caller with no header. The module cannot tell the two cases apart from request.state.

Observed

simple_module_hosting/middleware.py:185-196 (0.0.26):

user = getattr(request.state, "user", None)
if user is not None:
    tenant_id = getattr(user, "tenant_id", None)

if tenant_id is None and self.header:
    header_value = Headers(scope=scope).get(self.header)
    if header_value:
        tenant_id = header_value

request.state.tenant_id = tenant_id

Nothing records which branch produced tenant_id.

Expected

The request state says how the tenant was resolved, so a module can emit Vary: <header> for header-resolved tenants and Cache-Control: private for user-resolved ones.

Why a module cannot work around it

The records module currently re-derives it: it treats any request with request.state.user set as user-resolved and marks the response private, and everything else as header-resolved. That duplicates the middleware's rule and breaks silently if the rule changes (for example if #358 is fixed by consulting the header only for anonymous requests, which is the same outcome, or by a setting, which is not).

Proposed API

Either request.state.tenant_source: Literal["user", "header"] | None, or have TenantMiddleware append Vary: <header> itself whenever it consulted the header, so modules never have to know the header name.

Context

Found while making the records module (antosubash/smpy_modules#37) multi-tenant; design doc docs/plans/2026-09-23-records-multitenancy.md there, gap L17.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions