Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/adr/0002-scope-follows-the-handshake-probe.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Both connection providers bind `web.Request`
passing the chosen provider to `integrations.bind` for the child's scope and context. The probe is
aiohttp's own handshake check rather than a reimplementation that can drift from it: on a fresh
response object it cannot raise, it is synchronous, and it neither consumes the request nor starts
the handshake. dishka's `Connection == "Upgrade"` header comparison was rejected as brittle, missing
the handshake. The probe is skipped when the request has no `Upgrade` header, because aiohttp's
handshake rejects that case first, so the outcome is the same without the probe's cost. dishka's `Connection == "Upgrade"` header comparison was rejected as brittle, missing
the legal `keep-alive, Upgrade` form and casing variants, and dispatching on the route was
unavailable because scope selection runs before routing has produced a handler. The boundary is that
scope follows the handshake headers and nothing else, so a request advertising a valid upgrade to a
Expand Down
7 changes: 3 additions & 4 deletions modern_di_aiohttp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import typing

import aiohttp.abc
from aiohttp import web
from aiohttp import hdrs, web
from modern_di import Container, Scope, integrations, providers


Expand Down Expand Up @@ -64,9 +64,8 @@ async def _di_middleware(
# bind `web.Request`, so `integrations.classify_connection`'s isinstance
# dispatch can't tell them apart — this probe is what picks the provider;
# `integrations.bind` only derives the scope+context once it's picked.
provider = (
aiohttp_websocket_provider if web.WebSocketResponse().can_prepare(request).ok else aiohttp_request_provider
)
is_websocket = hdrs.UPGRADE in request.headers and web.WebSocketResponse().can_prepare(request).ok
provider = aiohttp_websocket_provider if is_websocket else aiohttp_request_provider
match = integrations.bind(provider, request)
async with fetch_di_container(request.app).build_child_container(
scope=match.scope, context=match.context
Expand Down
18 changes: 18 additions & 0 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import collections.abc
import gc
import http
from unittest import mock

import pytest
from aiohttp import web
from aiohttp.test_utils import TestClient
from modern_di import Container, Scope
Expand All @@ -26,6 +28,22 @@ async def endpoint(request: web.Request) -> web.Response:
assert (await client.get("/")).status == http.HTTPStatus.OK


async def test_a_request_without_an_upgrade_header_skips_the_handshake_probe(
aiohttp_client: AiohttpClient, app: web.Application, monkeypatch: pytest.MonkeyPatch
) -> None:
async def endpoint(request: web.Request) -> web.Response:
assert request[_CONTAINER_REQUEST_KEY].scope is Scope.REQUEST
return web.Response(text="ok")

app.router.add_get("/", endpoint)
client = await aiohttp_client(app)
websocket_response = mock.Mock(wraps=web.WebSocketResponse)
monkeypatch.setattr(web, "WebSocketResponse", websocket_response)

assert (await client.get("/")).status == http.HTTPStatus.OK
assert websocket_response.call_count == 0


async def test_middleware_opens_session_scoped_child_for_websocket(
aiohttp_client: AiohttpClient, app: web.Application
) -> None:
Expand Down
Loading