Repository: compiler-explorer/compiler-explorer
Pull request #7572 (2025-04-15 19:14:16)

Migrate to express 5
Main changes:
- type checker cares about the return value (`void`) of handlers, so no more `return res.send("...")` as that returns `express` type.
- regexes on slugs no longer supported, but we weren't really using them in any meaningful way. The two places that had to be updated:
  - `/clientstate/:clientstate64` (was `([^]*)` which I _think_ is the same)
  - `/bits/:bits.html` - was some `\w+` but I believe that's unnecessary for the same reasons
- actually call the Sentry handler. I don't know if this actually worked before but the API checks suggest not.

Will need to be tested with a clientstate64. The bits worked ok.



Patch:
diff --git a/app.ts b/app.ts
index d9fa99731..e58676dd6 100755
--- a/app.ts
+++ b/app.ts
@@ -47,6 +47,7 @@ import systemdSocket from 'systemd-socket';
 import _ from 'underscore';
 import urljoin from 'url-join';
 
+import {unwrap} from './lib/assert.js';
 import * as aws from './lib/aws.js';
 import * as normalizer from './lib/clientstate-normalizer.js';
 import {GoldenLayoutRootStruct} from './lib/clientstate-normalizer.js';
@@ -635,9 +636,10 @@ async function main() {
         logger.debug('Compilers:', compilers);
         prevCompilers = compilers;
         await clientOptionsHandler.setCompilers(compilers);
-        routeApi.apiHandler.setCompilers(compilers);
-        routeApi.apiHandler.setLanguages(languages);
-        routeApi.apiHandler.setOptions(clientOptionsHandler);
+        const apiHandler = unwrap(routeApi.apiHandler);
+        apiHandler.setCompilers(compilers);
+        apiHandler.setLanguages(languages);
+        apiHandler.setOptions(clientOptionsHandler);
     }
 
     await onCompilerChange(initialCompilers);
@@ -687,7 +689,7 @@ async function main() {
             next({status: 404, message: `page "${req.path}" could not be found`});
         })
         // sentry error handler must be the first error handling middleware
-        .use(Sentry.Handlers.errorHandler)
+        .use(Sentry.Handlers.errorHandler())
         // eslint-disable-next-line no-unused-vars
         .use((err: any, req: express.Request, res: express.Response, _next: express.NextFunction) => {
             const status = err.status || err.statusCode || err.status_code || err.output?.statusCode || 500;
@@ -837,7 +839,7 @@ async function main() {
             res.set('Content-Type', 'application/javascript');
             res.end(`window.compilerExplorerOptions = ${clientOptionsHandler.getJSON()};`);
         })
-        .use('/bits/:bits(\\w+).html', cached, csp, (req, res) => {
+        .use('/bits/:bits.html', cached, csp, (req, res) => {
             res.render(
                 `bits/${sanitize(req.params.bits)}`,
                 renderConfig(
diff --git a/lib/handlers/api.ts b/lib/handlers/api.ts
index 3c98bf12c..6dfbaee8b 100644
--- a/lib/handlers/api.ts
+++ b/lib/handlers/api.ts
@@ -43,8 +43,7 @@ import {StorageBase} from '../storage/index.js';
 import {CompileHandler} from './compile.js';
 
 function methodNotAllowed(req: express.Request, res: express.Response) {
-    res.send('Method Not Allowed');
-    return res.status(405).end();
+    res.status(405).send('Method Not Allowed').end();
 }
 
 export class ApiHandler {
diff --git a/lib/handlers/api/assembly-documentation-controller.ts b/lib/handlers/api/assembly-documentation-controller.ts
index 85317b6a6..41b23a4a2 100644
--- a/lib/handlers/api/assembly-documentation-controller.ts
+++ b/lib/handlers/api/assembly-documentation-controller.ts
@@ -48,7 +48,8 @@ export class AssemblyDocumentationController implements HttpController {
             const instruction = (req.params.opcode || '__UNKNOWN_OPCODE').toUpperCase();
             const information = provider.getInstructionInformation(instruction);
             if (information === null) {
-                return res.status(404).send({error: `Unknown opcode '${instruction}'`});
+                res.status(404).send({error: `Unknown opcode '${instruction}'`});
+                return;
             }
 
             const contentType = req.accepts(['text', 'json']);
@@ -67,7 +68,7 @@ export class AssemblyDocumentationController implements HttpController {
                 }
             }
         } catch {
-            return res.status(404).send({error: `No documentation for '${req.params.arch}'`});
+            res.status(404).send({error: `No documentation for '${req.params.arch}'`});
         }
     }
 }
diff --git a/lib/handlers/api/formatting-controller.ts b/lib/handlers/api/formatting-controller.ts
index c293f8861..b343d8c05 100644
--- a/lib/handlers/api/formatting-controller.ts
+++ b/lib/handlers/api/formatting-controller.ts
@@ -44,7 +44,8 @@ export class FormattingController implements HttpController {
         const formatter = this.formattingService.getFormatterById(req.params.tool);
         // Ensure the target formatter exists
         if (formatter === null) {
-            return res.status(422).json({exit: 2, answer: `Unknown format tool '${req.params.tool}'`});
+            res.status(422).json({exit: 2, answer: `Unknown format tool '${req.params.tool}'`});
+            return;
         }
         // Ensure there is source code to format
         if (!req.body || !req.body.source) {
@@ -53,7 +54,8 @@ export class FormattingController implements HttpController {
         }
         // Ensure that the requested style is supported by the formatter
         if (!formatter.isValidStyle(req.body.base)) {
-            return res.status(422).json({exit: 3, answer: `Style '${req.body.base}' is not supported`});
+            res.status(422).json({exit: 3, answer: `Style '${req.body.base}' is not supported`});
+            return;
         }
         // Do the formatting
         try {
diff --git a/lib/handlers/api/healthcheck-controller.ts b/lib/handlers/api/healthcheck-controller.ts
index 4a086e1f4..a3e21bac2 100644
--- a/lib/handlers/api/healthcheck-controller.ts
+++ b/lib/handlers/api/healthcheck-controller.ts
@@ -62,7 +62,8 @@ export class HealthcheckController implements HttpController {
         // If this is a worker, we don't require that the server has languages configured.
         if (!this.isExecutionWorker && !this.compileHandler.hasLanguages()) {
             logger.error('*** HEALTH CHECK FAILURE: no languages/compilers detected');
-            return res.status(500).send();
+            res.status(500).send();
+            return;
         }
 
         // If we have a healthcheck file, we require that it exists and it is non-empty. The /efs/.health file contents
@@ -74,14 +75,16 @@ export class HealthcheckController implements HttpController {
                     throw new Error('File is empty');
                 }
                 res.set('Content-Type', 'text/html');
-                return res.send(content);
+                res.send(content);
+                return;
             } catch (e) {
                 logger.error(`*** HEALTH CHECK FAILURE: while reading file '${this.healthCheckFilePath}' got ${e}`);
                 SentryCapture(e, 'Health check');
-                return res.status(500).send();
+                res.status(500).send();
+                return;
             }
         }
 
-        return res.send('Everything is awesome');
+        res.send('Everything is awesome');
     }
 }
diff --git a/lib/handlers/compile.ts b/lib/handlers/compile.ts
index a67866db3..f8f0826a2 100644
--- a/lib/handlers/compile.ts
+++ b/lib/handlers/compile.ts
@@ -486,7 +486,8 @@ export class CompileHandler implements ICompileHandler {
     handlePopularArguments(req: express.Request, res: express.Response) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
         res.send(compiler.possibleArguments.getPopularArguments(this.getUsedOptions(req)));
     }
@@ -494,7 +495,8 @@ export class CompileHandler implements ICompileHandler {
     handleOptimizationArguments(req: express.Request, res: express.Response) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
         res.send(compiler.possibleArguments.getOptimizationArguments(this.getUsedOptions(req)));
     }
@@ -513,18 +515,20 @@ export class CompileHandler implements ICompileHandler {
 
     handleApiError(error: any, res: express.Response, next: express.NextFunction) {
         if (error.message) {
-            return res.status(400).send({
+            res.status(400).send({
                 error: true,
                 message: error.message,
             });
+            return;
         }
-        return next(error);
+        next(error);
     }
 
     handleCmake(req: express.Request, res: express.Response, next: express.NextFunction) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
 
         const remote = compiler.getRemote();
@@ -561,14 +565,16 @@ export class CompileHandler implements ICompileHandler {
                     return this.handleApiError(e, res, next);
                 });
         } catch (e) {
-            return this.handleApiError(e, res, next);
+            this.handleApiError(e, res, next);
+            return;
         }
     }
 
     handle(req: express.Request, res: express.Response, next: express.NextFunction) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
 
         const remote = compiler.getRemote();
@@ -585,7 +591,8 @@ export class CompileHandler implements ICompileHandler {
         try {
             parsedRequest = this.parseRequest(req, compiler);
         } catch (error) {
-            return this.handleApiError(error, res, next);
+            this.handleApiError(error, res, next);
+            return;
         }
 
         const {source, options, backendOptions, filters, bypassCache, tools, executeParameters, libraries} =
diff --git a/lib/handlers/middleware.ts b/lib/handlers/middleware.ts
index 3e940c983..f02be6fd0 100644
--- a/lib/handlers/middleware.ts
+++ b/lib/handlers/middleware.ts
@@ -35,9 +35,10 @@ const ceProps = props.propsFor('compiler-explorer');
  */
 export const jsonOnly: express.Handler = (req, res, next) => {
     if (req.headers['content-type'] !== 'application/json') {
-        return res.status(400).json({message: 'bad request, expected json content'});
+        res.status(400).json({message: 'bad request, expected json content'});
+        return;
     }
-    return next();
+    next();
 };
 
 /** Add static headers to the response */
diff --git a/lib/handlers/route-api.ts b/lib/handlers/route-api.ts
index a1ddb6b7e..5c90773ff 100644
--- a/lib/handlers/route-api.ts
+++ b/lib/handlers/route-api.ts
@@ -29,7 +29,7 @@ import express from 'express';
 import {AppDefaultArguments, CompilerExplorerOptions} from '../../app.js';
 import {isString} from '../../shared/common-utils.js';
 import {Language} from '../../types/languages.interfaces.js';
-import {assert} from '../assert.js';
+import {assert, unwrap} from '../assert.js';
 import {ClientStateGoldenifier, ClientStateNormalizer} from '../clientstate-normalizer.js';
 import {ClientState} from '../clientstate.js';
 import {CompilationEnvironment} from '../compilation-env.js';
@@ -67,7 +67,7 @@ export type ShortLinkMetaData = {
 export class RouteAPI {
     renderGoldenLayout: any;
     storageHandler: StorageBase;
-    apiHandler: ApiHandler;
+    apiHandler: ApiHandler | undefined;
 
     constructor(
         private readonly router: express.Router,
@@ -89,17 +89,19 @@ export class RouteAPI {
 
             this.apiHandler.setReleaseInfo(config.defArgs.gitReleaseName, config.defArgs.releaseBuildNumber);
         } else {
-            this.apiHandler = undefined as any;
+            this.apiHandler = undefined;
         }
     }
 
     InitializeRoutes() {
+        if (this.apiHandler) {
+            this.router.use('/api', this.apiHandler.handle);
+        }
         this.router
-            .use('/api', this.apiHandler.handle)
             .get('/z/:id', cached, csp, this.storedStateHandler.bind(this))
             .get('/z/:id/code/:session', cached, csp, this.storedCodeHandler.bind(this))
             .get('/resetlayout/:id', cached, csp, this.storedStateHandlerResetLayout.bind(this))
-            .get('/clientstate/:clientstatebase64([^]*)', cached, csp, this.unstoredStateHandler.bind(this))
+            .get(/^\/clientstate\/(?<clientstatebase64>.*)/, cached, csp, this.unstoredStateHandler.bind(this))
             .get('/fromsimplelayout', cached, csp, this.simpleLayoutHandler.bind(this));
     }
 
@@ -111,7 +113,7 @@ export class RouteAPI {
             .then(result => {
                 const config = JSON.parse(result.config);
 
-                let clientstate: ClientState | null = null;
+                let clientstate: ClientState | null;
                 if (config.content) {
                     const normalizer = new ClientStateNormalizer();
                     normalizer.fromGoldenLayout(config);
@@ -268,7 +270,7 @@ export class RouteAPI {
             const sources = utils.glGetMainContents(config.content);
             if (sources.editors.length === 1) {
                 const editor = sources.editors[0];
-                lang = this.apiHandler.languages[editor.language];
+                lang = unwrap(this.apiHandler).languages[editor.language];
                 source = editor.source;
             } else {
                 const normalizer = new ClientStateNormalizer();
@@ -277,7 +279,7 @@ export class RouteAPI {
 
                 if (clientstate.trees && clientstate.trees.length === 1) {
                     const tree = clientstate.trees[0];
-                    lang = this.apiHandler.languages[tree.compilerLanguageId];
+                    lang = unwrap(this.apiHandler).languages[tree.compilerLanguageId];
 
                     if (tree.isCMakeProject) {
                         const firstSource = tree.files.find(file => {
@@ -304,7 +306,7 @@ export class RouteAPI {
                 metadata.ogTitle += ` - ${lang.name}`;
                 if (sources && sources.compilers.length === 1) {
                     const compilerId = sources.compilers[0].compiler;
-                    const compiler = this.apiHandler.compilers.find(c => c.id === compilerId);
+                    const compiler = unwrap(this.apiHandler).compilers.find(c => c.id === compilerId);
                     if (compiler) {
                         metadata.ogTitle += ` (${compiler.name})`;
                     }
diff --git a/package-lock.json b/package-lock.json
index e1978aec1..d424c4838 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,7 +31,7 @@
         "cross-env": "^7.0.3",
         "enhanced-ms": "^4.1.0",
         "events": "^3.3.0",
-        "express": "^4.21.2",
+        "express": "^5.1.0",
         "file-saver": "^2.0.5",
         "golden-layout": "^1.5.9",
         "http-proxy": "^1.18.1",
@@ -82,7 +82,7 @@
         "@types/bootstrap": "^5.2.10",
         "@types/chai": "^5.2.1",
         "@types/compression": "^1.7.5",
-        "@types/express": "^4.17.21",
+        "@types/express": "^5.0.1",
         "@types/file-saver": "^2.0.7",
         "@types/http-proxy": "^1.17.16",
         "@types/jquery": "^3.5.32",
@@ -4593,22 +4593,21 @@
       "license": "MIT"
     },
     "node_modules/@types/express": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
-      "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
+      "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/body-parser": "*",
-        "@types/express-serve-static-core": "^4.17.33",
-        "@types/qs": "*",
+        "@types/express-serve-static-core": "^5.0.0",
         "@types/serve-static": "*"
       }
     },
     "node_modules/@types/express-serve-static-core": {
-      "version": "4.19.6",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
-      "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
+      "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5303,22 +5302,34 @@
       }
     },
     "node_modules/accepts": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
-      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
       "license": "MIT",
       "dependencies": {
-        "mime-types": "~2.1.34",
-        "negotiator": "0.6.3"
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accepts/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
       },
       "engines": {
         "node": ">= 0.6"
       }
     },
     "node_modules/accepts/node_modules/negotiator": {
-      "version": "0.6.3",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
       "license": "MIT",
       "engines": {
         "node": ">= 0.6"
@@ -5498,12 +5509,6 @@
       ],
       "license": "MIT"
     },
-    "node_modules/array-flatten": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
-      "license": "MIT"
-    },
     "node_modules/asap": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@@ -5741,69 +5746,23 @@
       "license": "MIT"
     },
     "node_modules/body-parser": {
-      "version": "1.20.3",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
-      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
-      "license": "MIT",
-      "dependencies": {
-        "bytes": "3.1.2",
-        "content-type": "~1.0.5",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
-        "on-finished": "2.4.1",
-        "qs": "6.13.0",
-        "raw-body": "2.5.2",
-        "type-is": "~1.6.18",
-        "unpipe": "1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8",
-        "npm": "1.2.8000 || >= 1.4.16"
-      }
-    },
-    "node_modules/body-parser/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/body-parser/node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+      "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
       "license": "MIT",
       "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/body-parser/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
-    "node_modules/body-parser/node_modules/qs": {
-      "version": "6.13.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
-      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "side-channel": "^1.0.6"
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.0",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.6.3",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.0",
+        "raw-body": "^3.0.0",
+        "type-is": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=18"
       }
     },
     "node_modules/boolbase": {
@@ -6508,9 +6467,9 @@
       }
     },
     "node_modules/content-disposition": {
-      "version": "0.5.4",
-      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
-      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+      "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
       "license": "MIT",
       "dependencies": {
         "safe-buffer": "5.2.1"
@@ -6538,10 +6497,13 @@
       }
     },
     "node_modules/cookie-signature": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
-      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
-      "license": "MIT"
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
     },
     "node_modules/cookiejar": {
       "version": "2.1.4",
@@ -7040,7 +7002,6 @@
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
       "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ms": "^2.1.3"
@@ -7095,16 +7056,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/destroy": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
-      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8",
-        "npm": "1.2.8000 || >= 1.4.16"
-      }
-    },
     "node_modules/detect-libc": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -7624,79 +7575,66 @@
       }
     },
     "node_modules/express": {
-      "version": "4.21.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
-      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
-      "license": "MIT",
-      "dependencies": {
-        "accepts": "~1.3.8",
-        "array-flatten": "1.1.1",
-        "body-parser": "1.20.3",
-        "content-disposition": "0.5.4",
-        "content-type": "~1.0.4",
-        "cookie": "0.7.1",
-        "cookie-signature": "1.0.6",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "finalhandler": "1.3.1",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "merge-descriptors": "1.0.3",
-        "methods": "~1.1.2",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "path-to-regexp": "0.1.12",
-        "proxy-addr": "~2.0.7",
-        "qs": "6.13.0",
-        "range-parser": "~1.2.1",
-        "safe-buffer": "5.2.1",
-        "send": "0.19.0",
-        "serve-static": "1.16.2",
-        "setprototypeof": "1.2.0",
-        "statuses": "2.0.1",
-        "type-is": "~1.6.18",
-        "utils-merge": "1.0.1",
-        "vary": "~1.1.2"
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+      "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.0",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
       },
       "engines": {
-        "node": ">= 0.10.0"
+        "node": ">= 18"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/express"
       }
     },
-    "node_modules/express/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+    "node_modules/express/node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
       "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
+      "engines": {
+        "node": ">= 0.8"
       }
     },
-    "node_modules/express/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
-    "node_modules/express/node_modules/qs": {
-      "version": "6.13.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
-      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
-      "license": "BSD-3-Clause",
+    "node_modules/express/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
       "dependencies": {
-        "side-channel": "^1.0.6"
+        "mime-db": "^1.54.0"
       },
       "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 0.6"
       }
     },
     "node_modules/extend": {
@@ -7900,38 +7838,22 @@
       }
     },
     "node_modules/finalhandler": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
-      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+      "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
       "license": "MIT",
       "dependencies": {
-        "debug": "2.6.9",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "statuses": "2.0.1",
-        "unpipe": "~1.0.0"
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
       },
       "engines": {
         "node": ">= 0.8"
       }
     },
-    "node_modules/finalhandler/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/finalhandler/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
     "node_modules/find-up": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -8546,7 +8468,6 @@
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
       "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -9997,12 +9918,12 @@
       "license": "CC0-1.0"
     },
     "node_modules/media-typer": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
       "license": "MIT",
       "engines": {
-        "node": ">= 0.6"
+        "node": ">= 0.8"
       }
     },
     "node_modules/memfs": {
@@ -10026,10 +9947,13 @@
       }
     },
     "node_modules/merge-descriptors": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
-      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
       "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
@@ -10044,6 +9968,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
       "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.6"
@@ -10063,18 +9988,6 @@
         "node": ">=8.6"
       }
     },
-    "node_modules/mime": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
-      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
-      "license": "MIT",
-      "bin": {
-        "mime": "cli.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/mime-db": {
       "version": "1.54.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -10351,16 +10264,6 @@
         "@sinonjs/commons": "^3.0.1"
       }
     },
-    "node_modules/nise/node_modules/path-to-regexp": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
-      "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=16"
-      }
-    },
     "node_modules/nock": {
       "version": "14.0.3",
       "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.3.tgz",
@@ -10771,10 +10674,13 @@
       "license": "ISC"
     },
     "node_modules/path-to-regexp": {
-      "version": "0.1.12",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
-      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
-      "license": "MIT"
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+      "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
     },
     "node_modules/pathe": {
       "version": "2.0.3",
@@ -11754,7 +11660,6 @@
       "version": "6.14.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
       "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
-      "dev": true,
       "license": "BSD-3-Clause",
       "dependencies": {
         "side-channel": "^1.1.0"
@@ -11785,32 +11690,20 @@
       }
     },
     "node_modules/raw-body": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
-      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+      "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
       "license": "MIT",
       "dependencies": {
         "bytes": "3.1.2",
         "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
+        "iconv-lite": "0.6.3",
         "unpipe": "1.0.0"
       },
       "engines": {
         "node": ">= 0.8"
       }
     },
-    "node_modules/raw-body/node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "license": "MIT",
-      "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/readable-stream": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -12118,6 +12011,28 @@
         "fsevents": "~2.3.2"
       }
     },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/router/node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
     "node_modules/rrweb-cssom": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
@@ -12335,51 +12250,46 @@
       }
     },
     "node_modules/send": {
-      "version": "0.19.0",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
-      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
-      "license": "MIT",
-      "dependencies": {
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "mime": "1.6.0",
-        "ms": "2.1.3",
-        "on-finished": "2.4.1",
-        "range-parser": "~1.2.1",
-        "statuses": "2.0.1"
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "mime-types": "^3.0.1",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.1"
       },
       "engines": {
-        "node": ">= 0.8.0"
+        "node": ">= 18"
       }
     },
-    "node_modules/send/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+    "node_modules/send/node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
       "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
+      "engines": {
+        "node": ">= 0.8"
       }
     },
-    "node_modules/send/node_modules/debug/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
-    "node_modules/send/node_modules/encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+    "node_modules/send/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
       "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
       "engines": {
-        "node": ">= 0.8"
+        "node": ">= 0.6"
       }
     },
     "node_modules/serialize-javascript": {
@@ -12420,18 +12330,18 @@
       "license": "MIT"
     },
     "node_modules/serve-static": {
-      "version": "1.16.2",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
-      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
       "license": "MIT",
       "dependencies": {
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "parseurl": "~1.3.3",
-        "send": "0.19.0"
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
       },
       "engines": {
-        "node": ">= 0.8.0"
+        "node": ">= 18"
       }
     },
     "node_modules/setimmediate": {
@@ -13615,13 +13525,26 @@
       }
     },
     "node_modules/type-is": {
-      "version": "1.6.18",
-      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
-      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
       "license": "MIT",
       "dependencies": {
-        "media-typer": "0.3.0",
-        "mime-types": "~2.1.24"
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/type-is/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
       },
       "engines": {
         "node": ">= 0.6"
@@ -13758,15 +13681,6 @@
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
       "license": "MIT"
     },
-    "node_modules/utils-merge": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
-      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
     "node_modules/uuid": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
diff --git a/package.json b/package.json
index 30aa9d078..30e187dba 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "cross-env": "^7.0.3",
     "enhanced-ms": "^4.1.0",
     "events": "^3.3.0",
-    "express": "^4.21.2",
+    "express": "^5.1.0",
     "file-saver": "^2.0.5",
     "golden-layout": "^1.5.9",
     "http-proxy": "^1.18.1",
@@ -91,7 +91,7 @@
     "@types/bootstrap": "^5.2.10",
     "@types/chai": "^5.2.1",
     "@types/compression": "^1.7.5",
-    "@types/express": "^4.17.21",
+    "@types/express": "^5.0.1",
     "@types/file-saver": "^2.0.7",
     "@types/http-proxy": "^1.17.16",
     "@types/jquery": "^3.5.32",


Test patch:
diff --git a/test/handlers/middleware-tests.ts b/test/handlers/middleware-tests.ts
index 65b1bc9ab..a0bf6dc4b 100644
--- a/test/handlers/middleware-tests.ts
+++ b/test/handlers/middleware-tests.ts
@@ -31,7 +31,9 @@ import {cached, cors} from '../../lib/handlers/middleware.js';
 describe('middleware functions', () => {
     it('adds cache controls headers with cached()', async () => {
         const app = express();
-        app.use('/', cached, (_, res) => res.send('Hello World!'));
+        app.use('/', cached, (_, res) => {
+            res.send('Hello World!');
+        });
         await request(app)
             .get('/')
             .expect(200, 'Hello World!')
@@ -40,7 +42,9 @@ describe('middleware functions', () => {
 
     it('adds cors headers with cors()', async () => {
         const app = express();
-        app.use('/', cors, (_, res) => res.send('Hello World!'));
+        app.use('/', cors, (_, res) => {
+            res.send('Hello World!');
+        });
         await request(app)
             .get('/')
             .expect(200, 'Hello World!')
diff --git a/test/handlers/route-api-test.ts b/test/handlers/route-api-test.ts
index b258c4aec..7cd7b0ccb 100644
--- a/test/handlers/route-api-test.ts
+++ b/test/handlers/route-api-test.ts
@@ -24,9 +24,17 @@
 
 import zlib from 'node:zlib';
 
-import {describe, expect, it} from 'vitest';
+import {beforeAll, describe, expect, it} from 'vitest';
 
-import {extractJsonFromBufferAndInflateIfRequired} from '../../lib/handlers/route-api.js';
+import express from 'express';
+import request from 'supertest';
+import {GoldenLayoutRootStruct} from '../../lib/clientstate-normalizer.js';
+import {
+    HandlerConfig,
+    RouteAPI,
+    ShortLinkMetaData,
+    extractJsonFromBufferAndInflateIfRequired,
+} from '../../lib/handlers/route-api.js';
 
 function possibleCompression(buffer: Buffer): boolean {
     // code used in extractJsonFromBufferAndInflateIfRequired
@@ -63,3 +71,47 @@ describe('extractJsonFromBufferAndInflateIfRequired test cases', () => {
         expect(() => extractJsonFromBufferAndInflateIfRequired(buffer)).toThrow();
     });
 });
+
+describe('clientStateHandler', () => {
+    let app: express.Express;
+
+    beforeAll(() => {
+        app = express();
+        const apiHandler = new RouteAPI(app, {
+            renderGoldenLayout: (
+                config: GoldenLayoutRootStruct,
+                metadata: ShortLinkMetaData,
+                req: express.Request,
+                res: express.Response,
+            ) => {
+                res.send('This is ok');
+            },
+        } as unknown as HandlerConfig);
+        apiHandler.InitializeRoutes();
+    });
+
+    it('should return 200 for /clientstate', async () => {
+        // A valid document is a base64 encoded JSON object with a sessions array.
+        const document = Buffer.from(JSON.stringify({sessions: []}), 'utf-8').toString('base64');
+        const response = await request(app).get(`/clientstate/${document}`);
+        expect(response.status).toBe(200);
+    });
+    it('should return 200 for /clientstate even if the base64 contains `/`', async () => {
+        let document = '';
+        for (let i = 0; i < 1000; i++) {
+            document = Buffer.from(
+                JSON.stringify({
+                    sessions: [],
+                    magic: String.fromCharCode(i, i + 1234),
+                }),
+                'utf-8',
+            ).toString('base64');
+            if (document.includes('/')) {
+                break;
+            }
+        }
+        expect(document).toContain('/');
+        const response = await request(app).get(`/clientstate/${document}`);
+        expect(response.status).toBe(200);
+    });
+});