summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-07-11 07:22:18 +0200
committerRory& <root@rory.gay>2026-07-11 07:22:18 +0200
commit0c96e781100d1326386c509b58c9441faeb83462 (patch)
tree31f01d62a56007e993c8645347d0b53094020c44
parentWorking unit failure detection (diff)
downloadserver-ts-0c96e781100d1326386c509b58c9441faeb83462.tar.xz
Use error handler in CDN, clearer CDN errors, fix cdn in nixosTests
-rw-r--r--nix/tests/test-bundle-starts.nix6
-rw-r--r--src/cdn/Server.ts2
-rw-r--r--src/cdn/util/ErrorHandler.ts83
-rw-r--r--src/util/util/ProcessLifecycle.ts2
-rw-r--r--src/util/util/cdn.ts2
5 files changed, 90 insertions, 5 deletions
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}`); } }