astor 0.3.0

A fast, minimal HTTP framework for reverse-proxy deployments
Documentation
# nginx configuration

astor is built to run behind a reverse proxy. The settings below are
**required** — astor assumes this work has already been done and does not
re-implement it.

---

## Required

### `proxy_buffering on`

astor reads `Content-Length`-framed bodies only. This is the nginx default,
but it must not be disabled.

```nginx
proxy_buffering on;
```

Without it, nginx forwards chunked bodies with no `Content-Length` header.
astor cannot parse these and will drop the body silently.

### `proxy_http_version 1.1` + `proxy_set_header Connection ""`

astor loops on each TCP connection until nginx closes it (keep-alive). Without
these two lines nginx defaults to HTTP/1.0 and sends `Connection: close`,
which closes the connection after every single request — no pooling, pure TCP
churn.

```nginx
proxy_http_version 1.1;
proxy_set_header   Connection "";
```

### Body size limit

nginx enforces the body size limit before the request reaches astor. astor
does not check it. Without this, a client can stream an arbitrarily large body.

`client_max_body_size` is inherited by `location` blocks, so you can set a
global default and override it per route — no code changes in astor needed.

```nginx
http {
    client_max_body_size 1m;   # global default

    server {
        location /upload {
            client_max_body_size 100m;  # this route allows larger bodies
        }

        location /api {
            # inherits 1m from http block
        }
    }
}
```

### Header size limit

nginx enforces header size limits before the request reaches astor. The
defaults (`client_header_buffer_size 1k`, `large_client_header_buffers 4 8k`)
are usually fine. Raise them only if you forward large cookies or tokens.

```nginx
client_header_buffer_size    1k;
large_client_header_buffers  4 8k;
```

### Slow-client timeouts

nginx drops slow clients before they reach astor. astor has no timeout logic —
it trusts the proxy to handle this.

```nginx
client_body_timeout   30s;
client_header_timeout 10s;
```

### Method whitelist

nginx forwards any method string to upstream by default — `ANYTHING /path
HTTP/1.1` gets proxied unchanged. Filter to the methods your service actually
handles and return 405 for everything else.

```nginx
# Example — adjust this list to the methods your service handles.
# The regex is case-sensitive (~, not ~*). HTTP methods must be uppercase
# per RFC 9110 §9.1. astor does not normalise case — it assumes nginx already
# enforces the standard. A client sending `get` instead of `GET` is violating
# the spec and will get a 405 here, before reaching astor.
if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
    return 405;
}
```

---

## Header names

nginx lowercases all forwarded header names before passing them to astor
(e.g. `Authorization` → `authorization`). astor's `req.header()` does
case-insensitive matching, so both forms work — but on the wire from nginx
they will always be lowercase.

---

## Minimal upstream block

```nginx
upstream astor {
    server 127.0.0.1:3000;

    keepalive          64;    # idle connections per worker
    keepalive_requests 1000;  # recycle after N requests
    keepalive_timeout  60s;   # close idle connections after this long
}

server {
    listen 80;
    server_name example.com;

    # Required — adjust to your workload
    client_max_body_size     10m;
    client_body_timeout      30s;
    client_header_timeout    10s;
    client_header_buffer_size    1k;
    large_client_header_buffers  4 8k;

    # Required — example method list, adjust to your service
    if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
        return 405;
    }

    location / {
        proxy_pass         http://astor;

        # Required for keep-alive
        proxy_http_version 1.1;
        proxy_set_header   Connection "";

        # Required — do not disable
        proxy_buffering    on;

        # Forward real client info
        proxy_set_header   Host            $host;
        proxy_set_header   X-Real-IP       $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
```

---

## Kubernetes (ingress-nginx)

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: astor
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffering: "on"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/server-snippet: |
      client_body_timeout   30s;
      client_header_timeout 10s;

      # Example method list — adjust to your service. Case-sensitive — see note above.
      if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
        return 405;
      }
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: astor
                port:
                  number: 3000
```

**Required:** set `terminationGracePeriodSeconds` longer than your slowest
request. On `SIGTERM`, astor stops accepting new connections and drains
in-flight requests. If k8s sends `SIGKILL` before the drain finishes, those
requests are dropped — that is not graceful shutdown.

```yaml
spec:
  terminationGracePeriodSeconds: 30  # must be longer than your slowest request
  containers:
    - name: app
      image: your-registry/your-app:latest
      livenessProbe:
        httpGet: { path: /healthz, port: 3000 }
      readinessProbe:
        httpGet: { path: /readyz, port: 3000 }
```