mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
d02659d325
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import path from "node:path";
|
|
import formidable from "formidable";
|
|
import type Koa from "koa";
|
|
import { escape, isNil, snakeCase } from "es-toolkit/compat";
|
|
import env from "@server/env";
|
|
import { ClientClosedRequestError, InternalError } from "@server/errors";
|
|
import { requestErrorHandler } from "@server/logging/sentry";
|
|
import { addTags, getRootSpanFromRequestContext } from "@server/logging/tracer";
|
|
|
|
let errorHtmlCache: Buffer | undefined;
|
|
|
|
export default function onerror(app: Koa) {
|
|
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
|
|
app.context.onerror = function (err: any) {
|
|
// Don't do anything if there is no error, this allows you to pass `this.onerror` to node-style callbacks.
|
|
if (isNil(err)) {
|
|
return;
|
|
}
|
|
|
|
err = wrapInNativeError(err);
|
|
|
|
// Client aborted errors are a 500 by default, but 499 is more appropriate
|
|
if (err instanceof formidable.errors.FormidableError) {
|
|
if (err.internalCode === 1002) {
|
|
err = ClientClosedRequestError();
|
|
}
|
|
}
|
|
|
|
// Push only errors explicitly marked for Sentry reporting.
|
|
// For unknown errors without isReportable property, report them as well
|
|
// to ensure we don't miss unexpected errors.
|
|
const shouldReport =
|
|
err.isReportable === true ||
|
|
(err.isReportable !== false &&
|
|
(typeof err.status !== "number" ||
|
|
!http.STATUS_CODES[err.status] ||
|
|
err.status === 500));
|
|
|
|
if (shouldReport) {
|
|
requestErrorHandler(err, this);
|
|
|
|
if (!(err instanceof InternalError)) {
|
|
if (env.ENVIRONMENT === "test") {
|
|
// oxlint-disable-next-line no-console
|
|
console.error(err);
|
|
}
|
|
err = InternalError();
|
|
}
|
|
} else {
|
|
// Clear error tags that dd-trace's Koa plugin sets automatically
|
|
// when an exception propagates through middleware, so that
|
|
// non-reportable errors are not flagged as errors in DataDog.
|
|
const span = getRootSpanFromRequestContext(this);
|
|
if (span) {
|
|
addTags({ error: false }, span);
|
|
}
|
|
}
|
|
|
|
const headerSent = this.headerSent || !this.writable;
|
|
if (headerSent) {
|
|
err.headerSent = true;
|
|
}
|
|
|
|
// Nothing we can do here other than delegate to the app-level handler and log.
|
|
if (headerSent) {
|
|
return;
|
|
}
|
|
|
|
this.set(err.headers);
|
|
this.status = err.status;
|
|
this.type = this.accepts("json", "html") || "json";
|
|
|
|
if (this.type === "text/html") {
|
|
this.body = readErrorFile()
|
|
.toString()
|
|
.replace(/\/\/inject-message\/\//g, escape(err.message))
|
|
.replace(/\/\/inject-status\/\//g, escape(err.status))
|
|
.replace(/\/\/inject-stack\/\//g, escape(err.stack));
|
|
} else {
|
|
this.body = JSON.stringify({
|
|
ok: false,
|
|
error: snakeCase(err.id),
|
|
status: Number(err.status),
|
|
message: String(err.message || err.name),
|
|
data: err.errorData ?? undefined,
|
|
});
|
|
}
|
|
|
|
this.res.end(this.body);
|
|
};
|
|
|
|
return app;
|
|
}
|
|
|
|
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function wrapInNativeError(err: any): Error {
|
|
// When dealing with cross-globals a normal `instanceof` check doesn't work properly.
|
|
// See https://github.com/koajs/koa/issues/1466
|
|
const isNativeError =
|
|
Object.prototype.toString.call(err) === "[object Error]" ||
|
|
err instanceof Error;
|
|
|
|
if (isNativeError) {
|
|
return err as Error;
|
|
}
|
|
|
|
let errMsg = err;
|
|
if (typeof err === "object") {
|
|
try {
|
|
errMsg = JSON.stringify(err);
|
|
// oxlint-disable-next-line no-empty
|
|
} catch (_err) {
|
|
// Ignore
|
|
}
|
|
}
|
|
const newError = InternalError(`Non-error thrown: ${errMsg}`);
|
|
// err maybe an object, try to copy the name, message and stack to the new error instance
|
|
if (err) {
|
|
if (err.name) {
|
|
newError.name = err.name;
|
|
}
|
|
if (err.message) {
|
|
newError.message = err.message;
|
|
}
|
|
if (err.stack) {
|
|
newError.stack = err.stack;
|
|
}
|
|
if (err.status) {
|
|
newError.status = err.status;
|
|
}
|
|
if (err.headers) {
|
|
newError.headers = err.headers;
|
|
}
|
|
}
|
|
|
|
return newError;
|
|
}
|
|
|
|
function readErrorFile(): Buffer {
|
|
if (env.isDevelopment) {
|
|
return fs.readFileSync(path.join(__dirname, "error.dev.html"));
|
|
}
|
|
|
|
if (env.isProduction) {
|
|
return (
|
|
errorHtmlCache ??
|
|
(errorHtmlCache = fs.readFileSync(
|
|
path.join(__dirname, "error.prod.html")
|
|
))
|
|
);
|
|
}
|
|
|
|
return (
|
|
errorHtmlCache ??
|
|
(errorHtmlCache = fs.readFileSync(
|
|
path.join(__dirname, "static/error.dev.html")
|
|
))
|
|
);
|
|
}
|