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
176 changes: 176 additions & 0 deletions docs/advanced/async-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Async Tests

FasterAPI handlers are async coroutines. Use **pytest-asyncio** to test them
directly without going through HTTP, or combine with `TestClient` for full
integration testing.

## Installation

```bash
pip install pytest pytest-asyncio httpx
pip install faster-api-web[test]
```

## Configuring pytest-asyncio

Add to `pyproject.toml`:

```toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```

Or per-file with a marker:

```python
import pytest
pytestmark = pytest.mark.asyncio
```

## Testing handler functions directly

Call the handler as a coroutine without HTTP overhead:

```python
# main.py
import msgspec
from FasterAPI import Faster

app = Faster()


class Item(msgspec.Struct):
name: str
price: float


@app.get("/items/{item_id}")
async def get_item(item_id: int) -> Item:
return Item(id=item_id, name="Widget", price=9.99)
```

```python
# tests/test_handlers.py
import pytest
from main import get_item


@pytest.mark.asyncio
async def test_get_item():
result = await get_item(item_id=1)
assert result.name == "Widget"
assert result.price == 9.99
```

## Async fixtures

```python
import pytest_asyncio


@pytest_asyncio.fixture
async def db_session():
session = await open_test_db()
yield session
await session.close()


@pytest.mark.asyncio
async def test_with_db(db_session):
rows = await db_session.fetch("SELECT 1")
assert rows is not None
```

## Async TestClient

Use `httpx.AsyncClient` with the ASGI transport for fully async integration tests:

```python
import pytest
import httpx
from main import app


@pytest.mark.asyncio
async def test_list_items_async():
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://test",
) as client:
r = await client.get("/items")
assert r.status_code == 200
```

## Concurrent requests

Test that concurrent requests behave correctly:

```python
import asyncio


@pytest.mark.asyncio
async def test_concurrent_requests():
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://test",
) as client:
tasks = [client.get("/items") for _ in range(10)]
responses = await asyncio.gather(*tasks)
assert all(r.status_code == 200 for r in responses)
```

## Testing WebSockets

```python
from FasterAPI import TestClient
from main import app


def test_websocket():
client = TestClient(app)
with client.websocket_connect("/ws") as ws:
ws.send_text("hello")
data = ws.receive_text()
assert data == "Echo: hello"
```

## Testing lifespan events

```python
@pytest.fixture
def app_with_lifespan():
from FasterAPI import Faster
test_app = Faster()
state = {}

@test_app.on_startup
async def startup():
state["db"] = "connected"

@test_app.get("/status")
async def status():
return {"db": state.get("db")}

return test_app, state


def test_startup_ran(app_with_lifespan):
test_app, state = app_with_lifespan
with TestClient(test_app) as client:
r = client.get("/status")
assert r.json()["db"] == "connected"
```

## Coverage

Run with coverage:

```bash
pytest --cov=. --cov-report=term-missing
```

## Next steps

- [Testing with Overrides](testing-overrides.md) — swap dependencies in tests.
- [Dependencies](../tutorial/dependencies.md) — understand what you're testing.
132 changes: 132 additions & 0 deletions docs/advanced/behind-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Behind a Proxy

When FasterAPI runs behind a reverse proxy (Nginx, Traefik, AWS ALB, …), the
original client IP and URL scheme may be rewritten. Configure your app to trust
forwarded headers.

## The problem

Without configuration, `request.client` returns the **proxy's IP**, not the browser's.
Similarly, `request.url.scheme` may say `http` even though the client used `https`.

## Trusting `X-Forwarded-For`

```python
from FasterAPI import Faster, Request

app = Faster()


@app.get("/ip")
async def client_ip(request: Request):
forwarded_for = request.headers.get("x-forwarded-for")
if forwarded_for:
# X-Forwarded-For can be a comma-separated list: client, proxy1, proxy2
real_ip = forwarded_for.split(",")[0].strip()
else:
real_ip = request.client[0] if request.client else None
return {"ip": real_ip}
```

!!! warning
Only trust `X-Forwarded-For` if your app is **guaranteed to be behind a proxy**
that strips or overwrites this header. Otherwise a client can spoof it.

## Root path

If your API is mounted at a sub-path (e.g. `/api/v1`), tell the server via the
`root_path` ASGI extension. Pass it to uvicorn:

```bash
uvicorn main:app --root-path /api/v1
```

Or set in a middleware:

```python
from FasterAPI import BaseHTTPMiddleware


class RootPathMiddleware(BaseHTTPMiddleware):
def __init__(self, app, root_path: str = ""):
super().__init__(app)
self.root_path = root_path

async def dispatch(self, scope, receive, send):
if scope["type"] in ("http", "websocket"):
scope = {**scope, "root_path": self.root_path}
await self.app(scope, receive, send)


app.add_middleware(RootPathMiddleware, root_path="/api/v1")
```

This makes Swagger UI generate correct server URLs.

## Nginx configuration

```nginx
upstream fasterapi {
server 127.0.0.1:8000;
}

server {
listen 443 ssl;
server_name api.example.com;

ssl_certificate /etc/ssl/certs/api.pem;
ssl_certificate_key /etc/ssl/private/api.key;

location /api/ {
proxy_pass http://fasterapi/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
}
```

## Traefik configuration (Docker labels)

```yaml
services:
api:
image: my-fasterapi-app
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=PathPrefix(`/api`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.middlewares.strip-api.stripprefix.prefixes=/api"
- "traefik.http.routers.api.middlewares=strip-api"
```

## Trusted host validation

Only allow requests with specific `Host` headers to prevent host-header injection:

```python
from FasterAPI import TrustedHostMiddleware

app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com", "*.example.com"],
)
```

## HTTPS redirect

Redirect all plain HTTP traffic to HTTPS at the application layer (usually handled
by the proxy, but useful as a safety net):

```python
from FasterAPI import HTTPSRedirectMiddleware

app.add_middleware(HTTPSRedirectMiddleware)
```

## Next steps

- [Deployment: Nginx/Traefik](../deployment/nginx-traefik.md) — full proxy configs.
- [Deployment: Docker](../deployment/docker.md) — containerised deployments.
Loading
Loading