fix: improve web media fetch errors
This commit is contained in:
@@ -76,6 +76,26 @@ describe("web media loading", () => {
|
|||||||
fetchMock.mockRestore();
|
fetchMock.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes URL + status in fetch errors", async () => {
|
||||||
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
body: true,
|
||||||
|
text: async () => "Not Found",
|
||||||
|
headers: { get: () => null },
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not Found",
|
||||||
|
url: "https://example.com/missing.jpg",
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
loadWebMedia("https://example.com/missing.jpg", 1024 * 1024),
|
||||||
|
).rejects.toThrow(
|
||||||
|
/Failed to fetch media from https:\/\/example\.com\/missing\.jpg.*HTTP 404/i,
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchMock.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses content-disposition filename when available", async () => {
|
it("uses content-disposition filename when available", async () => {
|
||||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ function parseContentDispositionFileName(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readErrorBodySnippet(
|
||||||
|
res: Response,
|
||||||
|
maxChars = 200,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const text = await res.text();
|
||||||
|
if (!text) return undefined;
|
||||||
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
||||||
|
if (!collapsed) return undefined;
|
||||||
|
if (collapsed.length <= maxChars) return collapsed;
|
||||||
|
return `${collapsed.slice(0, maxChars)}…`;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWebMediaInternal(
|
async function loadWebMediaInternal(
|
||||||
mediaUrl: string,
|
mediaUrl: string,
|
||||||
options: WebMediaOptions = {},
|
options: WebMediaOptions = {},
|
||||||
@@ -85,9 +101,26 @@ async function loadWebMediaInternal(
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore parse errors; leave undefined
|
// ignore parse errors; leave undefined
|
||||||
}
|
}
|
||||||
const res = await fetch(mediaUrl);
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await fetch(mediaUrl);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to fetch media from ${mediaUrl}: ${String(err)}`);
|
||||||
|
}
|
||||||
if (!res.ok || !res.body) {
|
if (!res.ok || !res.body) {
|
||||||
throw new Error(`Failed to fetch media: HTTP ${res.status}`);
|
const statusText = res.statusText ? ` ${res.statusText}` : "";
|
||||||
|
const redirected =
|
||||||
|
res.url && res.url !== mediaUrl ? ` (redirected to ${res.url})` : "";
|
||||||
|
let detail = `HTTP ${res.status}${statusText}`;
|
||||||
|
if (!res.body) {
|
||||||
|
detail = `HTTP ${res.status}${statusText}; empty response body`;
|
||||||
|
} else if (!res.ok) {
|
||||||
|
const snippet = await readErrorBodySnippet(res);
|
||||||
|
if (snippet) detail += `; body: ${snippet}`;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch media from ${mediaUrl}${redirected}: ${detail}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const array = Buffer.from(await res.arrayBuffer());
|
const array = Buffer.from(await res.arrayBuffer());
|
||||||
const headerFileName = parseContentDispositionFileName(
|
const headerFileName = parseContentDispositionFileName(
|
||||||
|
|||||||
Reference in New Issue
Block a user