From f457986a7fa61cd358626d68b15a426584fd37cc Mon Sep 17 00:00:00 2001 From: Maksym H Date: Tue, 3 Nov 2026 19:51:54 +0000 Subject: [PATCH] Forward the query string through the dashboard proxy --- packages/cli/src/commands/dashboard.ts | 9 +++++++-- packages/cli/tests/dashboard.test.ts | 27 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index da3147b..27ad2cc 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -430,7 +430,7 @@ export async function dashboard(ctx: ServiceContext): Promise { // "Provided Host header is not whitelisted". Same rewrite nginx does per location. const upstream = http.request( { host: '127.0.0.1', port: route.target.port, - path: route.target.keepPrefix ? route.matched + route.rest : (route.rest || '/'), + path: upstreamPath(route, url.search), method: req.method, headers: { ...req.headers, host: `127.0.0.1:${route.target.port}` } }, (up) => { @@ -471,7 +471,7 @@ export async function dashboard(ctx: ServiceContext): Promise { const headers = Object.entries({ ...req.headers, host: `127.0.0.1:${route.target.port}` }) .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`) .join('\r\n'); - upstream.write(`${req.method} ${route.target.keepPrefix ? route.matched + route.rest : (route.rest || '/')} HTTP/1.1\r\n${headers}\r\n\r\n`); + upstream.write(`${req.method} ${upstreamPath(route, url.search)} HTTP/1.1\r\n${headers}\r\n\r\n`); if (head.length) upstream.write(head); upstream.pipe(socket); socket.pipe(upstream); @@ -491,6 +491,11 @@ export async function dashboard(ctx: ServiceContext): Promise { await new Promise(() => {}); } +/** The path and query a matched request is forwarded upstream with. */ +function upstreamPath(route: NonNullable>, search: string): string { + return (route.target.keepPrefix ? route.matched + route.rest : route.rest || '/') + search; +} + /** Longest-prefix route match; returns the remainder to forward upstream. */ function matchRoute( routes: Map, diff --git a/packages/cli/tests/dashboard.test.ts b/packages/cli/tests/dashboard.test.ts index 2510378..fca8e25 100644 --- a/packages/cli/tests/dashboard.test.ts +++ b/packages/cli/tests/dashboard.test.ts @@ -301,4 +301,31 @@ describe('dashboard API', () => { assert.ok(e.checkedAt, `${e.id}: no checkedAt`); } }); + + // A client finds a person on the DUB by `GET /dub/api/v1/usernames/search?prefix=`; without + // the query the backend answers an unfiltered page and the person never shows up. + it('proxies a request with its query string', async () => { + const { body } = await get('/api/network'); + const dub = (body as { services: { id: string; port: number }[] }).services.find((s) => s.id === 'dub')!; + let seen: string | undefined; + const stub = http.createServer((req, res) => { + seen = req.url; + res.end('{}'); + }); + const bound = await new Promise((resolve) => { + stub.once('error', () => resolve(false)); + stub.listen(dub.port, '127.0.0.1', () => resolve(true)); + }); + if (!bound) { + console.log(`# skipped: port ${dub.port} is already in use`); + return; + } + try { + const res = await fetch(`${BASE}/dub/api/v1/usernames/search?prefix=alice&limit=5`); + assert.equal(res.status, 200); + assert.equal(seen, '/api/v1/usernames/search?prefix=alice&limit=5'); + } finally { + stub.close(); + } + }); });