Skip to content

fix(routes): match on pathname so ?token= query strings don't bypass routing#34

Merged
lis186 merged 4 commits into
mainfrom
fix/api-route-query-string-bypass
May 25, 2026
Merged

fix(routes): match on pathname so ?token= query strings don't bypass routing#34
lis186 merged 4 commits into
mainfrom
fix/api-route-query-string-bypass

Conversation

@lis186
Copy link
Copy Markdown
Owner

@lis186 lis186 commented May 25, 2026

Summary

Route handlers compared clientReq.url literally against '/_api/<route>', so AUTH_TOKEN-mode requests like /_api/entries?token=… missed every matcher and fell through to the upstream proxy — producing 404 noise, broken entry/token detail lookups, and dashboard pollution.

Split off the pathname before comparing in hub.js and every file under server/routes/. New regression test covers both the literal-equality cases (/_api/entries, /_api/settings, /_api/hub/*, /_api/costs/*, /_api/intercept/*, /_events) and the regex-match cases (/_api/entry/<id>, /_api/tokens/<id>).

Why now

This is a prerequisite for the upcoming two-domain auth migration ([design doc to follow as docs/ PR]). The new dispatcher classifies requests by path, and that classification has to survive any query string the client appends.

Test plan

  • Existing 506-test suite passes
  • New `test/route-query-string.test.js` (3 cases) covers query-string fall-through regression
  • Manual: `AUTH_TOKEN=x npx ccxray claude` → dashboard `/_api/entries?token=x` returns entries (not proxy 404)

🤖 Generated with Claude Code

lis186 and others added 4 commits May 22, 2026 20:01
Codex 0.133+ pings ~10 platform endpoints on startup (plugin lists,
connector directory, app metadata, usage). They're internal RPC,
not conversation data — but without filtering they create ~30 entries
in the dashboard timeline before the first user prompt.

The fix:
- Extend isChatGPTCodexPath to include /v1/ps/plugins/* (was falling
  through to the Anthropic upstream and getting tagged provider:anthropic,
  agent:claude — a classification bug, not just noise).
- Add isCodexPlatformNoisePath predicate covering plugins, ps/plugins,
  connectors, codex/apps, codex/usage.
- In the request handler, short-circuit noise paths with skipEntry:true
  (same pattern as isQuotaCheck). Requests still get proxied so codex
  doesn't break; only entry creation and per-session counter bumps are
  skipped.

Telemetry endpoint (/v1/codex/analytics-events/events) is intentionally
kept visible — a follow-up will parse turn metadata (model, tokens,
duration) out of it so codex entries become comparable to claude ones.

Tested manually: 7 noise paths fire → 0 entries; telemetry POST → 1
entry. New unit tests cover the predicate and the /v1/ps/plugins
classification fix. New e2e test asserts entries stay empty for noise
paths while the telemetry endpoint still records.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… aren't truncated

When ccxray (or its agent child) exits while a WebSocket upgrade is in
flight, the storage writes for the WS entry race process.exit and end
up as 0-byte _req.json/_res.json. The entry shows up in index.ndjson
but its actual transport metadata is unrecoverable.

Root cause: recordWebSocketEntry calls config.storage.write(...) and
just .catch()s the promise — fire-and-forget. spawnStandaloneAgent's
onExit calls process.exit(code) immediately after server.close(), and
fs.writeFile hasn't flushed yet.

The fix is a three-piece drain on shutdown:

1. server/storage/index.js wraps every adapter's async writes in an
   in-flight Set and exposes drain() — used after WS finalize so any
   queued fs.writeFile completes before process.exit.

2. server/ws-proxy.js tracks active sessions (so we can force-finalize
   stragglers when the agent exits before its WS close event fires)
   and pending recordWebSocketEntry promises. drainWebSocketProxy()
   force-finalizes any open pairs and awaits all in-flight entry
   writes. Idle-timeout and queue-cap semantics from f695539 are
   preserved verbatim.

3. server/index.js adds gracefulExit(code) that awaits
   drainWebSocketProxy then storage.drain, bounded by a 5s safety
   timeout. Wired into spawnStandaloneAgent's onExit, hub SIGTERM/SIGINT
   cleanup, hub idle shutdown (via hub.setOnShutdown), and standalone
   non-agent mode SIGTERM/SIGINT.

New e2e test reproduces the race: opens a WS through the proxy, sends a
frame, SIGTERMs the proxy, verifies _req.json + _res.json contain valid
transport-only JSON with non-zero byte counts. Without gracefulExit the
files would be 0-byte; with it they're complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anism

Both bugs that blocked Beta-readiness landed:
- codex platform noise no longer pollutes the dashboard
- WS entries survive proxy shutdown

README gains a "Codex support (Beta)" section between Quick Start and
Features. Calls out what the WS transport actually captures (frame and
byte counts, not decoded content), what's still missing (token / model
extraction from telemetry — Bonus follow-up), and the tunable env vars.

CLAUDE.md gets two new bullets:
- the codex platform path filter and where to find it
- the gracefulExit / drainWebSocketProxy / storage.drain pipeline
The ws-proxy.js and server/storage/ module rows pick up one-line notes
about their shutdown contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…routing

Route handlers compared clientReq.url literally against '/_api/<route>',
so AUTH_TOKEN-mode requests like /_api/entries?token=… missed every
matcher and fell through to the upstream proxy — producing 404 noise,
broken entry/token detail lookups, and dashboard pollution.

Split off the pathname before comparing in hub.js + every route file
under server/routes/. Add test/route-query-string.test.js as a
regression net covering the literal-equality and regex-match cases.

This is a prerequisite for the upcoming two-domain auth migration
(reason/260525-0055-ccxray-auth-design/): the new dispatcher needs to
classify requests by path, and that classification has to survive any
query string the client appends.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@lis186 lis186 merged commit e899562 into main May 25, 2026
2 checks passed
@lis186 lis186 deleted the fix/api-route-query-string-bypass branch May 25, 2026 05:13
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