## 🧱 Components

Pingclair is a Cargo workspace. The running server is the `pingclair` binary,
which links the crates below.

| Crate | Responsibility |
| --- | --- |
| `pingclair` | Command-line entry point: argument parsing, logging, startup, and the service wrapper. |
| `pingclair-config` | Configuration compiler: lexes, parses, and semantically checks the Pingclairfile. |
| `pingclair-proxy` | HTTP/1.1 and HTTP/2 proxying on Pingora, the HTTP/3 listener on quiche, load balancing, and the shared request policy layer. |
| `pingclair-static` | Static file serving: file reads, MIME types, range requests, and streaming. |
| `pingclair-tls` | Certificate management: manual certificates, a persistent internal certificate authority, and automatic ACME issuance. |
| `pingclair-api` | Admin API for inspecting state and reloading configuration. |
| `pingclair-core` | Data structures and server lifecycle shared by the crates above. |

## 🚦 The path of a request

```text
client
  |
  |  TLS with ALPN, or QUIC
  v
listener             HTTP/1.1 and HTTP/2 on TCP, HTTP/3 on UDP
  |
  v
transport adapter    Pingora ProxyHttp for TCP, tokio-quiche for QUIC
  |
  v
policy layer         routing, matchers, headers, rate limits, access log
  |
  v
handler              file server | reverse proxy | FastCGI | static response
  |
  v
upstream or disk
```

Both transports converge on the same policy layer, so routing, header
handling, rate limiting, and access logging behave the same on HTTP/1.1,
HTTP/2, and HTTP/3. The transports differ only where the protocol requires it,
and those differences are listed below.

## 🌊 Request handling properties

- **Bodies are streamed.** Request and response bodies move through the proxy in
  bounded chunks. Compression, middleware, and proxying do not buffer a
  complete body, so a large upload or a slow reader does not consume memory
  proportional to the body size.
- **Upstream connections are pooled.** Keepalive connections to backends are
  reused. Hostname upstreams are re-resolved on the interval set by
  `dns_refresh`, so a container that restarts on a new address is followed
  without an operator action.
- **Runtime state is immutable at request time.** Requests read a published
  snapshot. A reload publishes a new snapshot instead of mutating the one in
  use.

## 🌐 Protocol-specific behavior

Some behavior differs by protocol by design. It is listed here rather than
discovered later:

| Area | Behavior |
| --- | --- |
| Trailers | Declared request trailers are not forwarded. The server answers `501` before the response is committed, resets an already committed HTTP/3 stream, and answers `502` when an upstream advertises response trailers. |
| CONNECT | `CONNECT` and extended `CONNECT` return `501` on HTTP/3 until tunnel support is implemented. |
| FastCGI | `php_fastcgi` works on HTTP/1.1 and HTTP/2. Routes that need FastCGI return `501` on HTTP/3 until that path has its own FastCGI client. |

## ⚠️ Known defect

WebSocket upgrades fail intermittently under load: roughly 10-15% of upgrades on
a busy machine. The cause is a race in the upstream `pingora-proxy` crate rather
than in Pingclair's own upgrade handling, and it is invisible on an idle
developer machine, which is why it is documented here. Upstream issue:
[cloudflare/pingora#946](https://github.com/cloudflare/pingora/issues/946).
