Skip to content

fix(flows): survive malformed requests in the compliance server#55

Open
0xKarm wants to merge 1 commit into
tempoxyz:mainfrom
0xKarm:fix/compliance-server-crash-safety
Open

fix(flows): survive malformed requests in the compliance server#55
0xKarm wants to merge 1 commit into
tempoxyz:mainfrom
0xKarm:fix/compliance-server-crash-safety

Conversation

@0xKarm

@0xKarm 0xKarm commented Jul 14, 2026

Copy link
Copy Markdown

compliance-server: one malformed request can kill the whole flow run

Repro

Against the server as it ships today:

$ curl -X POST -d 'not json' http://127.0.0.1:43999/rpc
curl: (52) Empty reply from server

$ curl http://127.0.0.1:43999/free      # server is gone
curl: (7) Failed to connect

JSON.parse throws inside the async request listener, nothing catches it, and
Node's default unhandled-rejections=throw terminates the process. Because the
flow runner starts this server once and reuses it for every adapter, the
crash cascades: every remaining flow case for every remaining adapter fails
with connection errors, and the eventual report points nowhere near the actual
cause.

The listener had several such landmines, all reachable with a single request:

Input Throwing call
invalid JSON body on /rpc JSON.parse(raw)
JSON-RPC credential without challenge credential.challenge.id
garbage Authorization on a digest_binding route Credential.deserialize(...)
garbage Authorization on a bind_request_resource route Credential.deserialize(...)
malformed Host header new URL(...)
client aborting a POST mid-body unheard 'error' event on req (plus a forever-pending readBody promise)

Notably the invalid_challenge_id branch does guard its
Challenge.deserialize with try/catch — the unguarded copies elsewhere look
like oversights rather than intent.

Fix

Three layers, smallest change that makes each input class behave sensibly:

  1. Error boundary. The listener body moved to handleRequest(); the
    exported listener wraps it in try/catch and answers 400 with
    {"ok": false, "error": "malformed request"} (only if headers weren't
    already sent), logging the error to stderr. The process no longer dies.
  2. readBody now rejects on req.on('error'), so an aborted upload
    surfaces as a caught error instead of a hung response plus process crash.
  3. Credential parsing on the digest/resource-binding paths goes through a
    deserializeCredentialSafely() helper. A garbage Authorization header is
    semantically "no valid credential", so those routes now respond with the
    protocol-correct 402 + fresh challenge rather than a 400 (or, before this
    patch, a dead server).

Verified

Before (main): POST 'not json' -> /rpc → connection dropped, process exits,
subsequent /free unreachable.

After (this branch), same server instance across all probes:

free: 200
bad rpc json: 400
rpc missing challenge: 400
garbage auth digest-binding: 402
garbage auth bind-resource: 402
free after all probes: 200

Regression: python3 scripts/flow_runner.py --adapter typescript
PASSED: 33 passed, 33 total (golden untouched). npx tsc --noEmit reports
the identical pre-existing error set as main (23 = 23, all environmental
@types/node resolution noise; nothing new from this change).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant