# Web Framework Integration (ASGI/WSGI)

This guide covers the optimized ASGI and WSGI modules for integrating Python web frameworks with erlang_python.

## Overview

The `py_asgi` and `py_wsgi` modules provide high-performance request handling by using optimized C-level marshalling between Erlang and Python. These modules bypass the generic `py:call()` path with specialized NIFs that:

- **Pre-intern keys** - Python string keys are interned once at startup, eliminating per-request string allocation and hashing overhead
- **Cache constants** - Common values like HTTP methods, versions, and schemes are reused across requests
- **Pool responses** - Thread-local response pooling reduces memory allocation during request processing
- **Direct NIF path** - Specialized NIF functions avoid the overhead of generic Python call marshalling

### Performance

Compared to generic `py:call()`-based handling:

| Optimization | ASGI | WSGI |
|--------------|------|------|
| Interned keys | +15-20% | +15-20% |
| Response pooling | +20-25% | N/A |
| Direct NIF | +25-30% | +25-30% |
| **Total** | ~60-80% | ~60-80% |

#### ASGI NIF Optimizations

The ASGI module includes six additional NIF-level optimizations:

| Optimization | Improvement | Description |
|--------------|-------------|-------------|
| Direct Response Extraction | 5-10% | Extract `(status, headers, body)` directly to Erlang terms |
| Pre-Interned Headers | 3-5% | 16 common HTTP headers cached as PyBytes |
| Cached Status Codes | 1-2% | 14 common status codes cached as PyLong |
| Zero-Copy Body | 10-15% | Large bodies (≥1KB) use buffer protocol |
| Scope Template Caching | 15-20% | Thread-local cache of 64 scope templates |
| Lazy Header Conversion | 5-10% | Headers converted on-demand (≥4 headers) |

**Total expected improvement: 40-60%** for typical ASGI workloads on top of the base optimizations.

These optimizations are automatic and require no code changes.

## ASGI Support

### Basic Usage

```erlang
%% Build ASGI scope from your HTTP server (e.g., Cowboy)
Scope = #{
    type => <<"http">>,
    http_version => <<"1.1">>,
    method => <<"GET">>,
    scheme => <<"http">>,
    path => <<"/api/users">>,
    query_string => <<"id=123">>,
    headers => [[<<"host">>, <<"localhost:8080">>]],
    server => {<<"localhost">>, 8080},
    client => {<<"127.0.0.1">>, 54321}
},

%% Execute ASGI application
case py_asgi:run(<<"myapp">>, <<"application">>, Scope, Body) of
    {ok, {Status, Headers, ResponseBody}} ->
        %% Send response
        cowboy_req:reply(Status, Headers, ResponseBody, Req);
    {error, Reason} ->
        %% Handle error
        cowboy_req:reply(500, #{}, <<"Internal Server Error">>, Req)
end.
```

### API Reference

#### `py_asgi:run/4`

```erlang
-spec run(Module, Callable, Scope, Body) -> Result when
    Module :: binary(),
    Callable :: binary(),
    Scope :: scope(),
    Body :: binary(),
    Result :: {ok, {integer(), [{binary(), binary()}], binary()}} | {error, term()}.
```

Execute an ASGI application with default options.

- `Module` - Python module containing the ASGI application (e.g., `<<"myapp">>`)
- `Callable` - Name of the ASGI callable (typically `<<"application">>` or `<<"app">>`)
- `Scope` - ASGI scope map (see Scope Fields below)
- `Body` - Request body as binary

#### `py_asgi:run/5`

```erlang
-spec run(Module, Callable, Scope, Body, Opts) -> Result when
    Module :: binary(),
    Callable :: binary(),
    Scope :: scope(),
    Body :: binary(),
    Opts :: map(),
    Result :: {ok, {integer(), [{binary(), binary()}], binary()}} | {error, term()}.
```

Execute an ASGI application with options.

Options:
- `runner` - Custom Python runner module (default: `<<"hornbeam_asgi_runner">>`)

#### `py_asgi:build_scope/1,2`

```erlang
-spec build_scope(Scope) -> {ok, reference()} | {error, term()}.
-spec build_scope(Scope, Opts) -> {ok, reference()} | {error, term()}.
```

Build an optimized Python scope dict with interned keys. The returned reference can be passed to multiple ASGI calls for further optimization when handling many requests with similar scopes.

### Scope Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | binary | Yes | Request type (`<<"http">>` or `<<"websocket">>`) |
| `asgi` | map | No | ASGI version info (default: `#{<<"version">> => <<"3.0">>}`) |
| `http_version` | binary | No | HTTP version (`<<"1.0">>`, `<<"1.1">>`, `<<"2">>`) |
| `method` | binary | No | HTTP method (`<<"GET">>`, `<<"POST">>`, etc.) |
| `scheme` | binary | No | URL scheme (`<<"http">>` or `<<"https">>`) |
| `path` | binary | Yes | Request path |
| `raw_path` | binary | No | Raw path (defaults to `path`) |
| `query_string` | binary | No | Query string without leading `?` |
| `root_path` | binary | No | Root path for mounted apps |
| `headers` | list | No | List of `[Name, Value]` header pairs |
| `server` | tuple | No | Server `{Host, Port}` tuple |
| `client` | tuple | No | Client `{Host, Port}` tuple |
| `state` | map | No | Request state dict |
| `extensions` | map | No | ASGI extensions |

## WSGI Support

### Basic Usage

```erlang
%% Build WSGI environ from your HTTP server
Environ = #{
    <<"REQUEST_METHOD">> => <<"GET">>,
    <<"SCRIPT_NAME">> => <<>>,
    <<"PATH_INFO">> => <<"/api/users">>,
    <<"QUERY_STRING">> => <<"id=123">>,
    <<"SERVER_NAME">> => <<"localhost">>,
    <<"SERVER_PORT">> => <<"8080">>,
    <<"SERVER_PROTOCOL">> => <<"HTTP/1.1">>,
    <<"wsgi.url_scheme">> => <<"http">>,
    <<"wsgi.input">> => Body
},

%% Execute WSGI application
case py_wsgi:run(<<"myapp">>, <<"application">>, Environ) of
    {ok, {Status, Headers, ResponseBody}} ->
        %% Parse status line (e.g., "200 OK")
        StatusCode = parse_status(Status),
        cowboy_req:reply(StatusCode, Headers, ResponseBody, Req);
    {error, Reason} ->
        cowboy_req:reply(500, #{}, <<"Internal Server Error">>, Req)
end.
```

### API Reference

#### `py_wsgi:run/3`

```erlang
-spec run(Module, Callable, Environ) -> Result when
    Module :: binary(),
    Callable :: binary(),
    Environ :: environ(),
    Result :: {ok, {binary(), [{binary(), binary()}], binary()}} | {error, term()}.
```

Execute a WSGI application with default options.

- `Module` - Python module containing the WSGI application
- `Callable` - Name of the WSGI callable
- `Environ` - WSGI environ map (see Environ Fields below)

Note: WSGI returns the status as a binary string (e.g., `<<"200 OK">>`), not an integer.

#### `py_wsgi:run/4`

```erlang
-spec run(Module, Callable, Environ, Opts) -> Result when
    Module :: binary(),
    Callable :: binary(),
    Environ :: environ(),
    Opts :: map(),
    Result :: {ok, {binary(), [{binary(), binary()}], binary()}} | {error, term()}.
```

Execute a WSGI application with options.

Options:
- `runner` - Custom Python runner module (default: `<<"hornbeam_wsgi_runner">>`)

### Environ Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `<<"REQUEST_METHOD">>` | binary | Yes | HTTP method |
| `<<"SCRIPT_NAME">>` | binary | Yes | Initial portion of URL path (can be empty) |
| `<<"PATH_INFO">>` | binary | Yes | Remainder of URL path |
| `<<"QUERY_STRING">>` | binary | No | Query string without leading `?` |
| `<<"CONTENT_TYPE">>` | binary | No | Content-Type header value |
| `<<"CONTENT_LENGTH">>` | binary | No | Content-Length header value |
| `<<"SERVER_NAME">>` | binary | Yes | Server hostname |
| `<<"SERVER_PORT">>` | binary | Yes | Server port as string |
| `<<"SERVER_PROTOCOL">>` | binary | Yes | Protocol version (e.g., `<<"HTTP/1.1">>`) |
| `<<"wsgi.version">>` | tuple | No | WSGI version tuple (default: `{1, 0}`) |
| `<<"wsgi.url_scheme">>` | binary | Yes | URL scheme (`<<"http">>` or `<<"https">>`) |
| `<<"wsgi.input">>` | binary | Yes | Request body |
| `<<"wsgi.errors">>` | any | No | Error stream |
| `<<"wsgi.multithread">>` | boolean | No | Default: `true` |
| `<<"wsgi.multiprocess">>` | boolean | No | Default: `true` |
| `<<"wsgi.run_once">>` | boolean | No | Default: `false` |
| `<<"HTTP_*">>` | binary | No | HTTP headers with `HTTP_` prefix |

## Custom Runner Modules

Both ASGI and WSGI support custom runner modules for advanced use cases.

### ASGI Runner

```python
# custom_asgi_runner.py
async def run_asgi(app, scope, body):
    """
    Custom ASGI runner.

    Args:
        app: The ASGI application callable
        scope: ASGI scope dict
        body: Request body bytes

    Returns:
        Tuple of (status_code, headers, body)
    """
    # Custom pre-processing
    scope['state']['custom_key'] = 'value'

    # Call the ASGI app
    response = await default_asgi_handler(app, scope, body)

    # Custom post-processing
    return response
```

```erlang
%% Use custom runner
py_asgi:run(<<"myapp">>, <<"app">>, Scope, Body, #{
    runner => <<"custom_asgi_runner">>
}).
```

### WSGI Runner

```python
# custom_wsgi_runner.py
def run_wsgi(app, environ):
    """
    Custom WSGI runner.

    Args:
        app: The WSGI application callable
        environ: WSGI environ dict

    Returns:
        Tuple of (status, headers, body)
    """
    # Custom pre-processing
    environ['custom.key'] = 'value'

    # Call the WSGI app
    response = default_wsgi_handler(app, environ)

    # Custom post-processing
    return response
```

```erlang
%% Use custom runner
py_wsgi:run(<<"myapp">>, <<"app">>, Environ, #{
    runner => <<"custom_wsgi_runner">>
}).
```

## Sub-interpreter and Free-threading Support

Both `py_asgi` and `py_wsgi` fully support Python's sub-interpreter and free-threading modes:

### Sub-interpreters (Python 3.12+)

Each request can run in an isolated sub-interpreter, providing:
- Isolated global state between requests
- No GIL contention between interpreters
- True parallelism for CPU-bound Python code

### Free-threading (Python 3.13+)

With Python 3.13's experimental free-threading build:
- No Global Interpreter Lock (GIL)
- True multi-threaded parallelism
- Best performance for concurrent requests

The modules automatically detect and use the optimal mode based on your Python installation.

## Integration Examples

### Cowboy Integration

```erlang
-module(my_asgi_handler).
-behaviour(cowboy_handler).

-export([init/2]).

init(Req0, State) ->
    Method = cowboy_req:method(Req0),
    Path = cowboy_req:path(Req0),
    QS = cowboy_req:qs(Req0),
    Headers = cowboy_req:headers(Req0),
    {ok, Body, Req1} = cowboy_req:read_body(Req0),

    Scope = #{
        type => <<"http">>,
        method => Method,
        path => Path,
        query_string => QS,
        headers => maps:to_list(Headers),
        scheme => <<"http">>
    },

    case py_asgi:run(<<"myapp">>, <<"app">>, Scope, Body) of
        {ok, {Status, RespHeaders, RespBody}} ->
            Req = cowboy_req:reply(Status, maps:from_list(RespHeaders), RespBody, Req1),
            {ok, Req, State};
        {error, _Reason} ->
            Req = cowboy_req:reply(500, #{}, <<"Internal Server Error">>, Req1),
            {ok, Req, State}
    end.
```

### Elli Integration

```erlang
-module(my_wsgi_handler).
-behaviour(elli_handler).

-export([handle/2, handle_event/3]).

handle(Req, _Args) ->
    Environ = #{
        <<"REQUEST_METHOD">> => elli_request:method(Req),
        <<"PATH_INFO">> => elli_request:path(Req),
        <<"QUERY_STRING">> => elli_request:query_str(Req),
        <<"SERVER_NAME">> => <<"localhost">>,
        <<"SERVER_PORT">> => <<"8080">>,
        <<"SERVER_PROTOCOL">> => <<"HTTP/1.1">>,
        <<"wsgi.url_scheme">> => <<"http">>,
        <<"wsgi.input">> => elli_request:body(Req)
    },

    case py_wsgi:run(<<"myapp">>, <<"application">>, Environ) of
        {ok, {Status, Headers, Body}} ->
            StatusCode = parse_wsgi_status(Status),
            {StatusCode, Headers, Body};
        {error, _} ->
            {500, [], <<"Internal Server Error">>}
    end.

handle_event(_, _, _) -> ok.

parse_wsgi_status(Status) ->
    [CodeBin | _] = binary:split(Status, <<" ">>),
    binary_to_integer(CodeBin).
```

## Performance Tips

### Use erlang_asyncio for Sleep Operations

For ASGI handlers that use `await asyncio.sleep()`, consider using `erlang_asyncio.sleep()` instead. This eliminates Python event loop overhead (~0.5-1ms per call) by using Erlang's native timer system:

```python
# In your ASGI application
import erlang_asyncio

async def delay_handler(scope, receive, send):
    # More efficient than asyncio.sleep()
    await erlang_asyncio.sleep(0.001)  # 1ms delay

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [[b'content-type', b'text/plain']],
    })
    await send({
        'type': 'http.response.body',
        'body': b'OK',
    })
```

For endpoints with short delays (1-10ms), this can improve throughput by 2-3x. See [Asyncio](asyncio.md#erlang_asyncio-module) for the full API.

## See Also

- [Getting Started](getting-started.md) - Basic usage guide
- [Asyncio](asyncio.md) - Async event loop integration and erlang_asyncio module
- [Threading](threading.md) - Thread support and callbacks
- [Scalability](scalability.md) - Performance tuning
