๐Ÿ“‹ ASGI Specification Essentials

๐Ÿ“– In this section: As a potential web framework contributor, you'll learn the ASGI fundamentals that drive framework design decisions. Understanding these patterns is essential for contributing to FastASGI or any Python async web framework project.

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.

๐Ÿ” Framework Design Insight: The beauty of ASGI is its simplicityโ€”three parameters handle every web interaction. Your framework's complexity should hide this simplicity, not expose it.

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.

๐Ÿ’ก Framework Implementation Key: Your framework must handle these message patterns correctly for each protocol. HTTP requires precise request/response sequencing, Lifespan needs startup/shutdown coordination, and WebSocket demands stateful connection management.

๐Ÿ”„ 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

1. Connection Scope
โ†’
2. http.request
โ†’
3. http.response.start
โ†’
4. http.response.body

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
    })
๐Ÿ’ก Framework Implementation Notes: Notice how much code is needed for a simple "Hello World"! Your framework must handle message types, encode text to bytes, set headers manually, and manage the response lifecycle. This complexity is exactly what your abstractions should hide from application developers.

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)

1. websocket.connect
โ†’
2. websocket.accept
โ†”
3. websocket.receive/send
โ†’
4. websocket.disconnect
๐Ÿ“‹ FastASGI Scope Note: WebSocket support is not currently implemented in FastASGI. The framework focuses on HTTP request/response patterns and application lifecycle management. For WebSocket functionality, consider frameworks like FastAPI, Starlette, or Django Channels.
๐Ÿ’ก Contributor Opportunity: Adding WebSocket support could be an excellent contribution to FastASGI! The protocol follows similar patterns to HTTP but requires connection state management and bidirectional message handling.

๐Ÿ”„ 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:

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": []}
๐ŸŽฏ Framework Design Principle: Your API surface should be inversely proportional to the complexity you handle internally. The more ASGI complexity you abstract away, the cleaner your user-facing APIs become.

๐Ÿ”„ 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)
๐Ÿ—๏ธ Architecture Insight: Middleware composition is where your framework's API design really matters. How will developers add middleware? Decorators? Method calls? Configuration? The pattern you choose affects the entire developer experience.

๐Ÿ”„ 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:

๐ŸŽฏ Contribution Opportunities: Understanding these ASGI patterns helps you identify where FastASGI can be improvedโ€”routing performance, middleware composition, request parsing, error handling, or testing utilities.
๐ŸŽฏ Next Up: Now that you understand ASGI fundamentals and framework design challenges, you'll dive into FastASGI's specific architecture and see how these concepts are implemented in practice.

๐ŸŽฏ Key Takeaways for Contributors

As a potential framework contributor, you now understand:

๐ŸŽฏ Next Up: Now that you understand ASGI fundamentals and framework design challenges, you'll dive into FastASGI's specific architecture and see exactly how these concepts are implemented, where you can contribute, and how to extend the framework.