You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@echo "Running upstream connection pool tests only..."
225
225
./$(TARGET) upstream
226
226
227
+
# Run only proxy engine tests
228
+
test_proxy: $(TARGET)
229
+
@echo "Running proxy engine tests only..."
230
+
./$(TARGET) proxy
231
+
227
232
# Display help information
228
233
help:
229
234
@echo "Reactor Server C++ - Makefile Help"
@@ -304,4 +309,4 @@ help:
304
309
# Build only the production server binary
305
310
server: $(SERVER_TARGET)
306
311
307
-
.PHONY: all clean test server test_basic test_stress test_race test_config test_http test_ws test_tls test_cli test_http2 test_upstream help
312
+
.PHONY: all clean test server test_basic test_stress test_race test_config test_http test_ws test_tls test_cli test_http2 test_upstream test_proxy help
Copy file name to clipboardExpand all lines: docs/configuration.md
+74Lines changed: 74 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -201,6 +201,80 @@ Upstream connection pools are configured via the `upstreams` array in the JSON c
201
201
202
202
**Note:** Upstream configuration changes require a server restart — pools are built once during `Start()` and cannot be rebuilt at runtime.
203
203
204
+
### Proxy Route Configuration
205
+
206
+
Each upstream entry may include an optional `proxy` section to auto-register a proxy route that forwards matching requests to the backend. When `proxy.route_prefix` is non-empty, `HttpServer::Start()` registers the route automatically — no handler code is needed.
207
+
208
+
```json
209
+
{
210
+
"upstreams": [
211
+
{
212
+
"name": "api-backend",
213
+
"host": "10.0.1.5",
214
+
"port": 8080,
215
+
"pool": { "max_connections": 64 },
216
+
"proxy": {
217
+
"route_prefix": "/api/v1",
218
+
"strip_prefix": true,
219
+
"response_timeout_ms": 5000,
220
+
"methods": ["GET", "POST", "PUT", "DELETE"],
221
+
"header_rewrite": {
222
+
"set_x_forwarded_for": true,
223
+
"set_x_forwarded_proto": true,
224
+
"set_via_header": true,
225
+
"rewrite_host": true
226
+
},
227
+
"retry": {
228
+
"max_retries": 2,
229
+
"retry_on_connect_failure": true,
230
+
"retry_on_5xx": false,
231
+
"retry_on_timeout": false,
232
+
"retry_on_disconnect": true,
233
+
"retry_non_idempotent": false
234
+
}
235
+
}
236
+
}
237
+
]
238
+
}
239
+
```
240
+
241
+
**Proxy fields** (`proxy.*`):
242
+
243
+
| Field | Default | Description |
244
+
|-------|---------|-------------|
245
+
|`route_prefix`| "" | Route pattern to match (empty = disabled). Supports full pattern syntax: `/api/v1`, `/api/:version/*path`, `/users/:id([0-9]+)`. Patterns ending in `/*rest` match anything under the prefix. |
246
+
|`strip_prefix`| false | When `true`, strip the static portion of `route_prefix` before forwarding. Example: `route_prefix="/api/v1"`, `strip_prefix=true` → client `GET /api/v1/users/123` reaches upstream as `GET /users/123`. |
247
+
|`response_timeout_ms`| 30000 | Max time to wait for upstream response headers after the request is fully sent. **Must be `0` or `>= 1000`** (timer scan has 1 s resolution). `0` disables the per-request deadline and lifts the async safety cap for this request only — use with caution, long-running handlers still respect the server-wide `max_async_deferred_sec_`. |
248
+
|`methods`|`[]`| Methods to proxy. Empty array means all methods. Methods listed here are auto-registered on the route; conflicts with any user-registered async route on the same `(method, pattern)` are detected at `Start()` and raise `std::invalid_argument`. |
|`set_x_forwarded_for`| true | Append the client IP to `X-Forwarded-For` (preserves any upstream chain) |
255
+
|`set_x_forwarded_proto`| true | Set `X-Forwarded-Proto` to `http` or `https` based on the client connection |
256
+
|`set_via_header`| true | Add the server's `Via` header per RFC 7230 §5.7.1 |
257
+
|`rewrite_host`| true | Rewrite the outgoing `Host` header to the upstream's authority (off = forward client's Host verbatim) |
258
+
259
+
Hop-by-hop headers listed in RFC 7230 §6.1 (`Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `TE`, `Trailers`, `Transfer-Encoding`, `Upgrade`) are always stripped from both the outgoing request and the returned response.
260
+
261
+
**Proxy retry fields** (`proxy.retry.*`):
262
+
263
+
| Field | Default | Description |
264
+
|-------|---------|-------------|
265
+
|`max_retries`| 0 | Max retry attempts (0 = no retries). Backoff is jittered exponential (25 ms base, 250 ms cap). |
266
+
|`retry_on_connect_failure`| true | Retry when the pool checkout fails to establish a TCP/TLS connection |
267
+
|`retry_on_5xx`| false | Retry when the upstream returns a 5xx response (headers only — once the body starts streaming to the client, retries stop) |
268
+
|`retry_on_timeout`| false | Retry when the response deadline fires before headers arrive |
269
+
|`retry_on_disconnect`| true | Retry when the upstream closes the connection before any response bytes are sent to the client |
270
+
|`retry_non_idempotent`| false | Allow retries on POST/PATCH/DELETE (dangerous — can duplicate side effects; default safe methods only) |
271
+
272
+
**Notes:**
273
+
274
+
- Retries never fire after any response bytes have been sent to the downstream client.
275
+
-`proxy.route_prefix` conflicts — two upstreams auto-registering the same pattern, or an upstream conflicting with a user-registered async route on the same `(method, pattern)` — are rejected at `Start()` with `std::invalid_argument`.
276
+
- The proxy engine is built on the async route framework: per-request deadlines, client abort propagation, and pool checkout cancellation are all handled by `ProxyTransaction::Cancel()`. See [docs/http.md](http.md) for the programmatic API.
Copy file name to clipboardExpand all lines: docs/http.md
+90-2Lines changed: 90 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -189,6 +189,90 @@ The handler receives a const request reference and a completion callback. Call `
189
189
-**Thread safety** — the completion callback MUST be invoked on the dispatcher thread that owns the connection. Upstream pool `CheckoutAsync` naturally routes callbacks to the correct dispatcher.
190
190
-**HTTP/2 support** — async routes work identically for H2 streams; the framework binds `SubmitStreamResponse` internally
191
191
192
+
## Proxy Routes
193
+
194
+
Proxy routes forward client requests to an upstream backend service. They are built on top of the async-route framework and require a matching `upstreams[]` entry in the server config so the connection pool, TLS client context, and retry/header policies exist. See [docs/configuration.md](configuration.md#proxy-route-configuration) for the full set of config fields.
195
+
196
+
### Auto-registration from config
197
+
198
+
The simplest way to use a proxy route is to set `proxy.route_prefix` in the upstream config. `HttpServer::Start()` walks every upstream with a non-empty `route_prefix` and registers the route automatically — no application code required.
199
+
200
+
```json
201
+
{
202
+
"upstreams": [
203
+
{
204
+
"name": "api-backend",
205
+
"host": "10.0.1.5",
206
+
"port": 8080,
207
+
"pool": { "max_connections": 64 },
208
+
"proxy": {
209
+
"route_prefix": "/api/v1/*rest",
210
+
"strip_prefix": true,
211
+
"methods": ["GET", "POST", "PUT", "DELETE"]
212
+
}
213
+
}
214
+
]
215
+
}
216
+
```
217
+
218
+
Any `GET/POST/PUT/DELETE` under `/api/v1/` is forwarded to `api-backend`, with the `/api/v1` prefix stripped before forwarding (so upstream sees `/users/123` instead of `/api/v1/users/123`).
219
+
220
+
### Programmatic registration
221
+
222
+
Applications that construct their own config in code can use `HttpServer::Proxy()`:
223
+
224
+
```cpp
225
+
#include"http/http_server.h"
226
+
227
+
HttpServer server(config);
228
+
229
+
// Register a proxy route on an already-configured upstream.
230
+
// Reuses the proxy fields (methods, strip_prefix, header_rewrite, retry,
231
+
// response_timeout_ms) from config.upstreams[i].proxy — only route_prefix
232
+
// is overridden by the first argument.
233
+
server.Proxy("/api/v1/*rest", "api-backend");
234
+
235
+
server.Start();
236
+
```
237
+
238
+
`Proxy()` calls must happen before `Start()`. Calling it afterwards — or naming an upstream that is not in the config — raises `std::invalid_argument`.
239
+
240
+
### HEAD precedence and companion methods
241
+
242
+
Proxy registrations interact with the HEAD-fallback rule from [Route Matching](#route-matching) as follows:
243
+
244
+
- **Paired HEAD + GET on the same registration** (both in `methods`): HEAD goes to the proxy, GET goes to the proxy. No fallback.
245
+
- **HEAD only** (no GET in `methods`): HEAD is registered as a proxy *default*. If a user async handler later registers GET on the same pattern, the router uses the user's GET for HEAD fallback and silently drops the proxy HEAD. This prevents accidental conflicts between library-provided proxies and application-defined GETs.
246
+
- **Companion methods**: If a proxy registers `OPTIONS` for a pattern that also has a user-registered async GET, the router marks the proxy pattern as a *companion*. At dispatch time, if the companion proxy route wins (e.g. for a non-matching method), it yields to the user handler via a runtime decision rather than a registration-time rejection — because the conflict is method-level and only detectable per-request.
247
+
- Per-`(method, pattern)` conflict markers are stored separately so two proxies registering disjoint methods on the same pattern do not contaminate each other's HEAD pairing.
248
+
249
+
### Request lifecycle and client abort
250
+
251
+
Each proxy request is handled by a per-request `ProxyTransaction`:
252
+
253
+
1. `CHECKOUT_PENDING` — wait for an idle pooled connection (or open a new one, subject to `pool.max_connections`)
254
+
2. `SENDING_REQUEST` — serialize and write the HTTP/1.1 request, with header rewriting applied
255
+
3. `AWAITING_RESPONSE` — wait for response headers (bounded by `proxy.response_timeout_ms`)
256
+
4. `RECEIVING_BODY` — stream the body back to the client
257
+
5. `COMPLETE` / `FAILED` — return the connection to the pool or discard it
258
+
259
+
If the client disconnects mid-request, the framework's async-abort hook calls `ProxyTransaction::Cancel()`, which:
260
+
261
+
- Sets a `cancelled_` flag guarding every callback entry point
262
+
- Signals the pool wait-queue via a shared cancel token so `PoolPartition` can purge the dead entry
263
+
- Poisons the upstream connection (`MarkClosing()`) if any bytes have already been written — retrying a partially-sent request on a reused connection is unsafe
264
+
- Returns the connection to the pool (or destroys it) without further I/O
265
+
266
+
### Response timeouts and the async safety cap
267
+
268
+
`proxy.response_timeout_ms` is the hard deadline for receiving response headers after the request is fully sent. Its valid values are:
269
+
270
+
- **`>= 1000`** — normal case. The deadline is armed when the request is flushed and cleared when headers arrive. If it fires, the transaction retries (if policy allows) or responds with 504.
271
+
- **`0`** — disables the per-request deadline *and* disables the server-wide async safety cap (`max_async_deferred_sec_`) for this request only. The `ProxyHandler` sets `request.async_cap_sec_override = 0` before dispatching. Use this only for intentionally long-polling backends; normal requests should keep a bounded timeout.
272
+
- **Other positive values below 1000** — rejected at config load (the 1 s floor matches the timer scan resolution).
273
+
274
+
Retries are bounded by `proxy.retry.max_retries` and never fire after any response bytes have reached the client. See [docs/configuration.md](configuration.md#proxy-route-configuration) for the full retry matrix.
0 commit comments