ASGI Bridge

How Pounce translates protocol events to the ASGI interface

2 min read 481 words

Overview

The ASGI bridge is the layer between Pounce's protocol parsers and your ASGI application. It constructs thescope dict, creates receive and sendcallables, and manages the per-request lifecycle.

HTTP Bridge

For HTTP requests, the bridge:

  1. Builds scope — Extracts method, path, headers, query string from the parsed request
  2. Validates proxy headers — AppliesX-Forwarded-*from trusted peers, strips from untrusted
  3. Generates request ID — UUID4 hex or honoured from trusted proxy'sX-Request-ID
  4. Creates receive — Returns request body chunks ashttp.requestevents
  5. Creates send — Acceptshttp.response.start and http.response.body events, sanitises response headers (CRLF stripping), injects X-Request-ID
  6. Tracks state — Monitors response status, headers sent, body complete
# Simplified flow
scope = build_scope(request, config)       # + proxy header validation
request_id = extract_or_generate(headers)  # UUID4 or from trusted proxy
receive = create_receive(request_body)
send = create_send(connection, config, request_id=request_id)

await app(scope, receive, send)

Scope Construction

The ASGI scope follows the ASGI HTTP Connection Scope specification:

{
    "type": "http",
    "asgi": {"version": "3.0", "spec_version": "2.4"},
    "http_version": "1.1",  # or "2"
    "method": "GET",
    "path": "/",
    "root_path": "",
    "scheme": "https",
    "query_string": b"",
    "headers": [(b"host", b"example.com"), ...],
    "server": ("127.0.0.1", 8000),
    "client": ("192.168.1.1", 54321),
}

Streaming Send

Thesendcallable writes response chunks directly to the socket:

# Your ASGI app sends:
await send({"type": "http.response.start", "status": 200, "headers": [...]})
await send({"type": "http.response.body", "body": b"chunk1", "more_body": True})
await send({"type": "http.response.body", "body": b"chunk2", "more_body": True})
await send({"type": "http.response.body", "body": b"", "more_body": False})

# Pounce writes each chunk to the socket immediately — no buffering

Disconnect Detection

Pounce monitors the client connection concurrently. If the client disconnects mid-request, your app receives ahttp.disconnect event from receive():

event = await receive()
if event["type"] == "http.disconnect":
    # Client disconnected — clean up and return
    return

Lifespan Bridge

The lifespan bridge handles application startup and shutdown:

sequenceDiagram participant P as Pounce participant A as ASGI App P->>A: lifespan.startup alt success A->>P: lifespan.startup.complete Note over P,A: Server runs — handling requests P->>A: lifespan.shutdown A->>P: lifespan.shutdown.complete else failure A->>P: lifespan.startup.failed Note over P: Server exits (non-zero) end
  1. Sendlifespan.startupevent
  2. Wait forlifespan.startup.complete or lifespan.startup.failed
  3. (server runs)
  4. Sendlifespan.shutdownevent
  5. Wait forlifespan.shutdown.complete

Security in the Bridge

Thesend callable applies several security measures during http.response.start:

  • CRLF sanitisation — Strips\r and \nfrom all response header names and values
  • Bodyless guard — Disables compression for 204/304 responses (no body allowed per RFC 9110)
  • HEAD guard — Disables compression for HEAD responses to preserveContent-Length
  • Request ID injection — AppendsX-Request-IDresponse header

These protections are active on both HTTP/1.1 and HTTP/2 bridges.

See Also