diff --git a/server/src/index.ts b/server/src/index.ts index 010921f..ded5c62 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -60,6 +60,29 @@ app.onError((err, c) => { return c.text(`Serverfehler: ${text}`, 500) }) +// Ein Fehler AUSSERHALB jedes try/catch darf den Dienst nicht umbringen. +// +// Genau das ist beim Uebernehmen von 3670 Dateien passiert: undici warf +// "ReadableStream is already closed" aus einem Microtask -- also an einer +// Stelle, an der kein try/catch der Welt steht. Node beendet den Prozess +// daraufhin, Docker startet ihn neu, die naechste Anfrage kippt ihn wieder: +// eine Neustartschleife, und nach aussen 502. +// +// DIE ABWAEGUNG, ausgesprochen: einen unerwarteten Fehler zu ueberleben ist +// nicht in jedem Fall richtig -- der Prozess KANN danach in einem kaputten +// Zustand sein. Hier ueberwiegt trotzdem das Weiterlaufen: dieser Dienst haelt +// keinen Zustand im Speicher, der verderben koennte (alles steht in SQLite und +// auf der Platte), und ein Dienst, der wegen EINER Anfrage fuer alle weg ist, +// ist der schlechtere Tausch. +// +// Laut wird es trotzdem: die Spur steht vollstaendig in der Konsole. +process.on('uncaughtException', (err) => { + console.error('[unerwartet] uncaughtException — der Dienst laeuft weiter:', err) +}) +process.on('unhandledRejection', (grund) => { + console.error('[unerwartet] unhandledRejection — der Dienst laeuft weiter:', grund) +}) + // -------------------------------------------------------- Dateien ausliefern /** Liefert eine Datei aus, mit ETag und Bereichsanfragen. diff --git a/server/src/routes/dash.ts b/server/src/routes/dash.ts index a82b939..b81d83e 100644 --- a/server/src/routes/dash.ts +++ b/server/src/routes/dash.ts @@ -591,19 +591,41 @@ dashRoutes.post('/upload', async (c) => { // eigenen Multipart-Parser, und der waere fuer diesen Dienst // unverhaeltnismaessig. Werkzeuge auf dieser Maschine nehmen deshalb // `curl.exe -F` (siehe tools/uebernehmen.ps1); curl setzt beides korrekt. - let form: FormData + let dateien: File[] + let ordnerRoh = '' + let ueberschreiben = true + try { const typ = c.req.header('content-type') ?? '' if (/boundary="/.test(typ)) { const gerade = typ.replace(/boundary="([^"]+)"/, 'boundary=$1') const rumpf = await c.req.arrayBuffer() - form = await new Request('http://formular.invalid/', { + const form = await new Request('http://formular.invalid/', { method: 'POST', headers: { 'content-type': gerade }, body: rumpf, }).formData() + dateien = form.getAll('file').filter((f) => typeof f !== 'string') as unknown as File[] + ordnerRoh = ((form.get('folder') as string | null) ?? '').trim() + ueberschreiben = form.get('overwrite') !== 'false' } else { - form = await c.req.raw.formData() + // DER NORMALFALL GEHT UEBER HONO, und das ist kein Geschmack: + // c.req.raw.formData() direkt aufzurufen nimmt Hono den Rumpf weg, + // den es selbst verwaltet. Beim Aufraeumen der Antwort schliesst + // dann jemand einen Strom, der schon zu ist -- "ReadableStream is + // already closed", eine Ausnahme AUSSERHALB jedes try/catch, und + // der Prozess ist weg. + // + // Genau das ist beim Uebernehmen von 3670 Dateien passiert: der + // Dienst lief in eine Neustartschleife, und NPM meldete 502. Beim + // Test mit 57 Dateien war es nicht aufgefallen. + const roh = await c.req.parseBody({ all: true }) + const feld = roh['file'] + dateien = (Array.isArray(feld) ? feld : [feld]).filter( + (f) => f && typeof f !== 'string', + ) as unknown as File[] + ordnerRoh = typeof roh['folder'] === 'string' ? roh['folder'].trim() : '' + ueberschreiben = roh['overwrite'] !== 'false' } } catch (err) { // Ein kaputter Rumpf ergab bisher einen 500, aufgefangen nur von der @@ -617,14 +639,7 @@ dashRoutes.post('/upload', async (c) => { ) } - // getAll und nicht get: sonst kaeme von fuenf Dateien nur die letzte an. - // Ohne DOM-Typen kennt TypeScript hier nur FormDataEntryValue; die - // Einengung auf File muss deshalb ausgesprochen werden. Der Test bleibt - // derselbe -- was kein Text ist, ist eine Datei. - const dateien = form.getAll('file').filter((f) => typeof f !== 'string') as unknown as File[] if (dateien.length === 0) return c.json({ error: 'Keine Datei dabei' }, 400) - - const ordnerRoh = (form.get('folder') as string | null)?.trim() ?? '' let ordner = '' if (ordnerRoh) { try { @@ -637,7 +652,6 @@ dashRoutes.post('/upload', async (c) => { } } - const ueberschreiben = form.get('overwrite') !== 'false' const ergebnisse: { name: string ok: boolean diff --git a/tools/uebernehmen.ps1 b/tools/uebernehmen.ps1 index c08ea0f..1ed17e4 100644 --- a/tools/uebernehmen.ps1 +++ b/tools/uebernehmen.ps1 @@ -181,16 +181,38 @@ foreach ($g in $gruppen) { ) foreach ($d in $dateien) { $argumente += @('-F', "file=@$($d.FullName)") } - try { - $res = (& curl.exe @argumente) | ConvertFrom-Json - foreach ($r in $res.ergebnisse) { - if ($r.ok) { $hoch++; Gut "$($r.path)" } - elseif ($r.error -like '*belegt*') { $schon++ } - else { $fehler++; Schlecht "$($r.name): $($r.error)" } - } - } catch { + # Den HTTP-Code MIT abfragen. Vorher stand hier nur ConvertFrom-Json, + # und als der Dienst wegbrach, meldete das Skript + # "Conversion from JSON failed ... Unexpected character <" -- also + # einen Parser-Fehler, obwohl die Wahrheit ein 502 vom Proxy war. Eine + # Fehlermeldung, die die Ursache verschweigt, kostet genau die Zeit, + # die sie sparen soll. + $argumente += @('-w', "`n%{http_code}") + $antwort = (& curl.exe @argumente) -join "`n" + $code = ($antwort -split "`n")[-1] + $rumpf = ($antwort -split "`n")[0..(($antwort -split "`n").Count - 2)] -join "`n" + + if ($code -ne '200') { $fehler += $dateien.Count - Schlecht "Stapel fehlgeschlagen: $($_.Exception.Message)" + Schlecht "Stapel fehlgeschlagen: HTTP $code — $($rumpf.Substring(0, [Math]::Min(120, $rumpf.Length)))" + if ($code -eq '502') { + Schlecht 'Der Dienst antwortet nicht. Laeuft der Container? (docker ps)' + Schlecht 'Der Lauf laesst sich danach einfach wiederholen — was da ist, wird uebersprungen.' + break + } + } else { + try { + $res = $rumpf | ConvertFrom-Json + foreach ($r in $res.ergebnisse) { + if ($r.ok) { $hoch++ } + elseif ($r.error -like '*belegt*') { $schon++ } + else { $fehler++; Schlecht "$($r.name): $($r.error)" } + } + Gut "$hoch uebernommen, $schon schon da, $fehler Fehler" + } catch { + $fehler += $dateien.Count + Schlecht "Antwort unlesbar: $($rumpf.Substring(0, [Math]::Min(120, $rumpf.Length)))" + } } $dateien | Remove-Item -Force -ErrorAction SilentlyContinue