Skip to content

Commit bd1dce0

Browse files
kriszypclaudeKris Zyp
authored
feat(#914): uWebSockets.js HTTP/WebSocket backend (default-off) (#1096)
* feat(http): add uWebSockets.js request adapter (spike, #914) Spike for evaluating uWS as a per-worker HTTP server on the plaintext-UDS path behind symphony (TLS/mTLS/HTTP-2 terminated upstream). Adds: - UwsRequest in Request.ts: a Harper request adapter modeled on BunRequest, sourced from uWS-extracted method/url/headers/body. Real client IP comes from X-Forwarded-For; peerCertificate/authorized are null (terminated upstream). - uwsServer.ts: createUwsServer(), a non-SSL uWS App on a unix socket that bridges each request through httpChain[port] and serializes the Harper response descriptor back onto the uWS HttpResponse. Benchmarks (CPU-µs/request, vs Node http on the same UDS) show uWS holds a ~1.56x efficiency edge with the real Request abstraction in the loop. Not yet wired into getUwsHTTPServer/threadServer.js; uWS is not yet a dependency. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(http): wire uWS UDS server behind HARPER_UWS_UDS flag (spike, #914) Makes the per-worker plaintext-UDS mirror optionally served by uWebSockets.js instead of a Node http server, gated behind the HARPER_UWS_UDS env flag (default off -> no behavior change). When set: - getHTTPServer registers a uwsServeConfigs entry for the UDS path instead of creating the Node udsServer. - makeUwsHandler mirrors the Bun fetchHandler's post-processing (httpChain, unhandled, universalHeaders, Server-Timing, analytics, logging) and returns a Harper response descriptor; createUwsServer serializes it onto the uWS res. - threadServer.listenOnPorts() starts the uWS UDS servers from uwsServeConfigs. - uWebSockets.js added as an optionalDependency (GitHub tag; ABI-locked, no musl build -> CI must build per Node major). Symphony must use sourceAddressHeader 'xForwardedFor' for these sockets (uWS does not parse the PROXY protocol). Fastify status===-1 fallback and response streaming are not wired in this spike. Type-checks clean (tsc --noEmit); not yet exercised against a live booted Harper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(http): null-guard request._nodeRequest in unhandled() (spike, #914) unhandled() (the middleware-chain terminal) set request._nodeRequest.user when an authenticated request hit no route, to hand auth to a Node fallback server. _nodeRequest is null for both BunRequest and UwsRequest, so an authenticated request to an unmatched route threw "Cannot set properties of null (setting 'user')" -> 500. Latent on the Bun path; surfaced by the live uWS-UDS bench. Guard on _nodeRequest: the handoff only applies to the Node fallback path; the Bun/uWS adapters have no Node fallback server. With this, the uWS UDS path returns 404 like Node. Verified on a live booted Harper. * fix(#914): harden uWS UDS adapter for production + add adapter unit test Graduates the uWS-behind-symphony spike toward landing by fixing the correctness issues surfaced in review and adding a regression suite. - Request body corruption (critical): Buffer.from(arrayBuffer) aliased uWS's receive buffer, which is neutered/reused once the onData callback returns while the body is read asynchronously in the handler. Multi-chunk POST/PUT bodies came back truncated/corrupt. Copy the bytes out synchronously via Buffer.from(new Uint8Array(chunk)). - Duplicate request headers were clobbered (headers[k] = v, last wins); accumulate repeats into an array like the Node path. - Empty reason phrase for uncommon status codes ("429 "); derive the status line from node:http STATUS_CODES with an "Unknown" fallback. - Route by method rather than a single app.any(hasBody:true) so bodyless methods dispatch immediately and unknown methods can't stall a connection. - Collapse of streaming/iterable response bodies now bails when the client disconnects (thread the request AbortSignal into uwsBodyToBuffer). - Refresh the stale adapter header comment (wiring is done). Adds unitTests/server/serverHelpers/uwsServer.test.js: exercises GET, bodyless OPTIONS, multi-chunk POST round-trip (guards the aliasing bug), duplicate headers, 404, thrown->500, and reason-phrase serialization over a real UDS. Skips gracefully when the uWebSockets.js optional dep is absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#914): address cross-model review of uWS UDS adapter Follow-up to the adapter hardening, resolving issues surfaced by the cross-model review (Codex + Gemini + Harper-domain adjudication). - WHATWG Response return path (significant): makeUwsHandler mutated response.status/response.body, which throws for a handler that returns a standard Response (read-only accessors) — a divergence from the Node/Bun paths, which build a fresh descriptor. Return a new descriptor instead of mutating the chain's result. - Write-method throttling was dropped on the uWS UDS mirror: the Node UDS path routes non-GET/OPTIONS/HEAD through the request-queue throttle (503 on overflow), the uWS path bypassed it. Restore parity via throttle() so data-modifying bursts shed instead of saturating a worker. - QUERY (and other non-standard body-bearing methods) had their body silently dropped: the per-method routing sent the any() fallback down the bodyless path. Route known-bodyless methods explicitly and treat the fallback as body-bearing (uWS still fires onData(len=0) for bodyless). - Shutdown shim entered the Node keep-alive drain loop and force-exited noisily every shutdown (uWS close() takes no callback): wrap close() to invoke the callback and omit closeIdleConnections so the drain is skipped. - UwsRequestBody now extends Readable, matching the RequestBody/BunRequestBody contract (for-await async iteration + destroy(), not a duck-typed subset). - Tidy: remove abort listener on the stream-error path in uwsBodyToBuffer, drop the unused AbortController param from writeResponse, add the uWebSockets.js optionalDependency to package-lock.json. Adds QUERY-body-routing and 413-over-limit tests; suite now 9 green. The WHATWG-Response, throttle, and shutdown-teardown paths live above the adapter unit boundary — flagged for the integration bench in the PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#914): plaintext uWS-over-HTTP path + full streaming responses Extends the uWS adapter beyond the symphony-UDS mirror toward a fully capable HTTP backend. Plaintext TCP path (HARPER_UWS_HTTP): - createUwsServer now accepts a `port`/`host` (app.listen, SO_REUSEPORT by default) in addition to `socketPath`, so uWS can back a non-secure HTTP TCP port directly — not just the UDS mirror. - getHTTPServer registers a uWS TCP config (and skips the Node server) for non-secure app HTTP ports when HARPER_UWS_HTTP is set; threadServer's start loop is generalized to UDS- or port-keyed configs. - This is the flag used to run the integration suite through uWS: a representative slice passes 45/45 (REST/SQL, data types, dates, arrays, binary/Brotli blob responses byte-exact, Content-Encoding, caching). Streaming responses: - normalizeUwsBody (was uwsBodyToBuffer) now passes Node streams and async-iterables through as a Readable instead of buffering — buffering an SSE/event-stream body never returns. - writeResponse streams a Readable body to uWS with real backpressure (res.write + res.onWritable pause/resume) and omits Content-Length so uWS uses chunked encoding. uWS only flushes headers on the first body write, so text/event-stream responses emit a spec-valid ':\n\n' comment to open the stream immediately (fixes SSE "headers never flushed"). Client abort or a source error destroys the source and stops writing. - Verified: MCP SSE integration test passes 4/4 (headers flushed up front); 3 new adapter unit tests cover SSE, a plain Readable, and a 4 MiB backpressure stream. Suite now 12 green. Remaining: WebSocket upgrade (MQTT-over-WS/subscriptions) — next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#914): WebSocket upgrade support on the uWS path Completes uWS as a full HTTP+WS backend. uWS owns its sockets, so WS can't be delegated to the ws library's WebSocketServer; instead the adapter uses uWS's native app.ws() and bridges each connection to a ws-library-shaped object that Harper's existing websocket chain consumes unchanged. - UwsWebSocket (server/serverHelpers/uwsServer.ts): adapts a uWS WebSocket to the subset of the ws interface Harper uses — send/close/terminate/ping, 'message'/'close' events, readyState, and a _socket shim exposing remoteAddress + backpressure (writableNeedDrain/'drain' via getBufferedAmount + the drain callback). Inbound frames are copied out of uWS's neutered buffer. - createUwsServer accepts a wsHandler; when set it registers app.ws('/*') (capturing the upgrade request's url/headers/ip, IPv4-mapped address normalized) alongside the HTTP routes — both coexist on one port. - onWebSocket (server/http.ts) detects a uWS-backed port and wires the wsHandler (build a WS UwsRequest, run httpChain auth, invoke websocketChains) instead of the Node ws.WebSocketServer + 'upgrade' event. Previously this crashed under HARPER_UWS_HTTP ("server.on is not a function"), failing MQTT component load; also guards a NaN-port config. Validated through the real harness: MQTT-over-WS passes 11/11 (RS256 JWT auth, topic ACLs, pub/sub, $SYS monitoring); SSE 4/4 and HTTP unaffected (24/24 combined). 2 new adapter unit tests (HTTP+WS coexistence on one port; upgrade + text/binary frame round-trip); suite now 14 green. With this, the full integration slice runs over uWS: HTTP, SSE/streaming, and WebSocket subscriptions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#914): address cross-model review of plaintext/streaming/WS uWS work Findings from the Codex+Gemini+domain review of the streaming/WS commits. - Client IP on the direct-TCP path (P1, Codex+sweep): the uWS HTTP handler never captured the peer address, so request.ip was '' and local auth (security/auth.ts AUTHORIZE_LOCAL, request.ip.includes('127.0.0.')) failed — anonymous localhost requests got "Must login". The integration sweep hit this: early-hints/redirector/risk-query (pass on baseline, "pass 0" under the flag). Fix: capture res.getRemoteAddressAsText() for the TCP path (left unset for UDS). AND flip UwsRequest.ip to prefer the real socket address over X-Forwarded-For, so a direct client can't spoof `X-Forwarded-For: 127.0.0.1` to satisfy local auth; XFF is trusted only on the symphony-UDS path (where the socket has no client address). - HEAD body (P2, Codex): uWS has no ServerResponse HEAD guard, so a handler returning a body on HEAD would send it. REST already nulls HEAD bodies; enforce it in writeResponse for any other handler. - WebSocket maxPayload (P2, Codex): the onWebSocket uWS branch didn't forward options.maxPayload, so a configured smaller WS frame limit wasn't enforced (defaulted to 100 MiB). Thread it through as wsMaxPayload. Gemini's headline "Buffer.from(new Uint8Array(message)) aliases uWS memory" blocker is a false positive (same conflation as last review): it COPIES — proven (survives source neutering) and corroborated by MQTT-over-WS 11/11 with async frame processing. Noted, not fixed (out of scope / parity): GraphQL POST reads _nodeRequest which is null on uWS AND Bun (pre-existing non-Node-adapter gap, needs a body-based deserialize); a raw Fastify server registered on a uWS-backed port could collide in SERVERS (low reachability; MCP Fastify passes). Integration sweep: 43/43 pass under HARPER_UWS_HTTP after the IP fix. Adapter unit suite now 16 (adds request.ip + HEAD-suppression tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#914): refresh uwsServer header (TCP+streaming+WS, not UDS-only) * fix(#914): deserialize GraphQL POST body via request.body The GraphQL POST handler read the body from request._nodeRequest, the raw Node IncomingMessage. That is null on the Bun and uWS request adapters, so GraphQL POST 500'd off the Node path. Read through request.body instead — a Readable-compatible body stream on every adapter, matching how REST.ts already deserializes bodies. Verified 24/24 graphql integration tests on both the Node and HARPER_UWS_HTTP paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#914): don't let raw-Fastify listeners collide with the uWS HTTP port Under HARPER_UWS_HTTP the app port is backed by uWebSockets.js and getHTTPServer early-returns a { uws: true } marker before it would have called registerServer(server, port). SERVERS[port] therefore stays empty. If a legacy Fastify-routes app is then deployed, fastifyRoutes registers its raw http.Server via server.http(fastify.server); with the port looking unused, registerServer set SERVERS[port] = fastifyServer and threadServer bound a Node http server competing with uWS on the same TCP port (Codex P2). Mirror the Bun path: divert non-function listeners on a uWS-backed port into the fallback map instead of registerServer(), so nothing lands in SERVERS to double -bind. Renamed bunFallbackServers -> fallbackServers since the map is now shared by both non-Node backends. Request-time delegation to this fallback is not yet wired on the uWS handler, so raw-Fastify routes are unreachable (clean, not a competing bind) under this flag - an accepted limitation of the bench vehicle, noted for a parity follow-up. Verified: components.test.mjs (deploys a Fastify-routes component) 25/25 on both the Node and HARPER_UWS_HTTP paths, with the Fastify registration diverting cleanly and no bind collision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#914): delegate to the Fastify fallback from the uWS HTTP path Completes the raw-Fastify story on HARPER_UWS_HTTP. Previously a legacy custom-function route (server.http(fastify.server)) was diverted to the fallback map to avoid a competing bind, but the uWS handler had no way to reach it, so the route 404'd. Now, when the chain doesn't handle a request (status === -1) and a Fastify instance is registered for the port, the uWS handler delegates via fastify.inject() — its internal router, no socket — mirroring the Bun path, including SSE streaming and the AUTHORIZE_LOCAL pre-auth user forward. - Extracted the shared inject core into injectToFastify() and routed both the Bun and uWS delegation paths through it (strip forged pre-auth header, forward resolved user when no Authorization, payloadAsStream for SSE). - fastifyRoutes now registers its app instance for the http port(s); it only ever registered the http.Server, so neither Bun nor uWS could delegate to legacy routes. Renamed bunFastifyInstances -> fastifyInstances / registerBunFastifyInstance -> registerFastifyInstance (shared, not Bun-only). - UwsRequest exposes rawBody for the inject payload. Verified: fastifyRoutes-test.mjs (GET /testApp/ping -> 'pong' + REST on the same component) passes on BOTH the Node and HARPER_UWS_HTTP paths; under uWS the route is served purely via inject-delegation. graphql 24/24, components 25/25, mcp/sse-listchanged 4/4 under the flag; 16 uWS unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(#914): run the full integration suite under HARPER_UWS_HTTP Adds a run-integration-tests-uws job mirroring the existing Bun variant: the same 6-shard test:integration:all on Node 24, but with HARPER_UWS_HTTP=1 so the plaintext app HTTP port(s) are served by uWebSockets.js. Secure/replication/ops paths keep running on Node, so this gives continuous coverage of the uWS request/streaming/WS/GraphQL/Fastify-fallback path across the whole suite instead of relying on a manual local flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(#914): make the uWS integration job informational (non-blocking) The first full-suite run under HARPER_UWS_HTTP surfaced two known uWS-path gaps (Bun and Node are green on the same tests): - static-file serving via `send` never flushes headers on the uWS response (client HeadersTimeout) — the deploy/static-access tests hang; - multiple Set-Cookie headers collapse to one (the WHATWG Headers comma-join limitation Harper-on-Bun already skips). Neither is a regression from the Fastify-delegation work. Mark the job continue-on-error so it reports the per-shard uWS signal without gating merges; remove once the gaps are closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#914): serve static files (send SendStream) on the uWS HTTP path Static handlers return a `send` SendStream, which only begins work when piped to a Node ServerResponse and writes its own headers there. The uWS path has no such object: it treated the stream as a plain Readable and attached .on('data') (which never starts a SendStream), and uWS only flushes status/headers on the first body write — so static responses hung and the client saw a HeadersTimeout. This is why every deploy+access integration test (deployed apps serve a static site) timed out under the flag. Pipe the SendStream into a Writable shim that captures the headers it writes (setHeader/writeHead) onto the response Headers and buffers the file, mirroring the Bun fetchHandler's SendStream path (incl. finished:false so on-finished doesn't tear down early). Gated on handlesHeaders, which only static.ts sets, so real streaming/SSE bodies keep streaming through normalizeUwsBody. Verified: deploy/deploy-from-source.test.ts (deploys an app with a web/ static site, polls the static index, asserts the served HTML) now passes 4/4 under HARPER_UWS_HTTP — previously deploy+access both hung ~300s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#914): preserve multiple Set-Cookie headers on the uWS HTTP path A WHATWG Headers comma-joins Set-Cookie when iterated, which merges multiple cookies into one and corrupts values containing commas (e.g. `expires=` dates). The uWS response path iterated the headers directly (and writeResponse converted a WHATWG Headers via `new Headers()`, comma-joining before serialization), so a response setting N cookies reached the client as 1. - writeHeaders now emits Set-Cookie individually via getSetCookie() when present (WHATWG), skipping the joined entry; a Harper Headers stores them as an array, already handled by the array branch. - writeResponse keeps an existing Headers-like object (Harper or WHATWG) as-is instead of round-tripping a WHATWG Headers through `new Headers()` (which would comma-join before writeHeaders could split it), wrapping only plain objects. - the Fastify-delegation path keeps Set-Cookie multi-valued instead of comma- joining inject()'s array. This is the multi-Set-Cookie limitation Harper-on-Bun documents and skips; uWS now handles it correctly. Verified: headers.test.mjs 2/2 under HARPER_UWS_HTTP (was 0/2); graphql/components/mcp-sse/deploy-from-source/fastifyRoutes all green under the flag; 16 uWS unit tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(#914): gate on the uWS integration job (full suite now green) The full test:integration:all suite passes on all 6 shards under HARPER_UWS_HTTP (CI run 28724670219) now that the static-`send` and multiple-Set-Cookie gaps are fixed, so the job no longer needs continue-on-error — make it a required check alongside the Node and Bun variants. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#914): address cross-model review of the uWS Fastify/static/header work Cross-model review (Codex + Gemini + Harper-domain adjudication) of the new uWS work. Both outside-model legs led with false positives (request.headers.asObject 'undefined' → auth bypass, and Set-Cookie comma-coercion) — both refuted: headers is a RequestHeaders with a real .asObject used across the REST path, and the uWS Headers is Harper's Map-based class that preserves Set-Cookie arrays. The INTERNAL_USER_HEADER pre-auth forward was probed and is spoof-safe (client-supplied header is stripped before the user is re-added). Real items addressed: - bufferSendStream no longer swallows send's status: capture statusCode / writeHead status and return it, so a 304 (conditional GET) or 206/416 (Range) is honored instead of flattened to 200. (End-to-end 304/Range is still gated upstream by send not reading Harper's RequestHeaders — a pre-existing limitation on all backends incl. Node, verified by probe; left as a separate follow-up.) - avoid re-copying already-Buffer chunks when draining a delegated Fastify response. - document the lowercased-'authorization' contract in injectToFastify. - refresh the fallback-divert comment: request-time delegation IS now wired, and the { uws: true } marker is guaranteed set by the getServer(port) call above. Regression under HARPER_UWS_HTTP: deploy-from-source 4/4 (static), headers 2/2 (Set-Cookie), fastifyRoutes 2/2 (delegation), 16 uWS unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#914): stream uWS request bodies + address review comments Feed the uWS request body into a push-based Readable and dispatch the handler on headers instead of buffering the whole body and dispatching on the last chunk. streamToBuffer (contentTypes.ts) already owns concatenation and the HTTP_MAXREQUESTBODYSIZE limit and is the entry point for the upcoming streaming deserializers, so the adapter no longer concatenates (drops the O(n^2) Buffer.concat) or enforces its own body limit; maxBodyBytes is demoted to a coarse socket-level DoS ceiling since uWS offers no inbound backpressure. The Fastify-delegation path passes the body stream to inject() (light-my-request consumes it), so rawBody is gone. Also address review feedback: - use when() so a synchronous handler stays synchronous (no extra promise) - rename logBunRequest -> logHttpRequest (shared Bun/uWS path) - correct the stale "WebSocket upgrades are not yet wired" comment - reword the SPIKE/spike comments now that this is graduating - document uWebSockets.js in dependencies.md Adds a test asserting the handler is dispatched before the request body ends (proves streaming, not full buffering). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(uws): guard 413 write against completed response + test XFF spoofing defense (#914) Address cb1kenobi PR review: - Track responseCompleted in onRequest and guard all three response-write sites (handler result, error, 413). The handler can respond (or start streaming) without consuming the body; a later over-limit 413 would then write to an already-completed uWS response and abort the process. - Add unit tests for request.ip trust boundary: a spoofed X-Forwarded-For must not override the authoritative TCP peer address, while the UDS path (no socket peer) still honors the trusted proxy's XFF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: sync package-lock with merged package.json (prettier 3.9.5, globals 17.7.0, aws-sdk lib-storage 3.1076.0) The npm-merge-driver left the lock resolved to the branch's older versions while package.json took main's bumps, breaking npm ci. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: reformat uwsServer.ts per prettier 3.9.5 (trailing comment placement) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Kris Zyp <kris@harperdb.io>
1 parent ba2a484 commit bd1dce0

12 files changed

Lines changed: 1496 additions & 65 deletions

File tree

.github/workflows/integration-tests.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,57 @@ jobs:
248248
path: /tmp/harper-integration-test-logs/
249249
retention-days: 3
250250
if-no-files-found: ignore
251+
252+
run-integration-tests-uws:
253+
name: Integration Tests ${{matrix.shard}}/6 (uWS HTTP)
254+
runs-on: ubuntu-latest
255+
needs: [build]
256+
strategy:
257+
fail-fast: false
258+
matrix:
259+
shard: [1, 2, 3, 4, 5, 6]
260+
261+
steps:
262+
- name: Checkout code
263+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
264+
265+
- name: Setup Node.js
266+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
267+
with:
268+
node-version: 24
269+
package-manager-cache: false
270+
271+
- name: Download build artifacts
272+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
273+
with:
274+
name: harper-build-artifacts-node-24
275+
276+
- name: Relink bin scripts
277+
run: npm install --ignore-scripts
278+
279+
- name: Install harperdb@4 for legacy tests
280+
run: |
281+
mkdir -p /tmp/harperdb-legacy
282+
npm install --ignore-scripts --prefix /tmp/harperdb-legacy harperdb@4
283+
284+
# Serve the plaintext app HTTP port(s) through uWebSockets.js instead of the Node http server
285+
# (#914). Only non-secure, non-operations TCP ports are affected; secure/replication/ops paths
286+
# keep running on Node, so this exercises the uWS request/streaming/WS/GraphQL/Fastify-fallback
287+
# path against the full integration suite.
288+
- name: Run Integration Test Shard ${{ matrix.shard }}
289+
env:
290+
HARPER_INTEGRATION_TEST_LOG_DIR: /tmp/harper-integration-test-logs
291+
HARPER_LEGACY_VERSION_PATH: /tmp/harperdb-legacy/node_modules/harperdb
292+
HARPER_UWS_HTTP: 1
293+
HARPER_INTEGRATION_TEST_INSTALL_SCRIPT: dist/bin/harper.js
294+
run: |
295+
npm run test:integration:all -- --shard=${{ matrix.shard }}/6
296+
297+
- name: Upload Harper server logs
298+
if: failure()
299+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
300+
with:
301+
name: harper-server-logs-uws-shard-${{ matrix.shard }}
302+
path: /tmp/harper-integration-test-logs/
303+
retention-days: 3
304+
if-no-files-found: ignore

dependencies.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ Generally, dependencies are added by simply adding them to the dependencies list
5757
- Can be deferred: Yes, this only loaded when secure sand-boxing is enabled and modules are loaded.
5858
- Eventual removal: Same as above
5959

60+
## uWebSockets.js
61+
62+
- Need for usage: Optional high-performance HTTP/WebSocket backend (#914), gated default-off behind HARPER_UWS_UDS (plaintext UDS behind symphony) / HARPER_UWS_HTTP (direct plaintext TCP). Loaded lazily only when a flag is set.
63+
- Size/memory cost: Prebuilt native V8 addon (~1MB per platform binary; the git dependency clones the repo with all platform binaries).
64+
- Security: Actively maintained C++ HTTP server; no npm advisory registry entry (installed as a git dependency, not from npm).
65+
- Binary compilation: Yes — ABI-locked, platform-specific prebuilt `.node` binaries committed in the repo. No musl/Alpine (glibc only). Installed via `github:uNetworking/uWebSockets.js#<tag>`, so it needs git at install time; being an optionalDependency, install tolerates its absence (missing git, unsupported platform) and Harper runs without it.
66+
- Overlap: Overlaps `ws` (WebSockets) and the Node/Bun HTTP paths; this is an alternative transport, not an addition.
67+
- Eventual removal: Kept as long as it demonstrates a meaningful throughput/latency win over the Node path; the flags let it be dropped without touching the default path.
68+
6069
## ws
6170

6271
- Need for usage: We need to support WebSockets

package-lock.json

Lines changed: 11 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@
252252
"optionalDependencies": {
253253
"bufferutil": "^4.0.9",
254254
"segfault-handler": "^1.3.0",
255+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.68.0",
255256
"utf-8-validate": "^5.0.10"
256257
},
257258
"peerDependencies": {

server/fastifyRoutes.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import getHeaderTimeoutConfig from './fastifyRoutes/helpers/getHeaderTimeoutConf
1717
import { serverErrorHandler } from '../server/serverHelpers/serverHandlers.js';
1818
import { registerContentHandlers } from '../server/serverHelpers/contentTypes.ts';
1919
import { server } from './Server.ts';
20+
import { registerFastifyInstance } from './http.ts';
2021

2122
let fastifyServer;
2223
const routeFolders = new Set();
@@ -44,7 +45,15 @@ export function handleApplication(scope: import('../components/Scope.ts').Scope)
4445
}
4546
if (!fastifyServer) {
4647
fastifyServer = buildServer(isHttps);
47-
server.http((await fastifyServer).server);
48+
const built = await fastifyServer;
49+
server.http(built.server);
50+
// Register the Fastify app for the http port(s). On plain Node the http.Server above
51+
// cascades unhandled requests via the 'unhandled' event, but the Bun and uWS backends bind
52+
// those ports themselves and have no Node http.Server to hand off to — they delegate to
53+
// these legacy routes via inject() and look the instance up by port (see injectToFastify).
54+
for (const port of [env.get(CONFIG_PARAMS.HTTP_PORT), env.get(CONFIG_PARAMS.HTTP_SECUREPORT)]) {
55+
if (port != null) registerFastifyInstance(port, built);
56+
}
4857
}
4958
const resolvedServer = await fastifyServer;
5059
const routeFolder = dirname(entry.absolutePath);

server/graphqlQuerying.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,9 @@ async function graphqlQueryingHandler(request: Request) {
559559
}
560560
case 'POST': {
561561
const requestBodyDeserialize = getDeserializer(request.headers.get('content-type'), true);
562-
// @ts-expect-error: _nodeRequest is a custom property on request and is the IncomingMessage with is a Readable
563-
const requestParams = await requestBodyDeserialize(request._nodeRequest);
562+
// Read the body through request.body (as REST.ts does): it is a Readable-compatible
563+
// body stream on every adapter, whereas _nodeRequest is null on the Bun/uWS adapters.
564+
const requestParams = await requestBodyDeserialize(request.body as any);
564565
assertRequestParams(requestParams);
565566
return resolver(requestParams, request);
566567
}

0 commit comments

Comments
 (0)