feat: universal proxy (new approach)#837
Conversation
Semgrep Security ScanNo security issues found. |
PR Metrics
Updated Thu, 14 May 2026 19:35:22 GMT · run #1499 |
…from ending up in server logs
- inject DnsLookup into url-validation, proxy, and preview routes - inject SearchExaClient into search route via createApp deps - inject email senders into createAuth to capture OTPs per test - avoids mock.module leaking across test files
- store test PGlite instance on globalThis so it survives module reloads - close on process beforeExit instead of afterAll, so reruns reuse one instance rather than reinitializing (and re-running migrations) each time
|
Preview environment deployed 🚀
Stack: Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — |
- Eliminates a sign-in race where the bearer is returned before the session row is visible, which surfaced as a confusing 401 on the first authenticated request in tests.
- move cleanup into bun:test afterAll so it runs inside the test runner lifecycle, before Bun terminates worker threads - drop the globalThis-shared PGlite instance; one instance per process is sufficient now that cleanup is reliable
…hrough)
- observability: strip PostHog (Pino + OTel only); add ProxyErrorType
union and `trace.getActiveSpan().setAttributes(...)` for proxy.* attrs.
Wire real status / duration_ms / bytes_in / bytes_out from the response
and capStream (fixes bot MEDIUM findings: hardcoded status, zero
duration/bytes). Tag error_type on every failure path.
- content-encoding passthrough: pass `decompress: false` to Bun fetch,
remove `content-encoding` from DROPPED_RESPONSE_HEADERS. Browser now
decodes; the proxy holds compressed bytes only. Comment near 10MB cap
documents the zip-bomb risk shift to the caller.
- streaming: capStream returns { stream, bytesRead() } and fires a
single `onComplete(bytes)` so observability emits exactly once after
stream drain (graceful, cap-hit, idle, error, or cancel).
For streaming POSTs (no follow-redirects), bytesIn was captured at the moment fetchFn() returned response headers — before the request body had fully drained through capStream. Replace the captured number with a late-read getter so the observability emission (which fires from the response stream's onComplete) sees the final upload byte count.
- routes.ts: replace unbounded arrayBuffer() in buffered-body path with bounded streaming accumulator that early-terminates at maxBodyBytes. Closes a DoS vector where a chunked upload (no Content-Length) would materialise the full body in memory before the 413 check fired. - routes.ts: document Bun's decompress:false semantics (Bun >=1.3 keeps content-encoding on the Response even after auto-decode, so the cast is load-bearing for correctness — not just a perf flag). - routes.test.ts: add streaming-POST bytes_in regression test exercising the validator's deferred-getter fix (commit 7d5ddada) directly. - routes.test.ts: add chunked-upload DoS test proving early-termination.
- Remove backend/scripts/dev.sh (Chris flagged out-of-scope) - Revert backend/src/db/client.ts dev rewrite - Revert playwright.config.ts tweak - Revert powersync localhost defaults in settings.ts (closes HIGH bot finding) These items moved to italomenezes/cjroth-proxy-dev-env (stacked PR).
# Conflicts: # backend/.env.example # backend/eslint.config.js # backend/src/auth/auth.ts # backend/src/matchers.d.ts # backend/src/pro/link-preview.ts # backend/src/proxy/routes.ts # backend/src/utils/streaming.ts # backend/src/utils/url-validation.ts # powersync-service/docker-compose.yml
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 94342ac. Configure here.
531b41c to
a9c2d2b
Compare
| } | ||
|
|
||
| /** | ||
| * Memoized `proxyFetch` keyed on `cloudUrl`. Construction is cheap, but creating |
There was a problem hiding this comment.
I think we should move this into React context instead of just let vars
There was a problem hiding this comment.
Also, what is the use case for having multiple cloud URLs at the same time? I feel this could be simplified by having a simple useState inside of the React context that is added in this commit.
| return proxyFetch | ||
| } | ||
|
|
||
| /** Test-only: clears the module-scoped proxy-fetch cache so tests start from a known state. */ |
There was a problem hiding this comment.
This could be removed if we can use useState
|
|
||
| import { fetch as tauriFetch } from '@tauri-apps/plugin-http' | ||
| import { | ||
| PASSTHROUGH_PREFIX, |
…text, preferences

Note
High Risk
High risk because it changes security-sensitive proxy/CORS behaviour (SSRF protections, header forwarding, redirect handling) and introduces a new WS proxy path; mistakes could leak credentials or open SSRF vectors.
Overview
Switches the backend to a new universal proxy implementation:
/v1/proxynow supports header passthrough viaX-Proxy-Passthrough-*, redirect controls, streaming with caps/timeouts, SSRF/DNS pinning, and adds a new/v1/proxy/wsrelay with subprotocol-encoded targets.Adds proxy observability via
proxy_request/proxy_ws_relaystructured logs and OpenTelemetry span attributes, explicitly redacting full URLs/credentials while recording hostname, bytes, duration, status, and categorical error types.Introduces new authenticated APIs:
/v1/search(Exa-backed, normalises results to HTTPS and derives favicons) and/v1/preview(POST-only OG metadata extraction with HTTPS image upgrade and per-user 10‑minute private caching), plus new e2e tests.Removes the old
/pro/proxy,/pro/link-preview, and/mcp-proxyroutes and their tests, updates CORS toallowedHeaders: true(echo requested headers) and refreshescorsExposeHeadersdefaults/docs/env examples accordingly; also tweaks auth creation for test DI, sets Better AuthbaseURL, and adjusts dev tooling/lint globals/logger exports.Reviewed by Cursor Bugbot for commit c214826. Bugbot is set up for automated code reviews on this repo. Configure here.