From fdeffebba8aeeb3de90c80cee7c920fee702aae7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 01:07:50 +0800 Subject: [PATCH] Fix catch-all route serving HTML for API requests (#2885) * Initial plan * Fix env.js loading error by preventing catch-all from matching API routes Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> Co-authored-by: whyour --- back/loaders/express.ts | 9 ++++- test/back/api-fallback.test.cjs | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 test/back/api-fallback.test.cjs diff --git a/back/loaders/express.ts b/back/loaders/express.ts index 8ef1f06d..3d68aa95 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -179,7 +179,14 @@ export default ({ app }: { app: Application }) => { app.use(rewrite('/open/*', '/api/$1')); app.use(config.api.prefix, routes()); - app.get('*', (_, res, next) => { + app.get('*', (req, res, next) => { + // Don't serve index.html for API routes + if ( + req.path === config.api.prefix || + req.path.startsWith(`${config.api.prefix}/`) + ) { + return res.status(404).json({ code: 404, message: 'Not Found' }); + } const indexPath = path.join(frontendPath, 'index.html'); res.sendFile(indexPath, (err) => { if (err) { diff --git a/test/back/api-fallback.test.cjs b/test/back/api-fallback.test.cjs new file mode 100644 index 00000000..d0e4888a --- /dev/null +++ b/test/back/api-fallback.test.cjs @@ -0,0 +1,61 @@ +const test = require('node:test'), + assert = require('node:assert/strict'), + fs = require('node:fs'), + os = require('node:os'), + path = require('node:path'), + express = require('express'); +const load = require('../helpers/load-security-module.cjs'); +test('unknown API requests return JSON 404 while env.js and SPA deep links work', async (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-fallback-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + fs.mkdirSync(path.join(dir, 'static/dist'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'static/dist/index.html'), 'SPA'); + const app = express(); + load('back/loaders/express.ts', { + '../api': () => express.Router(), + '../config': { + rootPath: dir, + uploadPath: dir, + api: { prefix: '/api' }, + apiWhiteList: ['/api/env.js'], + jwt: { secret: 'test' }, + }, + 'express-jwt': { + expressjwt: () => ({ unless: () => (_q, _r, next) => next() }), + UnauthorizedError: class extends Error {}, + }, + '../config/util': { + getPlatform: () => 'desktop', + getToken: () => 'stored', + }, + '../config/serverEnv': { + serveEnv: (_q, r) => r.type('js').send('window.QL_CONFIG={};'), + }, + '../shared/store': { + shareStore: { getAuthInfo: async () => ({}), getApps: async () => [] }, + }, + '../shared/auth': { + isValidToken: () => true, + isDefaultAuthInfo: () => false, + }, + '../shared/i18n': { t: (s) => s }, + '../middlewares/uniqueConstraintError': (_q, _r, next) => next(), + '../middlewares/protectedPathCase': (_q, _r, next) => next(), + '../data/system': {}, + '../data/open': {}, + }).default({ app }); + const server = await new Promise((r) => { + const s = app.listen(0, '127.0.0.1', () => r(s)); + }); + t.after(() => new Promise((r) => server.close(r))); + const url = `http://127.0.0.1:${server.address().port}`; + for (const p of ['/api', '/api/missing', '/open/missing']) { + const res = await fetch(url + p); + assert.equal(res.status, 404, p); + assert.match(res.headers.get('content-type'), /json/); + assert.equal((await res.json()).code, 404); + } + const env = await fetch(url + '/api/env.js'); + assert.match(await env.text(), /QL_CONFIG/); + assert.equal(await (await fetch(url + '/script')).text(), 'SPA'); +});