๐ ASGI Specification Essentials
The ASGI Pattern
As a framework developer, understanding this signature is crucial. Every abstraction you buildโrouters, middleware, request objectsโultimately translates back to this pattern:
async def application(scope, receive, send):
"""
The ASGI contract that your framework must satisfy.
Args:
scope: Connection metadata your framework needs to parse
receive: Stream of messages your framework must handle
send: Output channel your framework controls
"""
pass
Your framework's job is to provide elegant abstractions over this interface. Think about how you'll transform raw ASGI calls into intuitive APIs for application developers.
ASGI Message Types Overview
Before diving into implementation details, you need to understand the message types that flow through ASGI applications. Every ASGI interaction is built from these fundamental message exchanges between the server and your application.
Important: These messages are what your application will receive when
calling the receive function that's passed to your ASGI callable. Each
message is a Python dictionary containing a type field that identifies
the message type, plus additional fields specific to that message type.
๏ฟฝ๐ค HTTP Messages
http.request messages are sent by the server to your application when a client makes an HTTP request. Each message contains a chunk of the request body (which might be empty for GET requests). The more_body field tells you whether more chunks are coming - this is crucial for handling large uploads or streaming data.
http.response.start and http.response.body messages are sent by your application back to the server. You must send exactly one response.start message containing the HTTP status code and headers, followed by one or more response.body messages containing the actual response content. The final body message must have more_body set to False to signal the end of the response.
๐ Lifespan Messages
lifespan.startup messages are sent when the server is starting up, before it begins accepting HTTP requests. This is your opportunity to initialize databases, load configuration, start background tasks, or set up any resources your application needs. You must respond with either lifespan.startup.complete (success) or lifespan.startup.failed (with an error message).
lifespan.shutdown messages are sent when the server is gracefully shutting down. This is when you should close database connections, stop background tasks, clean up temporary files, and release any resources. Respond with lifespan.shutdown.complete when cleanup is finished, or lifespan.shutdown.failed if something goes wrong during cleanup.
๐ WebSocket Messages (Reference)
websocket.connect messages indicate a client wants to establish a WebSocket connection. Your application can respond with websocket.accept to allow the connection or websocket.close to reject it. Once accepted, the connection enters a bidirectional communication phase.
websocket.receive and websocket.send messages handle the ongoing communication. Receive messages contain data sent by the client (either text or binary), while send messages let you transmit data back to the client. The connection continues until either side sends a disconnect message or encounters an error.
๐ Implementation Details: Now that you understand what messages flow through ASGI, let's examine the three core parameters that make this communication possible.
Understanding Scope, Receive, Send
The Scope Dictionary
As a framework contributor, you need to understand scope structure intimately. This is the raw data your framework must parse to create request objects:
# HTTP request scope - framework parsing reference
scope = {
'type': 'http', # Protocol type for routing logic
'asgi': {
'version': '3.0', # ASGI version compatibility
'spec_version': '2.4' # HTTP spec version
},
'http_version': '1.1', # HTTP version for response format
'method': 'GET', # HTTP method for handler routing
'scheme': 'http', # URL scheme for redirect logic
'path': '/users/123', # Request path for URL routing
'query_string': b'include=profile', # Query parameters (decode to dict)
'root_path': '', # Application mount point
'headers': [ # HTTP headers (convert to case-insensitive dict)
(b'host', b'localhost:8000'),
(b'user-agent', b'Mozilla/5.0'),
(b'accept', b'application/json'),
],
'server': ('127.0.0.1', 8000), # Server address
'client': ('127.0.0.1', 54321), # Client address
}
| Key | Type | Description | Required |
|---|---|---|---|
type |
str | Protocol type: "http", "websocket", or "lifespan" | โ |
method |
str | HTTP method (GET, POST, etc.) | HTTP only |
path |
str | URL path, decoded from percent-encoding | HTTP/WS |
query_string |
bytes | Query string portion, percent-encoded | HTTP/WS |
headers |
List[Tuple[bytes, bytes]] | HTTP headers as name-value pairs | HTTP/WS |
The Receive Callable
Receive is an async function that yields messages from the client. Each protocol has different message types:
async def example_receive_usage(receive): """Example of using the receive callable.""" message = await receive() # HTTP request message if message['type'] == 'http.request': body_chunk = message['body'] # bytes more_coming = message['more_body'] # bool # WebSocket message elif message['type'] == 'websocket.receive': text_data = message.get('text') # str or None byte_data = message.get('bytes') # bytes or None # Lifespan events elif message['type'] == 'lifespan.startup': # Server is starting up pass elif message['type'] == 'lifespan.shutdown': # Server is shutting down pass
The Send Callable
Send is an async function for sending messages to the client. You must send the right message types for each protocol:
async def example_send_usage(send): """Example of using the send callable.""" # HTTP response (requires two messages) await send({ 'type': 'http.response.start', 'status': 200, 'headers': [(b'content-type', b'application/json')], }) await send({ 'type': 'http.response.body', 'body': b'{"message": "Hello, World!"}', 'more_body': False, # False = end of response }) # WebSocket messages await send({ 'type': 'websocket.accept', }) await send({ 'type': 'websocket.send', 'text': 'Hello from server!', }) # Lifespan responses await send({ 'type': 'lifespan.startup.complete', })
๐ From Theory to Practice: Now that you understand the ASGI interface structure, let's explore how each protocol type works in practice. We'll start with HTTPโthe foundation of web frameworks.
HTTP Protocol Essentials
HTTP is the most common ASGI protocol. Here's how the basic request/response flow works:
HTTP Request Lifecycle
Simple HTTP Example
Here's a complete "Hello World" HTTP handler to show the pattern:
async def simple_http_app(scope, receive, send): """ Complete HTTP handler demonstrating ASGI fundamentals. This example shows the minimal HTTP implementation pattern that your framework will abstract away for application developers. """ assert scope['type'] == 'http' # Framework responsibility: Read and parse request body message = await receive() assert message['type'] == 'http.request' # Framework responsibility: Extract routing information from scope method = scope['method'] path = scope['path'] # Framework responsibility: Construct and send response headers await send({ 'type': 'http.response.start', 'status': 200, 'headers': [(b'content-type', b'text/plain')], }) # Framework responsibility: Format and send response body response_text = f"Hello! You sent {method} {path}" await send({ 'type': 'http.response.body', 'body': response_text.encode(), 'more_body': False, # End of response })
Lifespan Protocol Essentials
As a framework developer, lifespan protocol is where you'll implement application lifecycle hooks. This is crucial for managing shared resources:
async def lifespan_handler(scope, receive, send): """ Basic lifespan pattern for framework contributors. This demonstrates the essential protocol flow - your framework will build more sophisticated resource management on top of this. """ assert scope['type'] == 'lifespan' while True: message = await receive() if message['type'] == 'lifespan.startup': # Initialize your app (database, cache, etc.) print("App starting up...") await send({'type': 'lifespan.startup.complete'}) elif message['type'] == 'lifespan.shutdown': # Cleanup resources print("App shutting down...") await send({'type': 'lifespan.shutdown.complete'}) return # Exit handler
๐ Protocol Transition: Now that we've covered HTTP and Lifespan protocols, let's briefly touch on WebSocket protocol for completeness.
WebSocket Protocol Essentials
WebSockets enable bidirectional real-time communication through a different ASGI protocol. While understanding WebSocket protocol is valuable for framework contributors, FastASGI currently focuses on HTTP and Lifespan protocols.
WebSocket Lifecycle (Reference)
๐ Moving to Framework Design: Now that you understand the core ASGI protocols, let's explore the fundamental challenges frameworks solve and why they exist.
Framework Design Challenges
As a framework contributor, you need to understand the complexity that raw ASGI presents to application developers. Consider the challenges your framework must solve:
- Routing: How will you map URLs to handler functions efficiently?
- Request abstraction: How do you parse JSON, forms, file uploads elegantly?
- Response helpers: What APIs make templates, redirects, errors intuitive?
- Middleware design: How do you compose cross-cutting concerns?
- Error handling: How do you provide useful debugging information?
- Testing utilities: What tools help developers test their applications?
Design Challenge: Raw ASGI vs Your Framework
What Developers Face (Raw ASGI)
async def app(scope, receive, send):
if scope['type'] != 'http':
return
if scope['path'] == '/users':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [(b'content-type', b'application/json')]
})
await send({
'type': 'http.response.body',
'body': b'{"users": []}',
'more_body': False
})
else:
# Handle 404...
What Your Framework Should Enable
@app.get('/users')
async def get_users():
return {"users": []}
๐ Architecture Deep Dive: Beyond basic request handling, frameworks need robust patterns for cross-cutting concerns. Let's explore middlewareโthe backbone of extensible framework architecture.
Middleware Architecture Patterns
Understanding middleware is crucial for framework architecture. The "onion" pattern is how you'll implement cross-cutting concerns in your framework:
class FrameworkMiddleware: """ Framework middleware pattern for cross-cutting concerns. This template shows how to implement the "onion" middleware pattern that wraps around your application core, enabling composable features like authentication, CORS, logging, and error handling. """ def __init__(self, app): self.app = app # Next layer in the middleware stack async def __call__(self, scope, receive, send): # Framework responsibility: Pre-process requests (auth, validation, etc.) print(f"Processing request to {scope['path']}") # Framework responsibility: Chain to next middleware or application await self.app(scope, receive, send) # Post-processing: logging, cleanup (limited in ASGI) print("Request completed") # Framework responsibility: Make middleware composition intuitive app = FrameworkMiddleware(your_application)
๐ FastASGI Focus: With ASGI fundamentals and framework patterns understood, let's explore how FastASGI specifically approaches these challenges and where you can contribute.
FastASGI's Contributor Philosophy
As a potential FastASGI contributor, understanding our architectural philosophy will guide your contributions. FastASGI takes a unique approach to framework design:
- Educational transparency: Code should teach ASGI concepts, not hide them
- Modular architecture: Contributors can work on isolated components
- Clear abstractions: Each layer has a well-defined responsibility
- Debugging-first design: Error messages guide developers to solutions
- Performance awareness: Optimizations that don't sacrifice clarity
๐ฏ Key Takeaways for Contributors
As a potential framework contributor, you now understand:
- ASGI Contract: async def app(scope, receive, send) - the interface your framework must implement perfectly
- Message Flow Patterns: How HTTP, Lifespan, and WebSocket messages flow between server and application, and when each message type is used
- Scope Parsing: How to extract and validate connection metadata for your request objects
- Protocol Handling: The essential patterns for HTTP and Lifespan protocols that FastASGI implements (WebSocket awareness for future contributions)
- Abstraction Challenges: The complexity your framework must hide to provide clean developer APIs
- Middleware Architecture: How to design composable, performance-aware middleware systems