{"record":{"id":"6363f04ee8a0674e","repo":"JuliusBrussee/caveman","slug":"asgi-context-must-come-from-authenticated-server-state","errorCode":null,"errorMessage":"ASGI context must come from authenticated server state","messagePattern":"ASGI context must come from authenticated server state","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/asgi.py","lineNumber":124,"sourceCode":"            return await passthrough(receive, \"protected_request\")\n        types = [value.lower().split(b\";\", 1)[0].strip() for key, value in headers if key.lower() == b\"content-type\"]\n        lengths = [value for key, value in headers if key.lower() == b\"content-length\"]\n        if types != [b\"application/json\"] or len(lengths) > 1:\n            return await passthrough(receive, \"unsupported_shape\")\n        if lengths:\n            try:\n                if not 0 <= int(lengths[0]) <= self.max_body_bytes:\n                    return await passthrough(receive, \"payload_limit\")\n            except ValueError:\n                return await passthrough(receive, \"unsupported_shape\")\n\n        context = self.resolve_context(scope)\n        if inspect.isawaitable(context):\n            context = await context\n        if context is None:\n            return await passthrough(receive, \"scope_unavailable\")\n        if not isinstance(context, ASGIContext):\n            raise TypeError(\"ASGI context must come from authenticated server state\")\n\n        buffered, parts, size = deque(), [], 0\n\n        async def replay():\n            return buffered.popleft() if buffered else await receive()\n\n        while True:\n            message = await receive()\n            buffered.append(message)\n            if message.get(\"type\") != \"http.request\" or set(message) - {\"type\", \"body\", \"more_body\"}:\n                return await passthrough(replay, \"request_interrupted\")\n            body = message.get(\"body\", b\"\")\n            if type(body) is not bytes:\n                return await passthrough(replay, \"unsupported_shape\")\n            size += len(body)\n            if size > self.max_body_bytes or len(buffered) > self.max_request_chunks:\n                return await passthrough(replay, \"payload_limit\")\n            parts.append(body)","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/asgi.py#L106-L142","documentation":"The middleware accepts a resolve_context callback but strictly requires that, when it returns a non-None value, that value is an ASGIContext instance built from authenticated server state. Returning any other object (a raw dict, a framework request/user object, a tuple) raises this TypeError during request handling. It enforces the library's security invariant that projected LLM context can never come from caller-controlled data.","triggerScenarios":"At request time, resolve_context(scope) (sync or awaited) returns a non-None value that is not an ASGIContext — e.g. returning scope['user'], a dict like {'scope': ..., 'recovery': ...}, a dataclass with a similar shape, or the result of a builder that wraps ASGIContext.","commonSituations":"Developers implement resolve_context by returning their framework's auth/user object directly; they deserialize a token/JSON body into a context-like dict; they refactor ASGIContext construction into a helper that accidentally returns .__dict__; or they confuse ASGIContext with the caveman_cloud Scope it wraps.","solutions":["Wrap the authenticated data in ASGIContext(scope=<Scope>, recovery=...) before returning it from resolve_context","Return None instead of a substitute object when authentication/context is unavailable — None cleanly declines to passthrough","If returning an awaitable, await-able result, ensure the awaited value (not the coroutine wrapper) is the ASGIContext","Check the isinstance: the check is exact (isinstance against caveman_middleware.asgi.ASGIContext); subclasses are fine, duck-typed lookalikes are not"],"exampleFix":"// before\ndef resolve_context(scope):\n    return {'scope': current_scope(), 'recovery': current_recovery()}  # TypeError\n// after\nfrom caveman_middleware.asgi import ASGIContext\ndef resolve_context(scope):\n    s = current_scope()\n    if s is None:\n        return None\n    return ASGIContext(scope=s, recovery=current_recovery())","handlingStrategy":"type-guard","validationCode":"from caveman_middleware.asgi import ASGIContext\ndef resolve_context(scope):\n    ctx = build_context(scope)\n    if ctx is not None and not isinstance(ctx, ASGIContext):\n        raise TypeError('resolver returned non-ASGIContext')\n    return ctx","typeGuard":"def is_asgi_context(value) -> bool:\n    from caveman_middleware.asgi import ASGIContext\n    return value is None or isinstance(value, ASGIContext)","tryCatchPattern":"try:\n    await middleware(scope, receive, send)\nexcept TypeError as e:\n    if 'authenticated server state' in str(e):\n        logging.error('resolve_context returned wrong type: %s', e)\n    raise","preventionTips":["Always construct the return value with ASGIContext(...); never return raw dicts or framework user objects","Return None to decline instead of a placeholder object","Add a unit test asserting resolve_context returns None or ASGIContext for every auth outcome"],"tags":["python","asgi","type-mismatch","authentication","middleware"],"backgroundTag":"type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}