fix(api): Log-Flut begrenzen und Appwrite-Fehler strenger behandeln
- Kein Zugriffsprotokoll je Anfrage mehr (abgewiesene Anfragen an das unauthentifizierte /api/melden loggten sonst unbegrenzt); nur noch Ereignis- und 5xx-Zeilen, höchstens 120 pro Minute mit Sammelzeile - Compose: logging json-file, max-size 5m, max-file 2 - appwrite.js: 404 nur mit Typ row_not_found/storage_file_not_found als "fehlt", listRows ohne rows-Array wirft, redirect: error - Appwrite-Fehler: 502 bei 0/429/5xx, sonst 500 "Interner Fehler." - Veröffentlichen legt beide Dateien auf die sichtbaren Rechte fest (erst nach dem Anlegen der Zeile, Rollback bei Fehler) - setFilePermissions mit Promise.allSettled, gemeinsamer safeToken-Helfer - Tests: 85 (Log-Flut, Log-Budget, 404-Typen, Auth-Header, Konto-Antworten) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd1597862a
commit
cd55d4c891
8 files changed
+423
-31
No files matched your search
+25
-15
@@ -4,16 +4,21 @@
|
||||
* Schmaler REST-Client für Appwrite 1.8 (Node 22, nur Bordmittel) mit dem Server-Key des API-Dienstes.
|
||||
*
|
||||
* createAppwrite({ endpoint, project, key, fetchImpl?, database?, timeoutMs? })
|
||||
* getRow(table, id) Zeile oder null (404)
|
||||
* listRows(table, queries) Array von Zeilen; queries = JSON-Strings (queryEqual, queryLimit)
|
||||
* getRow(table, id) Zeile oder null (404 row_not_found)
|
||||
* listRows(table, queries) Array von Zeilen; queries = JSON-Strings (queryEqual, queryLimit);
|
||||
* eine Antwort ohne `rows`-Array ist ein Fehler (kein stilles [])
|
||||
* createRow(table, rowId, data, permissions) doppelte ID -> AppwriteError mit status 409
|
||||
* updateRow(table, id, data, permissions?) Rechte nur ändern, wenn übergeben
|
||||
* deleteRow(table, id) true, false bei 404 (wiederholbar)
|
||||
* deleteRow(table, id) true, false bei 404 row_not_found (wiederholbar)
|
||||
* getFile(bucket, id) Datei-Metadaten ($permissions, bucketId, mimeType, sizeOriginal) oder null
|
||||
* updateFilePermissions(bucket, id, permissions) Datei oder null (404)
|
||||
* deleteFile(bucket, id) true, false bei 404
|
||||
* updateFilePermissions(bucket, id, permissions) Datei oder null (404 storage_file_not_found)
|
||||
* deleteFile(bucket, id) true, false bei 404 storage_file_not_found
|
||||
* accountFromJwt(jwt) Nutzer ($id, labels) oder null (401); OHNE Server-Key
|
||||
*
|
||||
* Als „fehlt“ gilt nur ein 404 mit dem erwarteten Typ (row_not_found, storage_file_not_found). Jedes andere 404
|
||||
* (table_not_found, database_not_found, storage_bucket_not_found, unbekannte Route, 404 ohne JSON) ist ein Fehler:
|
||||
* so verschwindet ein Konfigurationsfehler nicht als „Karte nicht gefunden“.
|
||||
* Redirects werden nie verfolgt (`redirect: 'error'`), damit der Server-Key nicht an eine andere Adresse geht.
|
||||
* Jeder Aufruf bricht nach `timeoutMs` (10 s) ab. Jedes Pfadsegment wird mit encodeURIComponent kodiert;
|
||||
* "." und ".." lehnt der Client ab, weil encodeURIComponent sie nicht verändert und die URL-Normalisierung
|
||||
* sie sonst als Verzeichniswechsel auslegen würde.
|
||||
@@ -23,6 +28,8 @@
|
||||
*/
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||
const ROW_NOT_FOUND = 'row_not_found';
|
||||
const FILE_NOT_FOUND = 'storage_file_not_found';
|
||||
|
||||
class AppwriteError extends Error {
|
||||
constructor(status, type, message) {
|
||||
@@ -60,11 +67,11 @@ function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch,
|
||||
const rowsPath = (table) => `/tablesdb/${segment(database)}/tables/${segment(table)}/rows`;
|
||||
const filePath = (bucket, id) => `/storage/buckets/${segment(bucket)}/files/${segment(id)}`;
|
||||
|
||||
/** Ein HTTP-Aufruf; liefert { status, body } für 2xx und 404, sonst wirft er. */
|
||||
async function send(method, path, { body, headers = {}, withKey = true, tolerate = [] } = {}) {
|
||||
/** Ein HTTP-Aufruf; liefert { status, body } für 2xx und für ein 404 mit einem Typ aus `missingTypes`, sonst wirft er. */
|
||||
async function send(method, path, { body, headers = {}, withKey = true, missingTypes = [] } = {}) {
|
||||
const requestHeaders = { 'X-Appwrite-Project': project, ...headers };
|
||||
if (withKey) requestHeaders['X-Appwrite-Key'] = key;
|
||||
const init = { method, headers: requestHeaders, signal: AbortSignal.timeout(timeoutMs) };
|
||||
const init = { method, headers: requestHeaders, redirect: 'error', signal: AbortSignal.timeout(timeoutMs) };
|
||||
if (body !== undefined) {
|
||||
requestHeaders['Content-Type'] = 'application/json';
|
||||
init.body = JSON.stringify(body);
|
||||
@@ -92,22 +99,25 @@ function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch,
|
||||
}
|
||||
}
|
||||
if (response.status >= 200 && response.status < 300) return { status: response.status, body: parsed };
|
||||
if (tolerate.includes(response.status)) return { status: response.status, body: null };
|
||||
const type = parsed && typeof parsed.type === 'string' ? parsed.type : 'unknown';
|
||||
if (response.status === 404 && missingTypes.includes(type)) return { status: 404, body: null };
|
||||
const message = parsed && typeof parsed.message === 'string' ? parsed.message : `HTTP ${response.status}`;
|
||||
throw new AppwriteError(response.status, type, message);
|
||||
}
|
||||
|
||||
return {
|
||||
async getRow(table, id) {
|
||||
const result = await send('GET', `${rowsPath(table)}/${segment(id)}`, { tolerate: [404] });
|
||||
const result = await send('GET', `${rowsPath(table)}/${segment(id)}`, { missingTypes: [ROW_NOT_FOUND] });
|
||||
return result.status === 404 ? null : result.body;
|
||||
},
|
||||
|
||||
async listRows(table, queries = []) {
|
||||
const search = queries.map((query) => `queries[]=${encodeURIComponent(query)}`).join('&');
|
||||
const result = await send('GET', `${rowsPath(table)}${search === '' ? '' : `?${search}`}`);
|
||||
return Array.isArray(result.body && result.body.rows) ? result.body.rows : [];
|
||||
if (!result.body || !Array.isArray(result.body.rows)) {
|
||||
throw new AppwriteError(502, 'invalid_response', 'Unerwartete Antwort von Appwrite');
|
||||
}
|
||||
return result.body.rows;
|
||||
},
|
||||
|
||||
async createRow(table, rowId, data, permissions) {
|
||||
@@ -123,22 +133,22 @@ function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch,
|
||||
},
|
||||
|
||||
async deleteRow(table, id) {
|
||||
const result = await send('DELETE', `${rowsPath(table)}/${segment(id)}`, { tolerate: [404] });
|
||||
const result = await send('DELETE', `${rowsPath(table)}/${segment(id)}`, { missingTypes: [ROW_NOT_FOUND] });
|
||||
return result.status !== 404;
|
||||
},
|
||||
|
||||
async getFile(bucket, id) {
|
||||
const result = await send('GET', filePath(bucket, id), { tolerate: [404] });
|
||||
const result = await send('GET', filePath(bucket, id), { missingTypes: [FILE_NOT_FOUND] });
|
||||
return result.status === 404 ? null : result.body;
|
||||
},
|
||||
|
||||
async updateFilePermissions(bucket, id, permissions) {
|
||||
const result = await send('PUT', filePath(bucket, id), { body: { permissions }, tolerate: [404] });
|
||||
const result = await send('PUT', filePath(bucket, id), { body: { permissions }, missingTypes: [FILE_NOT_FOUND] });
|
||||
return result.status === 404 ? null : result.body;
|
||||
},
|
||||
|
||||
async deleteFile(bucket, id) {
|
||||
const result = await send('DELETE', filePath(bucket, id), { tolerate: [404] });
|
||||
const result = await send('DELETE', filePath(bucket, id), { missingTypes: [FILE_NOT_FOUND] });
|
||||
return result.status !== 404;
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user