From 85bf7c75630f3a564be0a1189ae6903d5e26e52c Mon Sep 17 00:00:00 2001 From: Michel Betancourt Date: Wed, 10 Jun 2026 11:54:41 -0400 Subject: [PATCH] fix(opencode): accept non-2xx status codes in probe-url to support redirects (#1471) Co-authored-by: mdbetancourt Co-authored-by: Bohdan Triapitsyn --- .../web/server/lib/opencode/core-routes.js | 2 +- .../server/lib/opencode/core-routes.test.js | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/web/server/lib/opencode/core-routes.js b/packages/web/server/lib/opencode/core-routes.js index 101e3dc3..6454fb16 100644 --- a/packages/web/server/lib/opencode/core-routes.js +++ b/packages/web/server/lib/opencode/core-routes.js @@ -620,7 +620,7 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => { redirect: 'manual', signal: AbortSignal.timeout(1500), }); - return res.json({ ok: response.ok, status: response.status }); + return res.json({ ok: response.status >= 200 && response.status < 600, status: response.status }); } catch (error) { return res.json({ ok: false, error: error?.message || 'Probe failed' }); } diff --git a/packages/web/server/lib/opencode/core-routes.test.js b/packages/web/server/lib/opencode/core-routes.test.js index 61d40a01..dcd9765d 100644 --- a/packages/web/server/lib/opencode/core-routes.test.js +++ b/packages/web/server/lib/opencode/core-routes.test.js @@ -84,6 +84,65 @@ describe('core-routes', () => { } }); + it('should probe loopback preview URLs and return ok: true for status codes 200-599', async () => { + const app = express(); + const originalFetch = globalThis.fetch; + const fetchMock = vi.fn(); + globalThis.fetch = fetchMock; + + registerAuthAndAccessRoutes(app, { + express, + tunnelAuthController: { + classifyRequestScope: () => 'local', + requireTunnelSession: vi.fn(), + getTunnelSessionFromRequest: vi.fn(), + clearTunnelSessionCookie: vi.fn(), + exchangeBootstrapToken: vi.fn(), + }, + uiAuthController: { + requireAuth: (_req, _res, next) => next(), + handleSessionStatus: vi.fn(), + handleSessionCreate: vi.fn(), + handlePasskeyStatus: vi.fn(), + handlePasskeyAuthenticationOptions: vi.fn(), + handlePasskeyAuthenticationVerify: vi.fn(), + handlePasskeyRegistrationOptions: vi.fn(), + handlePasskeyRegistrationVerify: vi.fn(), + handlePasskeyList: vi.fn(), + handlePasskeyRevoke: vi.fn(), + handleResetAuth: vi.fn(), + }, + readSettingsFromDiskMigrated: vi.fn(async () => ({})), + normalizeTunnelSessionTtlMs: vi.fn(), + }); + + try { + const testCases = [ + { status: 200, expectedOk: true }, + { status: 302, expectedOk: true }, + { status: 404, expectedOk: true }, + { status: 500, expectedOk: true }, + { status: 600, expectedOk: false }, + ]; + + for (const { status, expectedOk } of testCases) { + fetchMock.mockResolvedValueOnce({ + status, + ok: status >= 200 && status < 300, + }); + + const response = await request(app) + .post('/api/system/probe-url') + .send({ url: 'http://127.0.0.1:5173/' }) + .expect(200); + + expect(response.body).toEqual({ ok: expectedOk, status }); + } + } finally { + globalThis.fetch = originalFetch; + } + }); + it('should let preview proxy credentials reach preview proxy validation', async () => { const app = express(); const requireAuth = vi.fn((_req, res) => res.status(401).type('text/plain').send('Authentication required'));