Add errorToResponse mapper with TDD tests covering all mapping rules

This commit is contained in:
2026-04-15 00:31:37 +03:00
parent 2eb118cb8b
commit fc57556010
2 changed files with 99 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest";
import { errorToResponse } from "./map.js";
import { ApiHttpError, ApiTimeoutError } from "@/shared/api/errors";
describe("errorToResponse", () => {
it("maps ApiHttpError(404) to not_found", () => {
const result = errorToResponse(new ApiHttpError("Not found", 404));
expect(result).toEqual({ status: 404, errorCode: "not_found" });
});
it("maps ApiHttpError(500) to internal", () => {
const result = errorToResponse(new ApiHttpError("Server error", 500));
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps ApiHttpError(502) to internal", () => {
const result = errorToResponse(new ApiHttpError("Bad gateway", 502));
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps ApiHttpError(503) to internal (5xx range)", () => {
const result = errorToResponse(new ApiHttpError("Service unavailable", 503));
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps ApiHttpError(599) to internal (5xx boundary)", () => {
const result = errorToResponse(new ApiHttpError("Edge case", 599));
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps ApiTimeoutError to unavailable with Retry-After header", () => {
const result = errorToResponse(new ApiTimeoutError(5000));
expect(result).toEqual({
status: 503,
headers: { "Retry-After": "30" },
errorCode: "unavailable",
});
});
it("maps unknown Error to internal", () => {
const result = errorToResponse(new Error("something broke"));
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps non-Error value to internal", () => {
const result = errorToResponse("string error");
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps null to internal", () => {
const result = errorToResponse(null);
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
it("maps ApiHttpError with non-5xx/non-404 status to internal", () => {
const result = errorToResponse(new ApiHttpError("Forbidden", 403));
expect(result).toEqual({ status: 500, errorCode: "internal" });
});
});
+40
View File
@@ -0,0 +1,40 @@
import { ApiHttpError, ApiTimeoutError } from "@/shared/api/errors";
export interface ErrorResponse {
status: 404 | 500 | 503;
headers?: Record<string, string>;
errorCode: "not_found" | "internal" | "unavailable";
}
/**
* Maps an application error to a structured HTTP-like response.
* Consumed by SSR loader paths and error page routing.
*
* Mapping rules (design spec S4.6):
* - ApiHttpError 404 -> { status: 404, errorCode: "not_found" }
* - ApiHttpError 500-599 -> { status: 500, errorCode: "internal" }
* - ApiTimeoutError -> { status: 503, Retry-After: 30, errorCode: "unavailable" }
* - Everything else -> { status: 500, errorCode: "internal" }
*/
export function errorToResponse(error: unknown): ErrorResponse {
if (error instanceof ApiHttpError) {
if (error.status === 404) {
return { status: 404, errorCode: "not_found" };
}
if (error.status >= 500 && error.status <= 599) {
return { status: 500, errorCode: "internal" };
}
// Any other HTTP status (e.g. 403) falls through to the default
return { status: 500, errorCode: "internal" };
}
if (error instanceof ApiTimeoutError) {
return {
status: 503,
headers: { "Retry-After": "30" },
errorCode: "unavailable",
};
}
return { status: 500, errorCode: "internal" };
}