fix: ein Upload durfte den ganzen Dienst umbringen -- und tat es
BEIM UEBERNEHMEN VON 3670 DATEIEN STARB DER DIENST. Nicht einmal, sondern in
Schleife: acht Neustarts, nach aussen 502 vom Proxy.
TypeError [ERR_INVALID_STATE]: ReadableStream is already closed
at ReadableByteStreamController.close (node:internal/webstreams/...)
DIE URSACHE WAR MEINE EIGENE AENDERUNG von vor einer Stunde. Um PowerShells
Formular-Rumpf zu vertragen, hatte ich `c.req.parseBody()` durch
`c.req.raw.formData()` ersetzt -- und damit Hono den Rumpf weggenommen, den es
selbst verwaltet. Beim Aufraeumen der Antwort schliesst dann jemand einen
Strom, der schon zu ist. Die Ausnahme faellt in einem Microtask an, also
AUSSERHALB jedes try/catch, und Node beendet den Prozess.
Beim Test mit 57 Dateien fiel das nicht auf. Bei 3670 schon.
Jetzt geht der Normalfall wieder ueber Hono; der eigene Weg gilt nur noch fuer
die Antwort mit Anfuehrungszeichen an der Grenzmarkierung, fuer die er gedacht
war.
UND EIN NETZ DARUNTER: process.on('uncaughtException') faengt, was ausserhalb
jedes try/catch anfaellt, und laesst den Dienst weiterlaufen. Die Abwaegung
steht im Code: das ist NICHT in jedem Fall richtig -- der Prozess kann danach
kaputt sein. Hier ueberwiegt das Weiterlaufen, weil dieser Dienst keinen
Zustand im Speicher haelt (alles in SQLite und auf der Platte) und ein Dienst,
der wegen EINER Anfrage fuer alle weg ist, der schlechtere Tausch waere. Laut
wird es trotzdem.
DER SCHADEN WAR REPARIERBAR, und zwar mit dem Werkzeug von heute Nachmittag:
206 Dateien waren durch, 2 lagen auf der Platte OHNE Datensatz (geschrieben,
dann starb der Prozess vor dem Eintrag), dazu eine verwaiste Vorschau. Der
Verwaisten-Finder hat sie gefunden, "aufnehmen" hat sie eingetragen -- keine
Datei verloren.
Und die Fehlermeldung im Uebernahmeskript verschwieg die Ursache: sie meldete
"Conversion from JSON failed ... Unexpected character <", obwohl die Wahrheit
ein 502 war. Jetzt wird der HTTP-Code mitgelesen, bei 502 sauber abgebrochen
und gesagt, dass ein neuer Lauf einfach weitermacht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,29 @@ app.onError((err, c) => {
|
|||||||
return c.text(`Serverfehler: ${text}`, 500)
|
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
|
// -------------------------------------------------------- Dateien ausliefern
|
||||||
|
|
||||||
/** Liefert eine Datei aus, mit ETag und Bereichsanfragen.
|
/** Liefert eine Datei aus, mit ETag und Bereichsanfragen.
|
||||||
|
|||||||
+25
-11
@@ -591,19 +591,41 @@ dashRoutes.post('/upload', async (c) => {
|
|||||||
// eigenen Multipart-Parser, und der waere fuer diesen Dienst
|
// eigenen Multipart-Parser, und der waere fuer diesen Dienst
|
||||||
// unverhaeltnismaessig. Werkzeuge auf dieser Maschine nehmen deshalb
|
// unverhaeltnismaessig. Werkzeuge auf dieser Maschine nehmen deshalb
|
||||||
// `curl.exe -F` (siehe tools/uebernehmen.ps1); curl setzt beides korrekt.
|
// `curl.exe -F` (siehe tools/uebernehmen.ps1); curl setzt beides korrekt.
|
||||||
let form: FormData
|
let dateien: File[]
|
||||||
|
let ordnerRoh = ''
|
||||||
|
let ueberschreiben = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const typ = c.req.header('content-type') ?? ''
|
const typ = c.req.header('content-type') ?? ''
|
||||||
if (/boundary="/.test(typ)) {
|
if (/boundary="/.test(typ)) {
|
||||||
const gerade = typ.replace(/boundary="([^"]+)"/, 'boundary=$1')
|
const gerade = typ.replace(/boundary="([^"]+)"/, 'boundary=$1')
|
||||||
const rumpf = await c.req.arrayBuffer()
|
const rumpf = await c.req.arrayBuffer()
|
||||||
form = await new Request('http://formular.invalid/', {
|
const form = await new Request('http://formular.invalid/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': gerade },
|
headers: { 'content-type': gerade },
|
||||||
body: rumpf,
|
body: rumpf,
|
||||||
}).formData()
|
}).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 {
|
} 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) {
|
} catch (err) {
|
||||||
// Ein kaputter Rumpf ergab bisher einen 500, aufgefangen nur von der
|
// 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)
|
if (dateien.length === 0) return c.json({ error: 'Keine Datei dabei' }, 400)
|
||||||
|
|
||||||
const ordnerRoh = (form.get('folder') as string | null)?.trim() ?? ''
|
|
||||||
let ordner = ''
|
let ordner = ''
|
||||||
if (ordnerRoh) {
|
if (ordnerRoh) {
|
||||||
try {
|
try {
|
||||||
@@ -637,7 +652,6 @@ dashRoutes.post('/upload', async (c) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ueberschreiben = form.get('overwrite') !== 'false'
|
|
||||||
const ergebnisse: {
|
const ergebnisse: {
|
||||||
name: string
|
name: string
|
||||||
ok: boolean
|
ok: boolean
|
||||||
|
|||||||
+31
-9
@@ -181,16 +181,38 @@ foreach ($g in $gruppen) {
|
|||||||
)
|
)
|
||||||
foreach ($d in $dateien) { $argumente += @('-F', "file=@$($d.FullName)") }
|
foreach ($d in $dateien) { $argumente += @('-F', "file=@$($d.FullName)") }
|
||||||
|
|
||||||
try {
|
# Den HTTP-Code MIT abfragen. Vorher stand hier nur ConvertFrom-Json,
|
||||||
$res = (& curl.exe @argumente) | ConvertFrom-Json
|
# und als der Dienst wegbrach, meldete das Skript
|
||||||
foreach ($r in $res.ergebnisse) {
|
# "Conversion from JSON failed ... Unexpected character <" -- also
|
||||||
if ($r.ok) { $hoch++; Gut "$($r.path)" }
|
# einen Parser-Fehler, obwohl die Wahrheit ein 502 vom Proxy war. Eine
|
||||||
elseif ($r.error -like '*belegt*') { $schon++ }
|
# Fehlermeldung, die die Ursache verschweigt, kostet genau die Zeit,
|
||||||
else { $fehler++; Schlecht "$($r.name): $($r.error)" }
|
# die sie sparen soll.
|
||||||
}
|
$argumente += @('-w', "`n%{http_code}")
|
||||||
} catch {
|
$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
|
$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
|
$dateien | Remove-Item -Force -ErrorAction SilentlyContinue
|
||||||
|
|||||||
Reference in New Issue
Block a user