mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
fix: preserve ky error semantics
This commit is contained in:
parent
5999cb1228
commit
18a5dbe058
3 changed files with 51 additions and 43 deletions
|
|
@ -8,6 +8,7 @@ export default defineConfig({
|
|||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? "github" : "list",
|
||||
use: {
|
||||
// Port 4173 isolates Playwright from the app's normal port 3000 server.
|
||||
baseURL: "http://localhost:4173",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
|
@ -20,7 +21,7 @@ export default defineConfig({
|
|||
webServer: {
|
||||
command: "npm run dev -- --port 4173 --strictPort",
|
||||
url: "http://localhost:4173",
|
||||
reuseExistingServer: false,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
import { HTTPError } from "ky";
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
import { isHTTPError, isNetworkError } from "ky";
|
||||
|
||||
export function mapHttpStatusToMessage(status: number, data?: unknown): string {
|
||||
if (status === 401) {
|
||||
|
|
@ -36,22 +26,15 @@ export function mapHttpStatusToMessage(status: number, data?: unknown): string {
|
|||
}
|
||||
|
||||
export function normalizeApiRequestError(error: Error): Error {
|
||||
if (!(error instanceof HTTPError)) {
|
||||
if (isHTTPError(error)) {
|
||||
error.message = mapHttpStatusToMessage(error.response.status, error.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
if (isNetworkError(error)) {
|
||||
error.message = "No response from server. Please check your connection.";
|
||||
return error;
|
||||
}
|
||||
|
||||
error.message = mapHttpStatusToMessage(error.response.status, error.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
export async function createApiErrorFromResponse(response: Response): Promise<ApiError> {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
data = undefined;
|
||||
}
|
||||
|
||||
return new ApiError(mapHttpStatusToMessage(response.status, data), response.status);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,54 @@
|
|||
import { HTTPError } from "ky";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import ky from "ky";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { normalizeApiRequestError } from "@/api/api-error";
|
||||
|
||||
function createHttpError(status: number, data: unknown): HTTPError {
|
||||
const response = new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const RESOURCE_URL = "https://example.test/api/resource";
|
||||
|
||||
function createClient(fetchImpl: typeof fetch) {
|
||||
return ky.create({
|
||||
fetch: fetchImpl,
|
||||
retry: 0,
|
||||
timeout: 50,
|
||||
hooks: { beforeError: [({ error }) => normalizeApiRequestError(error)] },
|
||||
});
|
||||
const request = new Request("https://example.test/api/resource");
|
||||
const error = new HTTPError(response, request, {} as never);
|
||||
error.data = data;
|
||||
return error;
|
||||
}
|
||||
|
||||
describe("normalizeApiRequestError", () => {
|
||||
it("uses Ky 2 pre-parsed response data for API messages", () => {
|
||||
const error = createHttpError(422, { message: "Validation failed" });
|
||||
it("surfaces Ky's pre-parsed JSON body for HTTP errors via the hook", async () => {
|
||||
const fetchImpl = vi.fn(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ message: "Validation failed" }), {
|
||||
status: 422,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
expect(normalizeApiRequestError(error)).toBe(error);
|
||||
expect(error.message).toBe("Validation failed");
|
||||
await expect(createClient(fetchImpl).get(RESOURCE_URL).json()).rejects.toThrow(
|
||||
"Validation failed",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the connection-error message for non-HTTP failures", () => {
|
||||
const error = new TypeError("fetch failed");
|
||||
it("reports a connection failure for network errors", async () => {
|
||||
const fetchImpl = vi.fn(() =>
|
||||
Promise.reject(new TypeError("fetch failed")),
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
expect(normalizeApiRequestError(error)).toBe(error);
|
||||
expect(error.message).toBe("No response from server. Please check your connection.");
|
||||
await expect(createClient(fetchImpl).get(RESOURCE_URL).json()).rejects.toThrow(
|
||||
"No response from server. Please check your connection.",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the original message for timeouts", async () => {
|
||||
const fetchImpl = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const signal = init?.signal;
|
||||
return new Promise<Response>((_resolve, reject) => {
|
||||
signal?.addEventListener("abort", () => reject(signal?.reason));
|
||||
});
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
await expect(createClient(fetchImpl).get(RESOURCE_URL).json()).rejects.toThrow(
|
||||
"Request timed out",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue