86 lines
2.8 KiB
JavaScript
86 lines
2.8 KiB
JavaScript
// Kommunikation mit der API. Einziger Ort, an dem fetch() aufgerufen wird.
|
|
"use strict";
|
|
|
|
export class ApiError extends Error {
|
|
constructor(status, detail) {
|
|
super(detail);
|
|
this.name = "ApiError";
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export class OfflineError extends Error {
|
|
constructor() {
|
|
super("Keine Verbindung zum Server.");
|
|
this.name = "OfflineError";
|
|
}
|
|
}
|
|
|
|
/** Das CSRF-Token steht in einem lesbaren Cookie. Das Session-Cookie ist
|
|
* HttpOnly und für JavaScript unsichtbar - genau so soll es sein. */
|
|
function csrfToken() {
|
|
const hit = document.cookie
|
|
.split(";")
|
|
.map((c) => c.trim())
|
|
.find((c) => c.startsWith("ea_csrf="));
|
|
return hit ? decodeURIComponent(hit.slice("ea_csrf=".length)) : "";
|
|
}
|
|
|
|
/** Übersetzt die Fehlerform von FastAPI in einen Satz für Menschen.
|
|
* Bei Validierungsfehlern ist "detail" eine Liste, keine Zeichenkette. */
|
|
function describe(status, data) {
|
|
const detail = data && data.detail;
|
|
if (typeof detail === "string") return detail;
|
|
if (Array.isArray(detail) && detail.length) {
|
|
const first = detail[0];
|
|
const field = Array.isArray(first.loc) ? first.loc[first.loc.length - 1] : "";
|
|
return field ? `Ungültige Eingabe bei "${field}": ${first.msg}` : first.msg;
|
|
}
|
|
if (status === 401) return "Nicht angemeldet.";
|
|
if (status === 429) return "Zu viele Versuche. Bitte einen Moment warten.";
|
|
if (status >= 500) return "Der Server hat einen Fehler gemeldet.";
|
|
return `Unerwarteter Fehler (HTTP ${status}).`;
|
|
}
|
|
|
|
export async function api(path, { method = "GET", body } = {}) {
|
|
const headers = {};
|
|
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
if (method !== "GET" && method !== "HEAD") headers["X-CSRF-Token"] = csrfToken();
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(path, {
|
|
method,
|
|
headers,
|
|
credentials: "same-origin",
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
} catch {
|
|
// Netzwerkfehler von HTTP-Fehlern trennen: In Phase 4 entscheidet
|
|
// genau das, ob eine Änderung in die Outbox wandert.
|
|
throw new OfflineError();
|
|
}
|
|
|
|
const text = await response.text();
|
|
let data = null;
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
throw new ApiError(
|
|
response.status,
|
|
`Unerwartete Antwort vom Server (HTTP ${response.status}).`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!response.ok) throw new ApiError(response.status, describe(response.status, data));
|
|
return data;
|
|
}
|
|
|
|
export const get = (path) => api(path);
|
|
export const post = (path, body) => api(path, { method: "POST", body });
|
|
export const patch = (path, body) => api(path, { method: "PATCH", body });
|
|
export const put = (path, body) => api(path, { method: "PUT", body });
|
|
export const del = (path) => api(path, { method: "DELETE" });
|