Skip to content

Commit e93a18d

Browse files
kriszypclaude
andcommitted
docs(http): address review feedback on connectionInfo docs
- Add <VersionBadge type="changed" version="v5.2.0" /> to the protocol row, matching the file's convention for documenting behavior changes to existing surface (Gemini + Ethan Arrowood, PR #614). - Fix the peerCertificate link: certificate-verification.md is about CRL/OCSP revocation and never mentions peerCertificate or the authenticated-user flow (Ethan Arrowood). - Tighten the connectionInfo undefined/runtime caveat: it's undefined whenever no TLS-bearing TLV decoded (not just "no v2 header"), and it's Node-runtime only — Bun and uWebSockets never populate it (Ethan Arrowood). - Flag alpn/authority/ja3/ja4 as string-decoded from raw, unvalidated TLV payloads with no grammar/length validation, matching the harper source docblock (server/serverHelpers/proxyProtocol.ts) that introduces these fields, and warn against placing them unsanitized in headers, logs, or cache keys. Switch the JA4 example's denial from a generic `throw new Error` (reads as a 500) to the repo's established `new Response(..., { status: 403 })` policy-denial convention (see reference/resources/resource-api.md). - Correct the verified-mTLS gating guidance: point at `request.authorized` (the chain-gated boolean the proxy layer populates with TLSSocket semantics — proxyProtocol.ts's own comment: "For the standard 'verified mTLS client' gate use request.authorized") instead of `request.peerCertificate` + "the authenticated user". Note it's Node-runtime only, same as connectionInfo. - Fix a real accuracy bug surfaced by independent review: `clientCertChain` was documented as "verified," but proxyProtocol.ts attaches the chain whenever the client presented a certificate, independent of the proxy's verify result — `tls.verified` and `clientCertChain` presence are two different signals, and treating chain presence as proof of verification accepts unverified clients. Corrected the property table, the ConnectionInfo interface comment, and the trust paragraph to say so explicitly. - Narrow the `protocol` row to Node-runtime SSL-TLV behavior; Bun and uWebSockets derive the scheme from X-Forwarded-Proto/the listener's secure flag instead. - Scope PROXY v2 decoding to Harper Fabric's proxy-protocol UDS mirrors specifically, not ordinary http.port/securePort listeners. The static-vs-instance-method suggestion on the get() example was deliberately left as-is: the file's other Resource example (line 148) also uses an instance method, and Ethan confirmed no such rule exists in AGENTS.md/CONTRIBUTING.md. Refs #614 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9271967 commit e93a18d

1 file changed

Lines changed: 21 additions & 21 deletions

File tree

reference/http/api.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,53 +78,53 @@ A `Request` object is passed to HTTP middleware handlers and direct static REST
7878

7979
### Properties
8080

81-
| Property | Type | Description |
82-
| ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
83-
| `url` | string | The request target (path + query string), e.g. `/path?query=string` |
84-
| `method` | string | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, etc. |
85-
| `headers` | [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) | Request headers |
86-
| `pathname` | string | Path portion of the URL, without query string |
87-
| `protocol` | string | `http` or `https`. Behind a TLS-terminating proxy that forwards PROXY protocol v2, this reflects the client's original scheme even though the proxy-to-Harper hop is plaintext. |
88-
| `data` | any | Deserialized body, based on `Content-Type` header |
89-
| `ip` | string | Remote IP address of the client (or last proxy) |
90-
| `host` | string | Host from the request headers |
91-
| `session` | object | Current cookie-based session (a `Table` record instance). Update with `request.session.update({ key: value })`. A cookie is set automatically the first time a session is updated or a login occurs. |
92-
| `connectionInfo` | object \| undefined | TLS facts a fronting proxy forwarded over PROXY protocol v2 — negotiated ALPN, SNI, TLS version/cipher, the client's JA3/JA4 fingerprint, and the verified mTLS client certificate chain. `undefined` for a direct connection or one without a v2 header. See [Connection info](#connection-info). <VersionBadge version="v5.2.0" /> |
81+
| Property | Type | Description |
82+
| ---------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83+
| `url` | string | The request target (path + query string), e.g. `/path?query=string` |
84+
| `method` | string | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, etc. |
85+
| `headers` | [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) | Request headers |
86+
| `pathname` | string | Path portion of the URL, without query string |
87+
| `protocol` | string | `http` or `https`. On the Node runtime, behind a TLS-terminating proxy that forwards the PROXY v2 SSL TLV, this reflects the client's original scheme even though the proxy-to-Harper hop is plaintext; a proxy forwarding only address info (no SSL TLV) still reads as `http`. Bun and uWebSockets derive this from `X-Forwarded-Proto`/the listener's secure flag instead, independent of the SSL TLV. <VersionBadge type="changed" version="v5.2.0" /> |
88+
| `data` | any | Deserialized body, based on `Content-Type` header |
89+
| `ip` | string | Remote IP address of the client (or last proxy) |
90+
| `host` | string | Host from the request headers |
91+
| `session` | object | Current cookie-based session (a `Table` record instance). Update with `request.session.update({ key: value })`. A cookie is set automatically the first time a session is updated or a login occurs. |
92+
| `connectionInfo` | object \| undefined | TLS facts a fronting proxy forwarded over PROXY protocol v2 — negotiated ALPN, SNI, TLS version/cipher, the client's JA3/JA4 fingerprint, and the client's mTLS certificate chain (presented, not necessarily verified — see below). `undefined` for a direct connection or one without a v2 header. See [Connection info](#connection-info). <VersionBadge version="v5.2.0" /> |
9393

9494
### Connection info
9595

9696
<VersionBadge version="v5.2.0" />
9797

98-
When Harper runs behind a TLS-terminating proxy (for example, Harper Fabric's Symphony edge), the TLS handshake happens at the proxy, so facts about the client's TLS connection — its fingerprint, negotiated protocol, and any client certificate — are otherwise invisible to your application. When the proxy is configured to forward them over PROXY protocol v2, Harper decodes them and exposes them on `request.connectionInfo`:
98+
When Harper runs behind a TLS-terminating proxy (for example, Harper Fabric's Symphony edge), the TLS handshake happens at the proxy, so facts about the client's TLS connection — its fingerprint, negotiated protocol, and any client certificate — are otherwise invisible to your application. Harper Fabric's proxy-protocol Unix domain socket mirrors decode this from a forwarded PROXY protocol v2 header and expose it on `request.connectionInfo`; a PROXY v2 header sent to an ordinary `http.port`/`securePort` listener isn't decoded (and isn't valid HTTP, so the connection fails).
9999

100100
```ts
101101
interface ConnectionInfo {
102-
alpn?: string; // negotiated ALPN protocol, e.g. "h2"
103-
authority?: string; // SNI hostname from the ClientHello
102+
alpn?: string; // negotiated ALPN protocol, e.g. "h2" — raw, unvalidated
103+
authority?: string; // SNI hostname from the ClientHello — raw, unvalidated
104104
tls?: {
105105
version?: string; // e.g. "TLSv1.3"
106106
cipher?: string; // negotiated cipher suite
107107
verified?: boolean; // a client certificate was presented and the proxy verified it
108108
};
109-
ja3?: string; // JA3 fingerprint (32-char MD5 hex)
110-
ja4?: string; // JA4 fingerprint
111-
clientCertChain?: Buffer[]; // verified mTLS client certificate chain (DER, leaf first)
109+
ja3?: string; // JA3 fingerprint (32-char MD5 hex) — raw, unvalidated
110+
ja4?: string; // JA4 fingerprint — raw, unvalidated
111+
clientCertChain?: Buffer[]; // client certificate chain (DER, leaf first) — presented, not necessarily verified; check tls.verified
112112
}
113113
```
114114

115-
`request.connectionInfo` is `undefined` unless a PROXY v2 header was decoded; each field is present only when the proxy actually forwarded it. Reading the client's JA4 fingerprint — e.g. to make a bot or abuse decision — is simply:
115+
`request.connectionInfo` is `undefined` unless a PROXY v2 header carrying TLS facts was decoded; each field is present only when the proxy actually forwarded it. On the Bun and uWebSockets runtimes it is always `undefined`. `alpn`, `authority`, `ja3`, and `ja4` are string-decoded from the TLV payload with no grammar or length validation beyond the header's own bounds, so validate them before placing them in response headers, logs, cache keys, or other security-sensitive contexts. Reading the client's JA4 fingerprint — e.g. to make a bot or abuse decision — is simply:
116116

117117
```javascript
118118
class Origin {
119119
get(request) {
120120
const ja4 = request.connectionInfo?.ja4;
121-
if (ja4 && abuseList.has(ja4)) throw new Error('blocked');
121+
if (ja4 && abuseList.has(ja4)) return new Response('blocked', { status: 403 });
122122
return fetch(request);
123123
}
124124
}
125125
```
126126
127-
**Trust and mTLS.** `connectionInfo` is populated only from the trusted proxy-protocol channel (Harper Fabric's per-worker Unix domain socket, writable only by the local proxy) — never from a request header, so a client cannot forge it. `tls.verified` reflects the proxy's verify bit: it can be `true` even when `clientCertChain` is absent (a proxy may verify a certificate but omit an oversized chain). For gating on a verified mTLS client identity, use the parsed [`request.peerCertificate`](../security/certificate-verification.md) and the connection's authenticated user, not `connectionInfo.tls.verified` alone.
127+
**Trust and mTLS.** `connectionInfo` is populated only from the trusted proxy-protocol channel (Harper Fabric's per-worker Unix domain socket, writable only by the local proxy) — never from a request header, so the channel itself cannot be forged by a client (its _contents_ are client-controlled, per above). `clientCertChain` and `tls.verified` are independent signals, not one another's proof: a chain is attached whenever the client presented a certificate, whether or not the proxy verified it, and `tls.verified` can be `true` while `clientCertChain` is absent (a proxy may verify a certificate but omit an oversized chain). Treating chain presence alone as proof of a verified client accepts unverified certificates. For gating on a verified mTLS client, use `request.authorized` — a chain-gated boolean the proxy layer populates with the same semantics as a directly-terminated TLS connection — rather than `connectionInfo.tls.verified` or `clientCertChain` presence alone. Like `connectionInfo`, this is Node-runtime only: `request.authorized` is always `undefined` on Bun and uWebSockets.
128128
129129
### Methods
130130

0 commit comments

Comments
 (0)