Alle drei Admin-Upload-Felder riefen response.json() ungeprueft auf. Kam statt JSON eine HTML-Seite zurueck, warf der Parser Unexpected token '<', "<!DOCTYPE "... is not valid JSON und genau dieser Text landete im Formular. Damit war die eigentliche Ursache vollstaendig verdeckt: weder der HTTP-Status noch die Schicht, die abgebrochen hat, waren daraus ablesbar. Der Punkt dabei: die Upload-Route-Handler antworten ausnahmslos mit JSON, auch in jedem Fehlerfall — lokal verifiziert bis hinauf zur Limit-Ueberschreitung (105 MB -> 413 mit JSON-Body). Trifft im Browser trotzdem HTML ein, stammt die Antwort gar nicht aus der Anwendung, sondern aus einer Schicht davor: Reverse-Proxy, CDN oder Gateway, das den Request wegen zu grossem Body abgewiesen oder wegen Zeitueberschreitung beim Transcoding abgebrochen hat. readUploadResponse() in lib/upload-client.ts prueft deshalb zuerst den Content-Type und reicht andernfalls den HTTP-Status nach oben, statt zu parsen. Die Felder zeigen daraus eine Meldung, die den Status nennt und die wahrscheinliche Ursache benennt (413 = vom Proxy abgewiesen, 502/503/504/522/524 = Zeitueberschreitung beim Umrechnen, sonst unerwartet). Ein Verbindungsabbruch, bei dem fetch() selbst wirft, wird getrennt gemeldet. Das behebt nicht die Ursache jenseits der Anwendung — es macht sie zum ersten Mal ablesbar, statt sie hinter einem Parser-Fehler zu verbergen.
71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
import { describe, test, expect } from "vitest";
|
|
import { readUploadResponse } from "./upload-client";
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
function htmlResponse(status: number): Response {
|
|
return new Response("<!DOCTYPE html><html><body>Gateway Time-out</body></html>", {
|
|
status,
|
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
});
|
|
}
|
|
|
|
describe("readUploadResponse", () => {
|
|
test("returns the path of a successful upload", async () => {
|
|
const result = await readUploadResponse(jsonResponse({ path: "/uploads/abc.mp4" }));
|
|
expect(result).toEqual({ ok: true, path: "/uploads/abc.mp4" });
|
|
});
|
|
|
|
test("passes through the server's own error message", async () => {
|
|
const result = await readUploadResponse(jsonResponse({ error: "Die Datei ist zu groß (max. 100 MB)." }, 413));
|
|
expect(result).toEqual({ ok: false, kind: "message", message: "Die Datei ist zu groß (max. 100 MB)." });
|
|
});
|
|
|
|
test("reports an HTML error page by status instead of trying to parse it as JSON", async () => {
|
|
// Der eigentliche Bug: response.json() auf einer HTML-Fehlerseite wirft
|
|
// "Unexpected token '<', "<!DOCTYPE "... is not valid JSON" und
|
|
// verdeckt damit vollstaendig, WAS schiefging. Eine Fehlerseite kommt
|
|
// nie von diesem Endpunkt selbst (der antwortet immer mit JSON),
|
|
// sondern von einer Schicht davor — Reverse-Proxy, CDN oder Gateway.
|
|
const result = await readUploadResponse(htmlResponse(504));
|
|
expect(result).toEqual({ ok: false, kind: "status", status: 504 });
|
|
});
|
|
|
|
test("reports a proxy 413 by status (the request never reached the app)", async () => {
|
|
const result = await readUploadResponse(htmlResponse(413));
|
|
expect(result).toEqual({ ok: false, kind: "status", status: 413 });
|
|
});
|
|
|
|
test("treats a 200 response that is not JSON as a status failure", async () => {
|
|
const result = await readUploadResponse(htmlResponse(200));
|
|
expect(result).toEqual({ ok: false, kind: "status", status: 200 });
|
|
});
|
|
|
|
test("treats JSON that is not parseable as a status failure", async () => {
|
|
const broken = new Response("{ truncated", { status: 200, headers: { "content-type": "application/json" } });
|
|
expect(await readUploadResponse(broken)).toEqual({ ok: false, kind: "status", status: 200 });
|
|
});
|
|
|
|
test("treats a JSON success body without a path as a status failure", async () => {
|
|
expect(await readUploadResponse(jsonResponse({}))).toEqual({ ok: false, kind: "status", status: 200 });
|
|
});
|
|
|
|
test("treats an empty body with an error status as a status failure", async () => {
|
|
const empty = new Response(null, { status: 502 });
|
|
expect(await readUploadResponse(empty)).toEqual({ ok: false, kind: "status", status: 502 });
|
|
});
|
|
|
|
test("accepts a JSON content type that carries extra parameters", async () => {
|
|
const res = new Response(JSON.stringify({ path: "/uploads/x.jpg" }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
});
|
|
expect(await readUploadResponse(res)).toEqual({ ok: true, path: "/uploads/x.jpg" });
|
|
});
|
|
});
|