diff --git a/nix/tests/test-bundle-starts.nix b/nix/tests/test-bundle-starts.nix
index 6ae19c13..89d26706 100644
--- a/nix/tests/test-bundle-starts.nix
+++ b/nix/tests/test-bundle-starts.nix
@@ -47,6 +47,8 @@ in
LOG_REQUESTS = "-"; # Log all requests
LOG_VALIDATION_ERRORS = true;
LOG_API_ERRORS = true;
+ CDN_SIGNATURE_PATH = "${pkgs.writeText "cdnSig" "meow"}";
+ REQUEST_SIGNATURE_PATH = "${pkgs.writeText "reqSig" "meow"}";
};
ipcMethod = withIpc;
@@ -96,7 +98,7 @@ in
];
requires = [ "spacebar-api.service" ];
environment = {
- TEST_APPSETTINGS_PATH=testConfigPath;
+ TEST_APPSETTINGS_PATH = testConfigPath;
};
serviceConfig = {
ExecStart = "${testBin} -reporter verbose -parallelAlgorithm aggressive -maxThreads unlimited -preEnumerateTheories";
@@ -152,7 +154,7 @@ in
machine.wait_for_unit("spacebar-tests")
machine.wait_until_fails("systemctl show spacebar-tests.service | grep 'SubState=running' -q") # ... wait for the unit to exit in any way
-
+
testUnitState = machine.get_unit_property("spacebar-tests.service", "SubState");
t.assertNotEqual("failed", testUnitState)
'';
diff --git a/src/cdn/Server.ts b/src/cdn/Server.ts
index 622e0911..4e162c05 100644
--- a/src/cdn/Server.ts
+++ b/src/cdn/Server.ts
@@ -26,6 +26,7 @@ import { ProcessLifecycle, SystemdLifecycle } from "../util/util/ProcessLifecycl
import { Monitoring } from "../util/monitoring/Monitoring";
import guildProfilesRoute from "./routes/guild-profiles";
import { storage } from "./util";
+import { ErrorHandler } from "@spacebar/cdn/util/ErrorHandler";
export type CDNServerOptions = ServerOptions;
@@ -65,6 +66,7 @@ export class CDNServer extends Server {
this.app.disable("x-powered-by");
+ this.app.use(ErrorHandler);
this.app.use(CORS);
this.app.use(BodyParser({ inflate: true, limit: "10mb" }));
diff --git a/src/cdn/util/ErrorHandler.ts b/src/cdn/util/ErrorHandler.ts
new file mode 100644
index 00000000..19d9203f
--- /dev/null
+++ b/src/cdn/util/ErrorHandler.ts
@@ -0,0 +1,83 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program 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 <https://www.gnu.org/licenses/>.
+*/
+
+import { NextFunction, Request, Response } from "express";
+import { HTTPError } from "lambert-server/HTTPError";
+import { ApiError, FieldError, FieldErrors } from "@spacebar/util";
+import { StringLengthOutOfBoundsException } from "@spacebar/extensions";
+const EntityNotFoundErrorRegex = /"(\w+)"/;
+
+export function ErrorHandler(error: Error & { type?: string }, req: Request, res: Response, next: NextFunction) {
+ if (!error) return next();
+
+ // Convert custom generic exception classes to spacebar errors
+ if (error instanceof StringLengthOutOfBoundsException)
+ error = FieldErrors({
+ [error.key]: {
+ code: "BASE_TYPE_BAD_LENGTH",
+ message: req.t("common:field.BASE_TYPE_BAD_LENGTH", {
+ length: `${error.min} - ${error.max}`,
+ }),
+ },
+ });
+
+ try {
+ let code = 400;
+ let httpcode = code;
+ let message = error?.toString();
+ let errors = undefined;
+ let _ajvErrors = undefined;
+
+ if (process.env.LOG_API_ERRORS === "true") console.error("[ErrorHandler] Uncaught exception:", error);
+
+ if (error instanceof HTTPError && error.code) code = httpcode = error.code;
+ else if (error instanceof ApiError) {
+ code = error.code;
+ message = error.message;
+ httpcode = error.httpStatus;
+ } else if (error.name === "EntityNotFoundError") {
+ message = `${error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"} could not be found`;
+ code = httpcode = 404;
+ } else if (error instanceof FieldError) {
+ code = Number(error.code);
+ message = error.message;
+ errors = error.errors;
+ _ajvErrors = error._ajvErrors;
+ } else if (error?.type == "entity.parse.failed") {
+ // body-parser failed
+ httpcode = 400;
+ code = 50109;
+ message = "The request body contains invalid JSON.";
+ } else {
+ console.error(`[Error] ${code} ${req.url}\n`, errors ?? error, "\nbody:", req.body);
+
+ if (req.server?.options?.production) {
+ // don't expose internal errors to the user, instead human errors should be thrown as HTTPError
+ message = "Internal Server Error";
+ }
+ code = httpcode = 500;
+ }
+
+ if (httpcode > 511) httpcode = 400;
+
+ res.status(httpcode).json({ code, message, errors, _ajvErrors, request: `${req.method} ${req.url}` });
+ } catch (error) {
+ console.error(`[Internal Server Error] 500`, error);
+ return res.status(500).json({ code: 500, message: `Internal server error while handling error`, request: `${req.method} ${req.url}` });
+ }
+}
diff --git a/src/util/util/ProcessLifecycle.ts b/src/util/util/ProcessLifecycle.ts
index f625ae77..a026b3f0 100644
--- a/src/util/util/ProcessLifecycle.ts
+++ b/src/util/util/ProcessLifecycle.ts
@@ -18,8 +18,6 @@
import EventEmitter from "node:events";
import whyIsNodeRunning from "why-is-node-running";
-import net from "node:net";
-import * as dgram from "node:dgram";
import { DgramSocket } from "node-unix-socket";
interface ProcessLifecycleEvents {
diff --git a/src/util/util/cdn.ts b/src/util/util/cdn.ts
index ef934f78..a2dc8c68 100644
--- a/src/util/util/cdn.ts
+++ b/src/util/util/cdn.ts
@@ -62,7 +62,7 @@ export async function handleFile(path: string, body?: string): Promise<string |
return id;
} catch (error) {
console.error(error);
- throw new HTTPError("Invalid " + path);
+ throw new HTTPError(`Internal CDN error: Invalid response from POST $CDN${path}: ${(error as Error).message}`);
}
}
|