feat(api): standardize errors as problem details

This commit is contained in:
dmg
2026-08-01 14:42:19 -04:00
parent 40abab7abc
commit 2657399628
16 changed files with 441 additions and 17 deletions
+29
View File
@@ -45,3 +45,32 @@ export const velocityAccessResponseSchema = z.discriminatedUnion("allowed", [
]);
export type VelocityAccessResponse = z.infer<typeof velocityAccessResponseSchema>;
export const problemDetailsSchema = z.object({
type: z.string().min(1),
title: z.string().min(1),
status: z.number().int().min(100).max(599),
detail: z.string().min(1).optional(),
instance: z.string().min(1).optional(),
extensions: z.record(z.string(), z.unknown()).optional(),
});
export type ProblemDetails = z.infer<typeof problemDetailsSchema>;
export function problemDetails(
type: string,
title: string,
status: number,
detail?: string,
instance?: string,
extensions?: Record<string, unknown>,
): ProblemDetails {
return {
type,
title,
status,
...(detail ? { detail } : {}),
...(instance ? { instance } : {}),
...(extensions ? { extensions } : {}),
};
}
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { problemDetails, problemDetailsSchema } from "../src/index";
describe("RFC 9457 Problem Details", () => {
it("builds the same problem shape as game-ingest-server", () => {
const problem = problemDetails(
"urn:error:invalid-velocity-access-request",
"Invalid Velocity access request",
400,
"The request body does not match the required contract.",
"/api/velocity/access",
{ issues: [{ path: "minecraftUuid", message: "Invalid UUID" }] },
);
expect(problemDetailsSchema.parse(problem)).toEqual({
type: "urn:error:invalid-velocity-access-request",
title: "Invalid Velocity access request",
status: 400,
detail: "The request body does not match the required contract.",
instance: "/api/velocity/access",
extensions: { issues: [{ path: "minecraftUuid", message: "Invalid UUID" }] },
});
});
it("omits optional members instead of serializing undefined values", () => {
expect(problemDetails("urn:error:internal", "Internal server error", 500)).toEqual({
type: "urn:error:internal",
title: "Internal server error",
status: 500,
});
});
it("rejects values outside valid HTTP status codes", () => {
expect(() => problemDetailsSchema.parse({ type: "urn:error:test", title: "Test", status: 42 }))
.toThrow();
});
});