File size: 3,194 Bytes
27329ef
 
 
 
5bbf908
27329ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a227722
 
f7a6136
5bbf908
27329ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a227722
 
 
 
 
 
 
 
 
 
 
 
f7a6136
 
 
 
 
 
27329ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { errorEnvelopeSchema } from "@oloka/contracts";
import type { FastifyInstance } from "fastify";
import { ZodError } from "zod";
import { ApplicationError, publicErrorCatalog } from "./application-error.js";
import { readMaterializationStage } from "../composition/composition-materializer.js";

export function registerErrorHandler(app: FastifyInstance): void {
  app.setErrorHandler((error, request, reply) => {
    const applicationError = normalizeApplicationError(error);
    const definition = publicErrorCatalog[applicationError.code];
    const requestId = request.id;
    reply.header("x-request-id", requestId);
    for (const [name, value] of Object.entries(
      applicationError.responseHeaders ?? {},
    )) {
      reply.header(name, value);
    }
    if (applicationError.code === "INTERNAL_ERROR") {
      request.log.error(
        {
          event: "http.request.failed",
          requestId,
          internalCause: applicationError.internalCause ?? "unexpected_error",
          errorType: error instanceof Error ? error.name : "UnknownError",
          errorCode: readErrorCode(error),
          errorSyscall: readErrorSyscall(error),
          previewStage: readPreviewStage(error),
          materializationStage: readMaterializationStage(error),
        },
        "request failed",
      );
    }
    return reply.code(definition.statusCode).send(
      errorEnvelopeSchema.parse({
        error: {
          code: applicationError.code,
          retryable: definition.retryable,
          messageKey: definition.messageKey,
          suggestedAction: definition.suggestedAction,
          requestId,
        },
      }),
    );
  });
}

function readErrorCode(error: unknown): string | undefined {
  if (typeof error !== "object" || error === null) return undefined;
  const code = (error as { code?: unknown }).code;
  return typeof code === "string" ? code : undefined;
}

function readErrorSyscall(error: unknown): string | undefined {
  if (typeof error !== "object" || error === null) return undefined;
  const syscall = (error as { syscall?: unknown }).syscall;
  return typeof syscall === "string" ? syscall : undefined;
}

function readPreviewStage(error: unknown): string | undefined {
  if (typeof error !== "object" || error === null) return undefined;
  const stage = (error as { previewStage?: unknown }).previewStage;
  return typeof stage === "string" ? stage : undefined;
}

function normalizeApplicationError(error: unknown): ApplicationError {
  if (error instanceof ApplicationError) return error;
  if (error instanceof ZodError) {
    return new ApplicationError("VALIDATION_ERROR", "request_schema_invalid");
  }
  if (isFastifyValidationError(error)) {
    return new ApplicationError(
      "VALIDATION_ERROR",
      "fastify_validation_failed",
    );
  }
  return new ApplicationError("INTERNAL_ERROR", "unexpected_application_error");
}

function isFastifyValidationError(error: unknown): boolean {
  if (typeof error !== "object" || error === null) return false;
  const candidate = error as { statusCode?: unknown; code?: unknown };
  return (
    candidate.statusCode === 400 || candidate.code === "FST_ERR_VALIDATION"
  );
}