From 13432209ee01fc66a95ecddf0ee0940339d1de57 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 27 Aug 2026 11:19:39 +0300 Subject: [PATCH 1/2] Restore the mount prefix when handing requests to Next Co-authored-by: Cursor --- server/Server.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index c1657ff1e..019e67c4f 100644 --- a/server/Server.js +++ b/server/Server.js @@ -158,6 +158,12 @@ class Server { } await Database.init(false) + + if (typeof global.RouterBasePath !== 'string') { + throw new Error('[Server] RouterBasePath must be a string before serving requests') + } + Logger.info(`[Server] Serving from base path "${global.RouterBasePath || '/'}"`) + // Create or set JWT secret in token manager await this.auth.tokenManager.initTokenSecret() @@ -409,7 +415,18 @@ class Server { const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath }) const handle = nextApp.getRequestHandler() await nextApp.prepare() - router.all('*', (req, res) => handle(req, res)) + router.all('*', (req, res) => { + // Next is configured with the same base path as this router, but Express strips the mount + // prefix from req.url, so put it back. The bare mount path is passed through without a + // trailing slash, which Next would otherwise redirect away from on every request. + if (req.baseUrl) { + const queryIndex = req.url.indexOf('?') + const pathname = queryIndex === -1 ? req.url : req.url.slice(0, queryIndex) + const search = queryIndex === -1 ? '' : req.url.slice(queryIndex) + req.url = `${req.baseUrl}${pathname === '/' ? '' : pathname}${search}` + } + handle(req, res) + }) } const unixSocketPrefix = 'unix/' From c0b9110e4ea9cf3b7990e61e22be170b05161f06 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 27 Aug 2026 12:17:10 +0300 Subject: [PATCH 2/2] Make sure Next has basePath before remounting baseUrl --- server/Server.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index 019e67c4f..5fac001d8 100644 --- a/server/Server.js +++ b/server/Server.js @@ -46,6 +46,18 @@ const passport = require('passport') const expressSession = require('express-session') const MemoryStore = require('./libs/memorystore') +/** + * `next()` for a custom server is a NextCustomServer wrapper. After prepare(), + * `.server` is another NextServer wrapper; the Node server with `nextConfig` is + * one more `.server` down. Older react clients have no basePath and must keep + * Express-stripped URLs. + */ +function getPreparedNextBasePath(nextApp) { + const nodeServer = nextApp.server?.server + const basePath = nodeServer?.nextConfig?.basePath + return typeof basePath === 'string' ? basePath : '' +} + class Server { constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { this.Port = PORT @@ -415,11 +427,18 @@ class Server { const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath }) const handle = nextApp.getRequestHandler() await nextApp.prepare() + const nextBasePath = getPreparedNextBasePath(nextApp) + if (nextBasePath) { + Logger.info(`[Server] Next client basePath is "${nextBasePath}"`) + } else { + Logger.info(`[Server] Next client has no basePath; leaving Express-stripped URLs as-is`) + } router.all('*', (req, res) => { // Next is configured with the same base path as this router, but Express strips the mount // prefix from req.url, so put it back. The bare mount path is passed through without a // trailing slash, which Next would otherwise redirect away from on every request. - if (req.baseUrl) { + // Skip when Next has no basePath (older client on master). + if (nextBasePath && req.baseUrl) { const queryIndex = req.url.indexOf('?') const pathname = queryIndex === -1 ? req.url : req.url.slice(0, queryIndex) const search = queryIndex === -1 ? '' : req.url.slice(queryIndex)