diff --git a/src/backend/src/api/eggspress.js b/src/backend/src/api/eggspress.js
index fca0bbf98..5840ef9fd 100644
--- a/src/backend/src/api/eggspress.js
+++ b/src/backend/src/api/eggspress.js
@@ -1,198 +1,2 @@
-/*
- * Copyright (C) 2024 Puter Technologies Inc.
- *
- * This file is part of Puter.
- *
- * Puter is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-const express = require('express');
-const multer = require('multer');
-const multest = require('@heyputer/multest');
-const api_error_handler = require('../api/api_error_handler.js');
-
-const fsBeforeMW = require('../middleware/fs');
-const APIError = require('./APIError.js');
-const { Context } = require('../util/context.js');
-
-/**
- * eggspress() is a factory function for creating express routers.
- *
- * @param {*} route the route to the router
- * @param {*} settings the settings for the router. The following
- * properties are supported:
- * - auth: whether or not to use the auth middleware
- * - fs: whether or not to use the fs middleware
- * - json: whether or not to use the json middleware
- * - customArgs: custom arguments to pass to the router
- * - allowedMethods: the allowed HTTP methods
- * @param {*} handler the handler for the router
- * @returns {express.Router} the router
- */
-module.exports = function eggspress (route, settings, handler) {
- const router = express.Router();
- const mw = [];
- const afterMW = [];
-
- // These flags enable specific middleware.
- if ( settings.abuse ) mw.push(require('../middleware/abuse')(settings.abuse));
- if ( settings.auth ) mw.push(require('../middleware/auth'));
- if ( settings.auth2 ) mw.push(require('../middleware/auth2'));
- if ( settings.fs ) {
- mw.push(fsBeforeMW);
- }
- if ( settings.verified ) mw.push(require('../middleware/verified'));
- if ( settings.json ) mw.push(express.json());
-
- // The `files` setting is an array of strings. Each string is the name
- // of a multipart field that contains files. `multer` is used to parse
- // the multipart request and store the files in `req.files`.
- if ( settings.files ) {
- for ( const key of settings.files ) {
- mw.push(multer().array(key));
- }
- }
-
- if ( settings.multest ) {
- mw.push(multest());
- }
-
- // The `multipart_jsons` setting is an array of strings. Each string
- // is the name of a multipart field that contains JSON. This middleware
- // parses the JSON in each field and stores the result in `req.body`.
- if ( settings.multipart_jsons ) {
- for ( const key of settings.multipart_jsons ) {
- mw.push((req, res, next) => {
- try {
- if ( ! Array.isArray(req.body[key]) ) {
- req.body[key] = [JSON.parse(req.body[key])];
- } else {
- req.body[key] = req.body[key].map(JSON.parse);
- }
- } catch (e) {
- return res.status(400).send({
- error: {
- message: `Invalid JSON in multipart field ${key}`
- }
- });
- }
- next();
- });
- }
- }
-
- // The `alias` setting is an object. Each key is the name of a
- // parameter. Each value is the name of a parameter that should
- // be aliased to the key.
- if ( settings.alias ) {
- for ( const alias in settings.alias ) {
- const target = settings.alias[alias];
- mw.push((req, res, next) => {
- const values = req.method === 'GET' ? req.query : req.body;
- if ( values[alias] ) {
- values[target] = values[alias];
- }
- next();
- });
- }
- }
-
- // The `parameters` setting is an object. Each key is the name of a
- // parameter. Each value is a `Param` object. The `Param` object
- // specifies how to validate the parameter.
- if ( settings.parameters ) {
- for ( const key in settings.parameters ) {
- const param = settings.parameters[key];
- mw.push(async (req, res, next) => {
- if ( ! req.values ) req.values = {};
-
- const values = req.method === 'GET' ? req.query : req.body;
- const getParam = (key) => values[key];
- try {
- const result = await param.consolidate({ req, getParam });
- req.values[key] = result;
- } catch (e) {
- api_error_handler(e, req, res, next);
- return;
- }
- next();
- });
- }
- }
-
- // what if I wanted to pass arguments to, for example, `json`?
- if ( settings.customArgs ) mw.push(settings.customArgs);
-
- if ( settings.alarm_timeout ) {
- mw.push((req, res, next) => {
- setTimeout(() => {
- if ( ! res.headersSent ) {
- const log = req.services.get('log-service').create('eggspress:timeout');
- const errors = req.services.get('error-service').create(log);
- let id = Array.isArray(route) ? route[0] : route;
- id = id.replace(/\//g, '_');
- errors.report(id, {
- source: new Error('Response timed out.'),
- message: 'Response timed out.',
- trace: true,
- alarm: true,
- });
- }
- }, settings.alarm_timeout);
- next();
- });
- }
-
- if ( settings.response_timeout ) {
- mw.push((req, res, next) => {
- setTimeout(() => {
- if ( ! res.headersSent ) {
- api_error_handler(APIError.create('response_timeout'), req, res, next);
- }
- }, settings.response_timeout);
- next();
- });
- }
-
- if ( settings.mw ) mw.push(...settings.mw);
-
- const errorHandledHandler = async function (req, res, next) {
- if ( settings.subdomain ) {
- if ( require('../helpers').subdomain(req) !== settings.subdomain ) {
- return next();
- }
- }
- try {
- const expected_ctx = res.locals.ctx;
- const received_ctx = Context.get(undefined, { allow_fallback: true });
-
- if ( expected_ctx != received_ctx ) {
- await expected_ctx.arun(async () => {
- await handler(req, res, next);
- });
- } else await handler(req, res, next);
- } catch (e) {
- api_error_handler(e, req, res, next);
- }
- };
-
- if ( settings.allowedMethods.includes('GET') ) {
- router.get(route, ...mw, errorHandledHandler, ...afterMW);
- }
-
- if ( settings.allowedMethods.includes('POST') ) {
- router.post(route, ...mw, errorHandledHandler, ...afterMW);
- }
-
- return router;
-}
\ No newline at end of file
+// This file is a legacy alias
+module.exports = require('../modules/web/lib/eggspress.js');
diff --git a/src/backend/src/modules/web/WebServerService.js b/src/backend/src/modules/web/WebServerService.js
index a9fd059a4..de1daaf89 100644
--- a/src/backend/src/modules/web/WebServerService.js
+++ b/src/backend/src/modules/web/WebServerService.js
@@ -104,7 +104,7 @@ class WebServerService extends BaseService {
// error handling middleware goes last, as per the
// expressjs documentation:
// https://expressjs.com/en/guide/error-handling.html
- this.app.use(require('../../api/api_error_handler.js'));
+ this.app.use(require('./lib/api_error_handler.js'));
const { jwt_auth } = require('../../helpers.js');
diff --git a/src/backend/src/modules/web/lib/__lib__.js b/src/backend/src/modules/web/lib/__lib__.js
new file mode 100644
index 000000000..2dc2a60ec
--- /dev/null
+++ b/src/backend/src/modules/web/lib/__lib__.js
@@ -0,0 +1,4 @@
+module.exports = {
+ eggspress: require("./eggspress"),
+ api_error_handler: require("./api_error_handler"),
+};
diff --git a/src/backend/src/modules/web/lib/api_error_handler.js b/src/backend/src/modules/web/lib/api_error_handler.js
new file mode 100644
index 000000000..2e8f16ce9
--- /dev/null
+++ b/src/backend/src/modules/web/lib/api_error_handler.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const APIError = require('../../../api/APIError.js');
+
+/**
+ * api_error_handler() is an express error handler for API errors.
+ * It adheres to the express error handler signature and should be
+ * used as the last middleware in an express app.
+ *
+ * Since Express 5 is not yet released, this function is used by
+ * eggspress() to handle errors instead of as a middleware.
+ *
+ * @todo remove this function and use express error handling
+ * when Express 5 is released
+ *
+ * @param {*} err
+ * @param {*} req
+ * @param {*} res
+ * @param {*} next
+ * @returns
+ */
+module.exports = function api_error_handler (err, req, res, next) {
+ if (res.headersSent) {
+ console.error('error after headers were sent:', err);
+ return next(err)
+ }
+
+ // API errors might have a response to help the
+ // developer resolve the issue.
+ if ( err instanceof APIError ) {
+ return err.write(res);
+ }
+
+ if (
+ typeof err === 'object' &&
+ ! (err instanceof Error) &&
+ err.hasOwnProperty('message')
+ ) {
+ const apiError = APIError.create(400, err);
+ return apiError.write(res);
+ }
+
+ console.error('internal server error:', err);
+
+ const services = globalThis.services;
+ if ( services && services.has('alarm') ) {
+ const alarm = services.get('alarm');
+ alarm.create('api_error_handler', err.message, {
+ error: err,
+ url: req.url,
+ method: req.method,
+ body: req.body,
+ headers: req.headers,
+ });
+ }
+
+ req.__error_handled = true;
+
+ // Other errors should provide as little information
+ // to the client as possible for security reasons.
+ return res.send(500, 'Internal Server Error');
+};
diff --git a/src/backend/src/modules/web/lib/eggspress.js b/src/backend/src/modules/web/lib/eggspress.js
new file mode 100644
index 000000000..5cfb220fb
--- /dev/null
+++ b/src/backend/src/modules/web/lib/eggspress.js
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const express = require('express');
+const multer = require('multer');
+const multest = require('@heyputer/multest');
+const api_error_handler = require('./api_error_handler.js');
+
+const fsBeforeMW = require('../../../middleware/fs.js');
+const APIError = require('../../../api/APIError.js');
+const { Context } = require('../../../util/context.js');
+const { subdomain } = require('../../../helpers.js');
+
+/**
+ * eggspress() is a factory function for creating express routers.
+ *
+ * @param {*} route the route to the router
+ * @param {*} settings the settings for the router. The following
+ * properties are supported:
+ * - auth: whether or not to use the auth middleware
+ * - fs: whether or not to use the fs middleware
+ * - json: whether or not to use the json middleware
+ * - customArgs: custom arguments to pass to the router
+ * - allowedMethods: the allowed HTTP methods
+ * @param {*} handler the handler for the router
+ * @returns {express.Router} the router
+ */
+module.exports = function eggspress (route, settings, handler) {
+ const router = express.Router();
+ const mw = [];
+ const afterMW = [];
+
+ // These flags enable specific middleware.
+ if ( settings.abuse ) mw.push(require('../../../middleware/abuse')(settings.abuse));
+ if ( settings.auth ) mw.push(require('../../../middleware/auth'));
+ if ( settings.auth2 ) mw.push(require('../../../middleware/auth2'));
+ if ( settings.fs ) {
+ mw.push(fsBeforeMW);
+ }
+ if ( settings.verified ) mw.push(require('../../../middleware/verified'));
+ if ( settings.json ) mw.push(express.json());
+
+ // The `files` setting is an array of strings. Each string is the name
+ // of a multipart field that contains files. `multer` is used to parse
+ // the multipart request and store the files in `req.files`.
+ if ( settings.files ) {
+ for ( const key of settings.files ) {
+ mw.push(multer().array(key));
+ }
+ }
+
+ if ( settings.multest ) {
+ mw.push(multest());
+ }
+
+ // The `multipart_jsons` setting is an array of strings. Each string
+ // is the name of a multipart field that contains JSON. This middleware
+ // parses the JSON in each field and stores the result in `req.body`.
+ if ( settings.multipart_jsons ) {
+ for ( const key of settings.multipart_jsons ) {
+ mw.push((req, res, next) => {
+ try {
+ if ( ! Array.isArray(req.body[key]) ) {
+ req.body[key] = [JSON.parse(req.body[key])];
+ } else {
+ req.body[key] = req.body[key].map(JSON.parse);
+ }
+ } catch (e) {
+ return res.status(400).send({
+ error: {
+ message: `Invalid JSON in multipart field ${key}`
+ }
+ });
+ }
+ next();
+ });
+ }
+ }
+
+ // The `alias` setting is an object. Each key is the name of a
+ // parameter. Each value is the name of a parameter that should
+ // be aliased to the key.
+ if ( settings.alias ) {
+ for ( const alias in settings.alias ) {
+ const target = settings.alias[alias];
+ mw.push((req, res, next) => {
+ const values = req.method === 'GET' ? req.query : req.body;
+ if ( values[alias] ) {
+ values[target] = values[alias];
+ }
+ next();
+ });
+ }
+ }
+
+ // The `parameters` setting is an object. Each key is the name of a
+ // parameter. Each value is a `Param` object. The `Param` object
+ // specifies how to validate the parameter.
+ if ( settings.parameters ) {
+ for ( const key in settings.parameters ) {
+ const param = settings.parameters[key];
+ mw.push(async (req, res, next) => {
+ if ( ! req.values ) req.values = {};
+
+ const values = req.method === 'GET' ? req.query : req.body;
+ const getParam = (key) => values[key];
+ try {
+ const result = await param.consolidate({ req, getParam });
+ req.values[key] = result;
+ } catch (e) {
+ api_error_handler(e, req, res, next);
+ return;
+ }
+ next();
+ });
+ }
+ }
+
+ // what if I wanted to pass arguments to, for example, `json`?
+ if ( settings.customArgs ) mw.push(settings.customArgs);
+
+ if ( settings.alarm_timeout ) {
+ mw.push((req, res, next) => {
+ setTimeout(() => {
+ if ( ! res.headersSent ) {
+ const log = req.services.get('log-service').create('eggspress:timeout');
+ const errors = req.services.get('error-service').create(log);
+ let id = Array.isArray(route) ? route[0] : route;
+ id = id.replace(/\//g, '_');
+ errors.report(id, {
+ source: new Error('Response timed out.'),
+ message: 'Response timed out.',
+ trace: true,
+ alarm: true,
+ });
+ }
+ }, settings.alarm_timeout);
+ next();
+ });
+ }
+
+ if ( settings.response_timeout ) {
+ mw.push((req, res, next) => {
+ setTimeout(() => {
+ if ( ! res.headersSent ) {
+ api_error_handler(APIError.create('response_timeout'), req, res, next);
+ }
+ }, settings.response_timeout);
+ next();
+ });
+ }
+
+ if ( settings.mw ) mw.push(...settings.mw);
+
+ const errorHandledHandler = async function (req, res, next) {
+ if ( settings.subdomain ) {
+ if ( subdomain(req) !== settings.subdomain ) {
+ return next();
+ }
+ }
+ try {
+ const expected_ctx = res.locals.ctx;
+ const received_ctx = Context.get(undefined, { allow_fallback: true });
+
+ if ( expected_ctx != received_ctx ) {
+ await expected_ctx.arun(async () => {
+ await handler(req, res, next);
+ });
+ } else await handler(req, res, next);
+ } catch (e) {
+ api_error_handler(e, req, res, next);
+ }
+ };
+
+ if ( settings.allowedMethods.includes('GET') ) {
+ router.get(route, ...mw, errorHandledHandler, ...afterMW);
+ }
+
+ if ( settings.allowedMethods.includes('POST') ) {
+ router.post(route, ...mw, errorHandledHandler, ...afterMW);
+ }
+
+ return router;
+}
\ No newline at end of file
diff --git a/src/backend/src/routers/filesystem_api/write.js b/src/backend/src/routers/filesystem_api/write.js
index 2bb289b7f..de0f4f65b 100644
--- a/src/backend/src/routers/filesystem_api/write.js
+++ b/src/backend/src/routers/filesystem_api/write.js
@@ -25,7 +25,6 @@ const { Context } = require('../../util/context.js');
const Busboy = require('busboy');
const { TeePromise } = require('../../util/promise.js');
const APIError = require('../../api/APIError.js');
-const api_error_handler = require('../../api/api_error_handler.js');
const { valid_file_size } = require('../../util/validutil.js');
// -----------------------------------------------------------------------//
@@ -172,13 +171,8 @@ module.exports = eggspress(['/up', '/write'], {
const values = req.method === 'GET' ? req.query : req.body;
const getParam = (key) => values[key];
- try {
- const result = await param.consolidate({ req, getParam });
- req.values[key] = result;
- } catch (e) {
- api_error_handler(e, req, res, next);
- return;
- }
+ const result = await param.consolidate({ req, getParam });
+ req.values[key] = result;
}
if ( req.body.size === undefined ) {
diff --git a/src/backend/src/routers/hosting/puter-site.js b/src/backend/src/routers/hosting/puter-site.js
index a2680a6be..d9d81bf48 100644
--- a/src/backend/src/routers/hosting/puter-site.js
+++ b/src/backend/src/routers/hosting/puter-site.js
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
const { AdvancedBase } = require("@heyputer/putility");
-const api_error_handler = require("../../api/api_error_handler");
+const api_error_handler = require("../../modules/web/lib/api_error_handler");
const config = require("../../config");
const { get_user, get_app, id2path } = require("../../helpers");
const { Context } = require("../../util/context");