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
fix(server): send index sec_type for per-contract index subscriptions (#878)
The per-contract WebSocket subscribe handler built `Contract::stock(symbol)` for every non-option subscribe, even when the client sent `sec_type=INDEX`. The FPSS contract wire encoding carries a load-bearing sec_type byte, so an INDEX subscription went out as a stock-typed contract: wrong instrument, no ticks. Branch on `sec_type` so INDEX builds `Contract::index(symbol)` and everything else defaults to stock, matching the wire default for a root with no security type. The selection is extracted into `non_option_contract` with unit coverage for both the index and stock arms.
Also harden the flatfile date boundary: `validate_date` now rejects filesystem path separators, and `handle_get` / `handle_post` validate `date` before it is interpolated into the temp artefact path, so path safety no longer depends solely on a downstream format validator.
Docs: correct the server README shutdown-token description (it is a 128-bit random hex token, 32 hex chars, not a UUID) and the example banner lines, fix the matching internal comment in the router, and add an Environment variables subsection documenting the three runtime env vars with the off-by-default rate-limit framing intact.
Co-authored-by: preview <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: tools/server/README.md
+16-4Lines changed: 16 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,6 +48,16 @@ The server starts:
48
48
49
49
Every request emits one `INFO` access-log line (method, URI, status, latency) by default. The startup banner prints `thetadatadx-server v<version>`.
50
50
51
+
### Environment variables
52
+
53
+
These runtime knobs are read from the environment, not from CLI flags. Per-IP rate limiting is off by default (matching the terminal it replaces); setting either rate-limit variable opts in. Full descriptions live in [`docs-site/docs/server/index.md`](../../docs-site/docs/server/index.md).
54
+
55
+
| Variable | Default | Description |
56
+
|----------|---------|-------------|
57
+
|`THETADATADX_RATE_LIMIT_PER_SECOND`| off | Opt into per-IP rate limiting at this many requests per second. Setting either rate-limit variable turns the limiter on. |
58
+
|`THETADATADX_RATE_LIMIT_BURST_SIZE`| off | Burst size for the per-IP rate limiter. If only one of the two rate-limit variables is set, the other falls back to `20` req/s / `40` burst. |
59
+
|`THETADATADX_WS_CLIENT_CAPACITY`|`4096`| Per-client WebSocket send-buffer capacity in events. A larger buffer trades memory for more headroom before a slow consumer drops events; invalid or zero values keep the default. |
60
+
51
61
## REST API
52
62
53
63
All registry endpoints are auto-generated into REST routes at startup from `ENDPOINTS`, alongside the hand-written system routes.
@@ -134,7 +144,7 @@ Send JSON commands to manage subscriptions:
134
144
135
145
## Hardening
136
146
137
-
-**`POST /v3/system/shutdown`** requires a random-UUID `X-Shutdown-Token` header printed once to stderr at startup. Token is compared in constant time so response latency does not leak the secret one byte at a time; no env var or CLI flag sets it externally. A route-scoped per-IP limiter caps attempts at roughly 3 per hour.
147
+
-**`POST /v3/system/shutdown`** requires a 128-bit random hex `X-Shutdown-Token` header (32 hex chars) printed once to stderr at startup. Token is compared in constant time so response latency does not leak the secret one byte at a time; no env var or CLI flag sets it externally. A route-scoped per-IP limiter caps attempts at roughly 3 per hour.
138
148
-**Global per-IP rate limit on non-loopback binds** via `tower_governor::GovernorLayer` keyed on `PeerIpKeyExtractor` (peer TCP socket, **not**`X-Forwarded-For`): 20 rps burst 40, rejected as `429` with the canonical error envelope and a `Retry-After` header. Loopback binds (`127.0.0.1`, `::1` — the default) disable the general limiter: every local client shares one peer-IP bucket, so parallel local workloads would throttle each other, which the legacy terminal never did. The shutdown-route limiter stays active on every bind. The server runs without a trusted reverse proxy, so forwarded-header extractors would let an attacker cycle fake IPs.
139
149
-**256 concurrent in-flight requests** — requests past the cap queue on the layer's semaphore (they are not rejected), then queue again on the SDK's tier-sized request semaphore that matches the upstream concurrency cap. Bursts absorb as latency, not errors; see the Concurrency Model section in `docs-site/docs/server/http.md`. Upstream capacity rejections that survive the SDK's retry budget surface as `503` + `Retry-After`, not 500. **64 KiB body limit**, **4 KiB WebSocket `Message::Text` cap**.
140
150
-**`BoundedQuery<32>` extractor** counts `&`-delimited query-string pairs BEFORE `serde_urlencoded` runs, so a `?a=1&b=2&...` flood is rejected at parse time rather than after HashMap rehashing allocates MB+.
@@ -145,9 +155,11 @@ Send JSON commands to manage subscriptions:
145
155
Example — initiating a graceful shutdown from the same machine:
146
156
147
157
```bash
148
-
# Server prints this line once at startup on stderr:
149
-
# thetadatadx-server: X-Shutdown-Token=<UUID>
150
-
curl -X POST -H "X-Shutdown-Token: <UUID>" http://127.0.0.1:25503/v3/system/shutdown
158
+
# Server prints these lines once at startup on stderr (TOKEN is a
159
+
# 128-bit random hex string, 32 hex chars):
160
+
# Shutdown token: <TOKEN>
161
+
# curl -X POST http://127.0.0.1:25503/v3/system/shutdown -H 'X-Shutdown-Token: <TOKEN>'
162
+
curl -X POST -H "X-Shutdown-Token: <TOKEN>" http://127.0.0.1:25503/v3/system/shutdown
0 commit comments