Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
cb710da
A proxy engine that receives client HTTP requests, selects an upstrea…
mwfj Apr 7, 2026
cc8f351
Fix review comment
mwfj Apr 8, 2026
3f8172b
Fix review comment
mwfj Apr 8, 2026
8e443f5
Fix review comment
mwfj Apr 8, 2026
45bd80e
Fix review comment
mwfj Apr 8, 2026
51b6a84
Fix review comment
mwfj Apr 8, 2026
8820605
Fix review comment
mwfj Apr 8, 2026
8ddf3cc
Fix review comment
mwfj Apr 9, 2026
2446314
Fix review comment
mwfj Apr 9, 2026
72d28fa
Fix review comment
mwfj Apr 9, 2026
3d45113
Fix review comment
mwfj Apr 9, 2026
712ca62
Fix review comment
mwfj Apr 9, 2026
8fa2c2f
Fix review comment
mwfj Apr 9, 2026
7c17db7
Fix review comment
mwfj Apr 9, 2026
9141ddc
Fix review comment
mwfj Apr 9, 2026
9eb49d8
Fix review comment
mwfj Apr 9, 2026
1a49e23
Fix review comment
mwfj Apr 9, 2026
24c15ba
Fix review comment
mwfj Apr 9, 2026
13d5aa5
Fix review comment
mwfj Apr 9, 2026
976b9f5
Fix review comment
mwfj Apr 9, 2026
36d477b
Fix review comment
mwfj Apr 9, 2026
54201bd
Fix review comment
mwfj Apr 10, 2026
05bd6b0
Fix review comment
mwfj Apr 10, 2026
8445321
Fix review comment
mwfj Apr 10, 2026
b06bf77
Fix review comment
mwfj Apr 10, 2026
7a5155f
Fix review comment
mwfj Apr 10, 2026
a790d63
Fix review comment
mwfj Apr 10, 2026
69d9709
Fix review comment
mwfj Apr 10, 2026
3fdff6b
Fix review comment
mwfj Apr 10, 2026
1b1f1bd
Fix review comment
mwfj Apr 10, 2026
ac9b1d7
Fix review comment
mwfj Apr 10, 2026
08f36c9
Fix review comment
mwfj Apr 10, 2026
bbac81b
Fix review comment
mwfj Apr 11, 2026
8a218e3
Fix review comment
mwfj Apr 11, 2026
26404d8
Fix review comment
mwfj Apr 11, 2026
0aa8e32
Fix review comment
mwfj Apr 11, 2026
927f068
Fix review comment
mwfj Apr 11, 2026
78d2ff2
Fix review comment
mwfj Apr 11, 2026
24559c8
Fix review comment
mwfj Apr 11, 2026
34d0e8e
Fix review comment
mwfj Apr 11, 2026
35dcc28
Fix review comment
mwfj Apr 11, 2026
8f85ba8
Fix review comment
mwfj Apr 11, 2026
b96c302
Fix review comment
mwfj Apr 11, 2026
f97e16f
Fix review comment
mwfj Apr 11, 2026
6fb00d8
Fix review comment
mwfj Apr 11, 2026
9cf7104
Fix review comment
mwfj Apr 11, 2026
0470e62
Fix review comment
mwfj Apr 11, 2026
74b5794
Fix review comment
mwfj Apr 11, 2026
2d67cb6
Fix review comment
mwfj Apr 11, 2026
2cc121b
Fix review comment
mwfj Apr 11, 2026
7110eac
Fix review comment
mwfj Apr 11, 2026
c434951
Fix review comment
mwfj Apr 11, 2026
621c5e9
Fix review comment
mwfj Apr 12, 2026
7d68602
Fix review comment
mwfj Apr 12, 2026
76e6e60
Fix review comment
mwfj Apr 12, 2026
b9324b6
Fix review comment
mwfj Apr 12, 2026
0410e3c
Update docs
mwfj Apr 12, 2026
d49f66e
Fix review comment
mwfj Apr 12, 2026
54c8518
Update docs
mwfj Apr 12, 2026
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
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ HTTP2_SRCS = $(SERVER_DIR)/http2_session.cc $(SERVER_DIR)/http2_stream.cc $(SERV
TLS_SRCS = $(SERVER_DIR)/tls_context.cc $(SERVER_DIR)/tls_connection.cc $(SERVER_DIR)/tls_client_context.cc

# Upstream connection pool sources
UPSTREAM_SRCS = $(SERVER_DIR)/upstream_connection.cc $(SERVER_DIR)/pool_partition.cc $(SERVER_DIR)/upstream_host_pool.cc $(SERVER_DIR)/upstream_manager.cc
UPSTREAM_SRCS = $(SERVER_DIR)/upstream_connection.cc $(SERVER_DIR)/pool_partition.cc $(SERVER_DIR)/upstream_host_pool.cc $(SERVER_DIR)/upstream_manager.cc $(SERVER_DIR)/header_rewriter.cc $(SERVER_DIR)/retry_policy.cc $(SERVER_DIR)/upstream_http_codec.cc $(SERVER_DIR)/http_request_serializer.cc $(SERVER_DIR)/proxy_transaction.cc $(SERVER_DIR)/proxy_handler.cc

# CLI layer sources
CLI_SRCS = $(SERVER_DIR)/cli_parser.cc $(SERVER_DIR)/signal_handler.cc $(SERVER_DIR)/pid_file.cc $(SERVER_DIR)/daemonizer.cc
Expand Down Expand Up @@ -137,9 +137,9 @@ HTTP_HEADERS = $(LIB_DIR)/http/http_callbacks.h $(LIB_DIR)/http/http_connection_
HTTP2_HEADERS = $(LIB_DIR)/http2/http2_callbacks.h $(LIB_DIR)/http2/http2_connection_handler.h $(LIB_DIR)/http2/http2_constants.h $(LIB_DIR)/http2/http2_session.h $(LIB_DIR)/http2/http2_stream.h $(LIB_DIR)/http2/protocol_detector.h
WS_HEADERS = $(LIB_DIR)/ws/websocket_connection.h $(LIB_DIR)/ws/websocket_frame.h $(LIB_DIR)/ws/websocket_handshake.h $(LIB_DIR)/ws/websocket_parser.h $(LIB_DIR)/ws/utf8_validate.h
TLS_HEADERS = $(LIB_DIR)/tls/tls_context.h $(LIB_DIR)/tls/tls_connection.h $(LIB_DIR)/tls/tls_client_context.h
UPSTREAM_HEADERS = $(LIB_DIR)/upstream/upstream_manager.h $(LIB_DIR)/upstream/upstream_host_pool.h $(LIB_DIR)/upstream/pool_partition.h $(LIB_DIR)/upstream/upstream_connection.h $(LIB_DIR)/upstream/upstream_lease.h
UPSTREAM_HEADERS = $(LIB_DIR)/upstream/upstream_manager.h $(LIB_DIR)/upstream/upstream_host_pool.h $(LIB_DIR)/upstream/pool_partition.h $(LIB_DIR)/upstream/upstream_connection.h $(LIB_DIR)/upstream/upstream_lease.h $(LIB_DIR)/upstream/upstream_http_codec.h $(LIB_DIR)/upstream/http_request_serializer.h $(LIB_DIR)/upstream/header_rewriter.h $(LIB_DIR)/upstream/retry_policy.h $(LIB_DIR)/upstream/proxy_transaction.h $(LIB_DIR)/upstream/proxy_handler.h $(LIB_DIR)/upstream/upstream_response.h $(LIB_DIR)/upstream/upstream_callbacks.h
CLI_HEADERS = $(LIB_DIR)/cli/cli_parser.h $(LIB_DIR)/cli/signal_handler.h $(LIB_DIR)/cli/pid_file.h $(LIB_DIR)/cli/version.h $(LIB_DIR)/cli/daemonizer.h
TEST_HEADERS = $(TEST_DIR)/test_framework.h $(TEST_DIR)/http_test_client.h $(TEST_DIR)/basic_test.h $(TEST_DIR)/stress_test.h $(TEST_DIR)/race_condition_test.h $(TEST_DIR)/timeout_test.h $(TEST_DIR)/config_test.h $(TEST_DIR)/http_test.h $(TEST_DIR)/websocket_test.h $(TEST_DIR)/tls_test.h $(TEST_DIR)/cli_test.h $(TEST_DIR)/http2_test.h $(TEST_DIR)/route_test.h $(TEST_DIR)/upstream_pool_test.h
TEST_HEADERS = $(TEST_DIR)/test_framework.h $(TEST_DIR)/http_test_client.h $(TEST_DIR)/basic_test.h $(TEST_DIR)/stress_test.h $(TEST_DIR)/race_condition_test.h $(TEST_DIR)/timeout_test.h $(TEST_DIR)/config_test.h $(TEST_DIR)/http_test.h $(TEST_DIR)/websocket_test.h $(TEST_DIR)/tls_test.h $(TEST_DIR)/cli_test.h $(TEST_DIR)/http2_test.h $(TEST_DIR)/route_test.h $(TEST_DIR)/upstream_pool_test.h $(TEST_DIR)/proxy_test.h

# All headers combined
HEADERS = $(CORE_HEADERS) $(CALLBACK_HEADERS) $(REACTOR_HEADERS) $(NETWORK_HEADERS) $(SERVER_HEADERS) $(THREAD_POOL_HEADERS) $(UTIL_HEADERS) $(FOUNDATION_HEADERS) $(HTTP_HEADERS) $(HTTP2_HEADERS) $(WS_HEADERS) $(TLS_HEADERS) $(UPSTREAM_HEADERS) $(CLI_HEADERS) $(TEST_HEADERS)
Expand Down Expand Up @@ -224,6 +224,11 @@ test_upstream: $(TARGET)
@echo "Running upstream connection pool tests only..."
./$(TARGET) upstream

# Run only proxy engine tests
test_proxy: $(TARGET)
@echo "Running proxy engine tests only..."
./$(TARGET) proxy

# Display help information
help:
@echo "Reactor Server C++ - Makefile Help"
Expand Down Expand Up @@ -304,4 +309,4 @@ help:
# Build only the production server binary
server: $(SERVER_TARGET)

.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
.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
74 changes: 74 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,80 @@ Upstream connection pools are configured via the `upstreams` array in the JSON c

**Note:** Upstream configuration changes require a server restart — pools are built once during `Start()` and cannot be rebuilt at runtime.

### Proxy Route Configuration

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.

```json
{
"upstreams": [
{
"name": "api-backend",
"host": "10.0.1.5",
"port": 8080,
"pool": { "max_connections": 64 },
"proxy": {
"route_prefix": "/api/v1",
"strip_prefix": true,
"response_timeout_ms": 5000,
"methods": ["GET", "POST", "PUT", "DELETE"],
"header_rewrite": {
"set_x_forwarded_for": true,
"set_x_forwarded_proto": true,
"set_via_header": true,
"rewrite_host": true
},
"retry": {
"max_retries": 2,
"retry_on_connect_failure": true,
"retry_on_5xx": false,
"retry_on_timeout": false,
"retry_on_disconnect": true,
"retry_non_idempotent": false
}
}
}
]
}
```

**Proxy fields** (`proxy.*`):

| Field | Default | Description |
|-------|---------|-------------|
| `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. |
| `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`. |
| `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_`. |
| `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`. |

**Proxy header rewrite fields** (`proxy.header_rewrite.*`):

| Field | Default | Description |
|-------|---------|-------------|
| `set_x_forwarded_for` | true | Append the client IP to `X-Forwarded-For` (preserves any upstream chain) |
| `set_x_forwarded_proto` | true | Set `X-Forwarded-Proto` to `http` or `https` based on the client connection |
| `set_via_header` | true | Add the server's `Via` header per RFC 7230 §5.7.1 |
| `rewrite_host` | true | Rewrite the outgoing `Host` header to the upstream's authority (off = forward client's Host verbatim) |

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.

**Proxy retry fields** (`proxy.retry.*`):

| Field | Default | Description |
|-------|---------|-------------|
| `max_retries` | 0 | Max retry attempts (0 = no retries). Backoff is jittered exponential (25 ms base, 250 ms cap). |
| `retry_on_connect_failure` | true | Retry when the pool checkout fails to establish a TCP/TLS connection |
| `retry_on_5xx` | false | Retry when the upstream returns a 5xx response (headers only — once the body starts streaming to the client, retries stop) |
| `retry_on_timeout` | false | Retry when the response deadline fires before headers arrive |
| `retry_on_disconnect` | true | Retry when the upstream closes the connection before any response bytes are sent to the client |
| `retry_non_idempotent` | false | Allow retries on POST/PATCH/DELETE (dangerous — can duplicate side effects; default safe methods only) |

**Notes:**

- Retries never fire after any response bytes have been sent to the downstream client.
- `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`.
- 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.

### Validation

`ConfigLoader::Validate()` checks:
Expand Down
92 changes: 90 additions & 2 deletions docs/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,90 @@ The handler receives a const request reference and a completion callback. Call `
- **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.
- **HTTP/2 support** — async routes work identically for H2 streams; the framework binds `SubmitStreamResponse` internally

## Proxy Routes

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.

### Auto-registration from config

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.

```json
{
"upstreams": [
{
"name": "api-backend",
"host": "10.0.1.5",
"port": 8080,
"pool": { "max_connections": 64 },
"proxy": {
"route_prefix": "/api/v1/*rest",
"strip_prefix": true,
"methods": ["GET", "POST", "PUT", "DELETE"]
}
}
]
}
```

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`).

### Programmatic registration

Applications that construct their own config in code can use `HttpServer::Proxy()`:

```cpp
#include "http/http_server.h"

HttpServer server(config);

// Register a proxy route on an already-configured upstream.
// Reuses the proxy fields (methods, strip_prefix, header_rewrite, retry,
// response_timeout_ms) from config.upstreams[i].proxy — only route_prefix
// is overridden by the first argument.
server.Proxy("/api/v1/*rest", "api-backend");

server.Start();
```

`Proxy()` calls must happen before `Start()`. Calling it afterwards — or naming an upstream that is not in the config — raises `std::invalid_argument`.

### HEAD precedence and companion methods

Proxy registrations interact with the HEAD-fallback rule from [Route Matching](#route-matching) as follows:

- **Paired HEAD + GET on the same registration** (both in `methods`): HEAD goes to the proxy, GET goes to the proxy. No fallback.
- **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.
- **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.
- 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.

### Request lifecycle and client abort

Each proxy request is handled by a per-request `ProxyTransaction`:

1. `CHECKOUT_PENDING` — wait for an idle pooled connection (or open a new one, subject to `pool.max_connections`)
2. `SENDING_REQUEST` — serialize and write the HTTP/1.1 request, with header rewriting applied
3. `AWAITING_RESPONSE` — wait for response headers (bounded by `proxy.response_timeout_ms`)
4. `RECEIVING_BODY` — stream the body back to the client
5. `COMPLETE` / `FAILED` — return the connection to the pool or discard it

If the client disconnects mid-request, the framework's async-abort hook calls `ProxyTransaction::Cancel()`, which:

- Sets a `cancelled_` flag guarding every callback entry point
- Signals the pool wait-queue via a shared cancel token so `PoolPartition` can purge the dead entry
- Poisons the upstream connection (`MarkClosing()`) if any bytes have already been written — retrying a partially-sent request on a reused connection is unsafe
- Returns the connection to the pool (or destroys it) without further I/O

### Response timeouts and the async safety cap

`proxy.response_timeout_ms` is the hard deadline for receiving response headers after the request is fully sent. Its valid values are:

- **`>= 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.
- **`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.
- **Other positive values below 1000** — rejected at config load (the 1 s floor matches the timer scan resolution).

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.

## Middleware

```cpp
Expand Down Expand Up @@ -254,8 +338,10 @@ Proxied requests using absolute-form URIs (`GET http://example.com/foo HTTP/1.1`
### Builder Pattern

```cpp
// Chained builder
HttpResponse().Status(200).Header("X-Custom", "value").Json(R"({"ok":true})")
#include "http/http_status.h"

// Chained builder — use HttpStatus::* constants (see include/http/http_status.h)
HttpResponse().Status(HttpStatus::OK).Header("X-Custom", "value").Json(R"({"ok":true})")

// Content type helpers
res.Json(json_string); // Sets Content-Type: application/json
Expand All @@ -278,7 +364,9 @@ res.Body(data, "image/png"); // Custom content type
| `PayloadTooLarge()` | 413 | Body exceeds limit |
| `HeaderTooLarge()` | 431 | Headers exceed limit |
| `InternalError(msg)` | 500 | Server error |
| `BadGateway()` | 502 | Upstream unreachable |
| `ServiceUnavailable()` | 503 | Overloaded |
| `GatewayTimeout()` | 504 | Upstream timeout |
| `HttpVersionNotSupported()` | 505 | Non-1.x HTTP version |

### Header Behavior
Expand Down
73 changes: 72 additions & 1 deletion include/config/server_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,87 @@ struct UpstreamPoolConfig {
bool operator!=(const UpstreamPoolConfig& o) const { return !(*this == o); }
};

struct ProxyHeaderRewriteConfig {
bool set_x_forwarded_for = true; // Append client IP to X-Forwarded-For
bool set_x_forwarded_proto = true; // Set X-Forwarded-Proto
bool set_via_header = true; // Add Via header
bool rewrite_host = true; // Rewrite Host to upstream address

bool operator==(const ProxyHeaderRewriteConfig& o) const {
return set_x_forwarded_for == o.set_x_forwarded_for &&
set_x_forwarded_proto == o.set_x_forwarded_proto &&
set_via_header == o.set_via_header &&
rewrite_host == o.rewrite_host;
}
bool operator!=(const ProxyHeaderRewriteConfig& o) const { return !(*this == o); }
};

struct ProxyRetryConfig {
int max_retries = 0; // 0 = no retries
bool retry_on_connect_failure = true; // Retry when pool checkout connect fails
bool retry_on_5xx = false; // Retry on 5xx response from upstream
bool retry_on_timeout = false; // Retry on response timeout
bool retry_on_disconnect = true; // Retry when upstream closes mid-response
bool retry_non_idempotent = false; // Retry POST/PATCH/DELETE (dangerous)

bool operator==(const ProxyRetryConfig& o) const {
return max_retries == o.max_retries &&
retry_on_connect_failure == o.retry_on_connect_failure &&
retry_on_5xx == o.retry_on_5xx &&
retry_on_timeout == o.retry_on_timeout &&
retry_on_disconnect == o.retry_on_disconnect &&
retry_non_idempotent == o.retry_non_idempotent;
}
bool operator!=(const ProxyRetryConfig& o) const { return !(*this == o); }
};

struct ProxyConfig {
// Response timeout: max time to wait for upstream response headers
// after request is fully sent. 0 = disabled (no deadline). Otherwise
// must be >= 1000 (timer scan has 1s resolution).
int response_timeout_ms = 30000; // 30 seconds

// Route pattern prefix to match (e.g., "/api/users")
// Supports the existing pattern syntax: "/api/:version/users/*path"
std::string route_prefix;

// Strip the route prefix before forwarding to upstream.
// Example: route_prefix="/api/v1", strip_prefix=true
// client: GET /api/v1/users/123 -> upstream: GET /users/123
// When false: upstream sees the full original path.
bool strip_prefix = false;

// Methods to proxy. Empty = all methods.
std::vector<std::string> methods;

// Header rewriting configuration
ProxyHeaderRewriteConfig header_rewrite;

// Retry policy configuration
ProxyRetryConfig retry;

bool operator==(const ProxyConfig& o) const {
return response_timeout_ms == o.response_timeout_ms &&
route_prefix == o.route_prefix &&
strip_prefix == o.strip_prefix &&
methods == o.methods &&
header_rewrite == o.header_rewrite &&
retry == o.retry;
}
bool operator!=(const ProxyConfig& o) const { return !(*this == o); }
};

struct UpstreamConfig {
std::string name;
std::string host;
int port = 80;
UpstreamTlsConfig tls;
UpstreamPoolConfig pool;
ProxyConfig proxy;

bool operator==(const UpstreamConfig& o) const {
return name == o.name && host == o.host && port == o.port &&
tls == o.tls && pool == o.pool;
tls == o.tls && pool == o.pool && proxy == o.proxy;
}
bool operator!=(const UpstreamConfig& o) const { return !(*this == o); }
};
Expand Down
Loading
Loading