fix: Video-Upload als roher Body statt multipart — beseitigt die 502-Ursache
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m10s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m10s
Live beobachtet: der Upload scheitert mit HTTP 502, nicht mit 504. Das ist ein wesentlicher Unterschied. 502 bedeutet, dass der vorgeschaltete Proxy vom Backend gar keine Antwort bekam — der Prozess hat die Verbindung mitten im Request fallen gelassen. Eine Zeitueberschreitung waere 504. Die bisher verfolgte Timeout-Spur war damit falsch. Uebrig blieb als Erklaerung der Arbeitsspeicher, und dort lag noch die eigentliche Ursache: request.formData() liest den GESAMTEN Body in den Speicher, bevor eine einzige Zeile eigener Code laeuft. Der vorherige Streaming-Umbau hat nur die zusaetzlichen Kopien danach beseitigt (arrayBuffer, Buffer, readFile) — diese erste nicht. Bei einem Video bis 100 MB genuegt das, um zusammen mit dem Grundverbrauch des Servers das Speicherlimit des Pods zu sprengen; der Kernel beendet den Prozess (OOMKilled), und der Proxy meldet 502. Die Datei kommt jetzt als roher Request-Body statt als multipart/form-data. Damit laesst sich request.body direkt auf die Platte durchreichen, und der Speicherbedarf haengt nicht mehr von der Dateigroesse ab. Ein Formularfeld wurde ohnehin nicht gebraucht, es wird genau eine Datei uebertragen. Die Groessengrenze prueft jetzt ein mitzaehlender Transform-Stream, weil Content-Length nur eine Ankuendigung des Clients ist. Gemessen im Produktions-Build mit einem 95-MB-Video: Status 200 nach 5,9 Sekunden, Arbeitsspeicher des Servers von 67 MB auf in der Spitze 149 MB — also rund ein Drittel des Limits von 512Mi, mit dem der Pod bisher lief. Ausserdem berichtigt: der eigene Meldungstext behauptete bei 502 "fast immer eine Zeitueberschreitung". Das ist falsch und hat die Fehlersuche in die falsche Richtung gelenkt. 502 und 504 werden jetzt getrennt erklaert — 502 als weggebrochenes Backend (meist Speichermangel), 504 als echtes Zeitlimit.
This commit is contained in:
1 parent
e8f0bd4f55
commit
96bb3d9c50
4 files changed
+120
-51
No files matched your search
@@ -3,7 +3,7 @@ import { createWriteStream } from "node:fs";
|
|||||||
import { copyFile, mkdir, mkdtemp, open, rename, rm, stat } from "node:fs/promises";
|
import { copyFile, mkdir, mkdtemp, open, rename, rm, stat } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Readable } from "node:stream";
|
import { Readable, Transform } from "node:stream";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
||||||
import { isTrustedOrigin } from "@/lib/csrf";
|
import { isTrustedOrigin } from "@/lib/csrf";
|
||||||
@@ -23,6 +23,35 @@ export const runtime = "nodejs";
|
|||||||
*/
|
*/
|
||||||
const MAGIC_BYTES_LENGTH = 12;
|
const MAGIC_BYTES_LENGTH = 12;
|
||||||
|
|
||||||
|
/** Signalisiert, dass der Datenstrom MAX_UPLOAD_VIDEO_BYTES ueberschritten hat. */
|
||||||
|
class UploadTooLargeError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Upload ueberschreitet MAX_UPLOAD_VIDEO_BYTES.");
|
||||||
|
this.name = "UploadTooLargeError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zaehlt die durchfliessenden Bytes und bricht ab, sobald die Obergrenze
|
||||||
|
* ueberschritten ist. Noetig, weil Content-Length nur eine Ankuendigung des
|
||||||
|
* Clients ist: ohne diese Pruefung koennte ein Aufrufer beliebig viele Daten
|
||||||
|
* nachschieben und damit das Dateisystem fuellen.
|
||||||
|
*/
|
||||||
|
function limitBytes(maximum: number, onTotal: (total: number) => void): Transform {
|
||||||
|
let total = 0;
|
||||||
|
return new Transform({
|
||||||
|
transform(chunk: Buffer, _encoding, callback) {
|
||||||
|
total += chunk.length;
|
||||||
|
if (total > maximum) {
|
||||||
|
callback(new UploadTooLargeError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onTotal(total);
|
||||||
|
callback(null, chunk);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verschiebt die fertige Datei aus dem Temp-Verzeichnis an ihren
|
* Verschiebt die fertige Datei aus dem Temp-Verzeichnis an ihren
|
||||||
* endgueltigen Platz. rename() ist ein reiner Verzeichniseintrag-Wechsel und
|
* endgueltigen Platz. rename() ist ein reiner Verzeichniseintrag-Wechsel und
|
||||||
@@ -46,33 +75,33 @@ async function moveIntoUploadDir(from: string, to: string): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* Admin-only Upload-Endpunkt fuer das Hero-Hintergrundvideo (siehe
|
* Admin-only Upload-Endpunkt fuer das Hero-Hintergrundvideo (siehe
|
||||||
* lib/content-registry.ts "video"-Feldtyp, aktuell nur home.hero.video).
|
* lib/content-registry.ts "video"-Feldtyp, aktuell nur home.hero.video).
|
||||||
* Eigene Route statt Erweiterung von app/api/admin/upload/route.ts, weil
|
|
||||||
* Videos anders validiert werden: sharp kann nur Bilder dekodieren, hier
|
|
||||||
* uebernimmt ffmpeg (siehe lib/video-transcode.ts) die Neuenkodierung.
|
|
||||||
*
|
*
|
||||||
* Ablauf: (1) die hochgeladene Datei wird STREAMEND in eine temporaere Datei
|
* Nimmt die Datei als ROHEN Request-Body entgegen, nicht als
|
||||||
* geschrieben, (2) ihre Magic-Bytes bestaetigen MP4/WebM (nur
|
* multipart/form-data. Das ist der entscheidende Unterschied zur vorherigen
|
||||||
* Format-Erkennung, sagt noch nichts ueber vollstaendige Dekodierbarkeit),
|
* Fassung: request.formData() liest den gesamten Body in den
|
||||||
* (3) ffmpeg enkodiert sie zu einem web-optimierten MP4 neu, (4) NUR das
|
* Arbeitsspeicher, bevor eine einzige Zeile eigener Code laeuft. Bei einem
|
||||||
* transkodierte Ergebnis wird nach UPLOAD_DIR verschoben, nie die
|
* Video bis MAX_UPLOAD_VIDEO_BYTES (100 MB) sprengte das zusammen mit dem
|
||||||
* Originaldatei — ein erfolgreicher Transcode validiert nebenbei
|
* Grundverbrauch des Next.js-Servers das Speicherlimit des Pods; der Kernel
|
||||||
* vollstaendig, dass die Datei tatsaechlich als Video dekodierbar ist
|
* beendete den Prozess mitten im Request (OOMKilled), und der vorgeschaltete
|
||||||
* (analog zu reencodeImage() fuer Bilder).
|
* Proxy meldete dem Browser daraufhin "502 Bad Gateway" — kein Zeitlimit,
|
||||||
|
* sondern ein weggebrochenes Backend. Genau dieser Status wurde live
|
||||||
|
* beobachtet.
|
||||||
*
|
*
|
||||||
* Warum streamend statt ueber Buffer: ein Video darf bis
|
* Als roher Body laesst sich request.body direkt auf die Platte
|
||||||
* MAX_UPLOAD_VIDEO_BYTES (100 MB) gross sein. Die vorherige Fassung hielt es
|
* durchreichen. Der Speicherbedarf ist damit konstant und haengt nicht mehr
|
||||||
* gleich mehrfach vollstaendig im Node-Heap — einmal aus file.arrayBuffer(),
|
* von der Dateigroesse ab — ein Chunk zur Zeit statt 100 MB am Stueck. Ein
|
||||||
* einmal als Buffer daraus, und das Transcoding-Ergebnis noch einmal aus
|
* Formularfeld wird ohnehin nicht gebraucht, es wird genau eine Datei
|
||||||
* readFile(). Bei dem in k8s/05-app-deployment.yaml gesetzten Speicherlimit
|
* uebertragen.
|
||||||
* reicht das aus, um den Prozess mitten im Upload abzuschiessen; im Browser
|
|
||||||
* kommt davon nur die HTML-Fehlerseite des Reverse-Proxy an, die der Client
|
|
||||||
* frueher als "Unexpected token '<'" meldete. Jetzt wandert die Datei ueber
|
|
||||||
* einen Stream auf die Platte und von dort per rename/copyFile an ihr Ziel,
|
|
||||||
* ohne je vollstaendig im Speicher zu liegen.
|
|
||||||
*
|
*
|
||||||
* Zugriff ohnehin nur fuer eingeloggte 2FA-Admins (requireAdminForApi),
|
* Weiterer Ablauf unveraendert: (1) Magic-Bytes bestaetigen MP4/WebM, (2)
|
||||||
* Auslieferung mit festem Content-Type + "X-Content-Type-Options: nosniff"
|
* ffmpeg enkodiert web-optimiert neu (siehe lib/video-transcode.ts), (3) nur
|
||||||
* (siehe app/uploads/[...path]/route.ts) und zufaelligem Dateinamen.
|
* das Ergebnis wird nach UPLOAD_DIR verschoben, nie die Originaldatei — ein
|
||||||
|
* erfolgreicher Transcode validiert nebenbei vollstaendig, dass die Datei
|
||||||
|
* als Video dekodierbar ist.
|
||||||
|
*
|
||||||
|
* Zugriff nur fuer eingeloggte 2FA-Admins (requireAdminForApi), Auslieferung
|
||||||
|
* mit festem Content-Type + "X-Content-Type-Options: nosniff" (siehe
|
||||||
|
* app/uploads/[...path]/route.ts) und zufaelligem Dateinamen.
|
||||||
*/
|
*/
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
if (!isTrustedOrigin(request)) {
|
if (!isTrustedOrigin(request)) {
|
||||||
@@ -92,17 +121,19 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = await request.formData().catch(() => null);
|
if (!request.body) {
|
||||||
const file = formData?.get("file");
|
|
||||||
if (!(file instanceof File)) {
|
|
||||||
return NextResponse.json({ error: "Keine Datei übermittelt." }, { status: 400 });
|
return NextResponse.json({ error: "Keine Datei übermittelt." }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (file.size === 0) {
|
|
||||||
return NextResponse.json({ error: "Die Datei ist leer." }, { status: 400 });
|
const maxMegabytes = Math.floor(MAX_UPLOAD_VIDEO_BYTES / (1024 * 1024));
|
||||||
}
|
|
||||||
if (file.size > MAX_UPLOAD_VIDEO_BYTES) {
|
// Content-Length ist nur eine Ankuendigung des Clients — trotzdem frueh
|
||||||
|
// ablehnen, wenn schon sie zu gross ist, damit erst gar nichts uebertragen
|
||||||
|
// wird. Verbindlich ist die mitzaehlende Pruefung unten beim Schreiben.
|
||||||
|
const declaredLength = Number(request.headers.get("content-length"));
|
||||||
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_UPLOAD_VIDEO_BYTES) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `Die Datei ist zu groß (max. ${Math.floor(MAX_UPLOAD_VIDEO_BYTES / (1024 * 1024))} MB).` },
|
{ error: `Die Datei ist zu groß (max. ${maxMegabytes} MB).` },
|
||||||
{ status: 413 }
|
{ status: 413 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,12 +143,33 @@ export async function POST(request: Request) {
|
|||||||
const outputPath = path.join(dir, "output.mp4");
|
const outputPath = path.join(dir, "output.mp4");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Der Cast ueberbrueckt nur eine Typ-Doppelung, keine Laufzeitgrenze:
|
let receivedBytes = 0;
|
||||||
// File.stream() ist in den DOM-Typen als ReadableStream deklariert,
|
try {
|
||||||
// Readable.fromWeb() erwartet die Deklaration aus node:stream/web. Unter
|
// Der Cast ueberbrueckt nur eine Typ-Doppelung, keine Laufzeitgrenze:
|
||||||
// der Node-Runtime ist beides dieselbe globale Klasse.
|
// request.body ist in den DOM-Typen als ReadableStream deklariert,
|
||||||
const webStream = file.stream() as unknown as NodeReadableStream<Uint8Array>;
|
// Readable.fromWeb() erwartet die Deklaration aus node:stream/web.
|
||||||
await pipeline(Readable.fromWeb(webStream), createWriteStream(inputPath));
|
// Unter der Node-Runtime ist beides dieselbe globale Klasse.
|
||||||
|
const webStream = request.body as unknown as NodeReadableStream<Uint8Array>;
|
||||||
|
await pipeline(
|
||||||
|
Readable.fromWeb(webStream),
|
||||||
|
limitBytes(MAX_UPLOAD_VIDEO_BYTES, (total) => {
|
||||||
|
receivedBytes = total;
|
||||||
|
}),
|
||||||
|
createWriteStream(inputPath)
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof UploadTooLargeError) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Die Datei ist zu groß (max. ${maxMegabytes} MB).` },
|
||||||
|
{ status: 413 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receivedBytes === 0) {
|
||||||
|
return NextResponse.json({ error: "Die Datei ist leer." }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
// Formaterkennung ausschliesslich anhand der tatsaechlichen Magic-Bytes
|
// Formaterkennung ausschliesslich anhand der tatsaechlichen Magic-Bytes
|
||||||
// am Dateianfang — nie ueber den Client-Dateinamen oder den vom Client
|
// am Dateianfang — nie ueber den Client-Dateinamen oder den vom Client
|
||||||
@@ -173,7 +225,7 @@ export async function POST(request: Request) {
|
|||||||
action: "media.upload",
|
action: "media.upload",
|
||||||
entityType: "Upload",
|
entityType: "Upload",
|
||||||
entityId: filename,
|
entityId: filename,
|
||||||
detail: `mp4, ${Math.ceil(file.size / 1024)} KB -> ${Math.ceil(outputBytes / 1024)} KB`,
|
detail: `mp4, ${Math.ceil(receivedBytes / 1024)} KB -> ${Math.ceil(outputBytes / 1024)} KB`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ path: `/uploads/${filename}` });
|
return NextResponse.json({ path: `/uploads/${filename}` });
|
||||||
|
|||||||
@@ -33,11 +33,20 @@ export function AdminVideoField({ name, label, initialValue }: AdminVideoFieldPr
|
|||||||
setStatus("uploading");
|
setStatus("uploading");
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
try {
|
try {
|
||||||
const body = new FormData();
|
|
||||||
body.append("file", file);
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetch("/api/admin/upload-video", { method: "POST", body });
|
// Die Datei wird als ROHER Body gesendet, nicht als FormData: auf der
|
||||||
|
// Serverseite liest request.formData() den gesamten Body erst
|
||||||
|
// vollstaendig in den Arbeitsspeicher, was bei einem Video bis 100 MB
|
||||||
|
// das Speicherlimit des Pods sprengte (ausfuehrliche Begruendung in
|
||||||
|
// app/api/admin/upload-video/route.ts). Als roher Body laesst er sich
|
||||||
|
// dort direkt auf die Platte durchreichen. fetch() streamt ein
|
||||||
|
// File-Objekt, es landet also auch hier nie am Stueck im Speicher.
|
||||||
|
response = await fetch("/api/admin/upload-video", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": file.type || "application/octet-stream" },
|
||||||
|
body: file,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// fetch() wirft nur, wenn der Request den Server gar nicht erreicht
|
// fetch() wirft nur, wenn der Request den Server gar nicht erreicht
|
||||||
// oder die Verbindung abbricht — ein HTTP-Fehlerstatus landet NICHT
|
// oder die Verbindung abbricht — ein HTTP-Fehlerstatus landet NICHT
|
||||||
|
|||||||
@@ -1142,8 +1142,10 @@ export const de: Dictionary = {
|
|||||||
uploadErrorStatus: (status) =>
|
uploadErrorStatus: (status) =>
|
||||||
status === 413
|
status === 413
|
||||||
? `Die Datei wurde abgewiesen, weil sie zu groß ist (HTTP 413). Sie hat den Shop gar nicht erreicht — diese Grenze setzt der vorgeschaltete Proxy bzw. das CDN.`
|
? `Die Datei wurde abgewiesen, weil sie zu groß ist (HTTP 413). Sie hat den Shop gar nicht erreicht — diese Grenze setzt der vorgeschaltete Proxy bzw. das CDN.`
|
||||||
: status === 502 || status === 503 || status === 504 || status === 522 || status === 524
|
: status === 502
|
||||||
? `Der Server hat die Verarbeitung abgebrochen (HTTP ${status}). Das ist fast immer eine Zeitüberschreitung: die Verarbeitung dauert länger, als der vorgeschaltete Proxy auf eine Antwort wartet.`
|
? `Der Server hat die Verarbeitung abgebrochen (HTTP 502). Das Backend hat die Verbindung fallen gelassen, ohne zu antworten — häufig, weil ihm der Arbeitsspeicher ausging. Bitte informiere die Technik und nenne diese Nummer.`
|
||||||
|
: status === 503 || status === 504 || status === 522 || status === 524
|
||||||
|
? `Der Server hat die Verarbeitung abgebrochen (HTTP ${status}). Das ist eine Zeitüberschreitung: die Verarbeitung dauerte länger, als der vorgeschaltete Proxy zulässt.`
|
||||||
: `Der Server hat unerwartet geantwortet (HTTP ${status}) — keine verwertbare Rückmeldung. Bitte informiere die Technik und nenne diese Nummer.`,
|
: `Der Server hat unerwartet geantwortet (HTTP ${status}) — keine verwertbare Rückmeldung. Bitte informiere die Technik und nenne diese Nummer.`,
|
||||||
uploadErrorNetwork: "Die Verbindung zum Server wurde während des Uploads unterbrochen.",
|
uploadErrorNetwork: "Die Verbindung zum Server wurde während des Uploads unterbrochen.",
|
||||||
},
|
},
|
||||||
@@ -1156,8 +1158,10 @@ export const de: Dictionary = {
|
|||||||
uploadErrorStatus: (status) =>
|
uploadErrorStatus: (status) =>
|
||||||
status === 413
|
status === 413
|
||||||
? `Das Video wurde abgewiesen, weil es zu groß ist (HTTP 413). Es hat den Shop gar nicht erreicht — diese Grenze setzt der vorgeschaltete Proxy bzw. das CDN, nicht der Shop selbst.`
|
? `Das Video wurde abgewiesen, weil es zu groß ist (HTTP 413). Es hat den Shop gar nicht erreicht — diese Grenze setzt der vorgeschaltete Proxy bzw. das CDN, nicht der Shop selbst.`
|
||||||
: status === 502 || status === 503 || status === 504 || status === 522 || status === 524
|
: status === 502
|
||||||
? `Der Server hat die Verarbeitung abgebrochen (HTTP ${status}). Bei Videos ist das fast immer eine Zeitüberschreitung: das Umrechnen fürs Web dauert länger, als der vorgeschaltete Proxy auf eine Antwort wartet. Ein kürzeres oder kleineres Video kommt in der Regel durch.`
|
? `Der Server hat die Verarbeitung abgebrochen (HTTP 502). Das Backend hat die Verbindung fallen gelassen, ohne zu antworten — bei Videos meist, weil ihm beim Verarbeiten der Arbeitsspeicher ausging. Bitte informiere die Technik und nenne diese Nummer.`
|
||||||
|
: status === 503 || status === 504 || status === 522 || status === 524
|
||||||
|
? `Der Server hat die Verarbeitung abgebrochen (HTTP ${status}). Das ist eine Zeitüberschreitung: der Upload dauerte länger, als der vorgeschaltete Proxy zulässt. Ein kürzeres oder kleineres Video kommt in der Regel durch.`
|
||||||
: `Der Server hat unerwartet geantwortet (HTTP ${status}) — keine verwertbare Rückmeldung. Bitte informiere die Technik und nenne diese Nummer.`,
|
: `Der Server hat unerwartet geantwortet (HTTP ${status}) — keine verwertbare Rückmeldung. Bitte informiere die Technik und nenne diese Nummer.`,
|
||||||
uploadErrorNetwork: "Die Verbindung zum Server wurde während des Uploads unterbrochen.",
|
uploadErrorNetwork: "Die Verbindung zum Server wurde während des Uploads unterbrochen.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1142,8 +1142,10 @@ export const en: Dictionary = {
|
|||||||
uploadErrorStatus: (status) =>
|
uploadErrorStatus: (status) =>
|
||||||
status === 413
|
status === 413
|
||||||
? `The file was rejected because it is too large (HTTP 413). It never reached the shop — that limit comes from the reverse proxy or CDN in front of it.`
|
? `The file was rejected because it is too large (HTTP 413). It never reached the shop — that limit comes from the reverse proxy or CDN in front of it.`
|
||||||
: status === 502 || status === 503 || status === 504 || status === 522 || status === 524
|
: status === 502
|
||||||
? `The server aborted processing (HTTP ${status}). This is almost always a timeout: processing takes longer than the proxy in front of it waits for a response.`
|
? `The server aborted processing (HTTP 502). The backend dropped the connection without answering — often because it ran out of memory. Please report this number to your technical contact.`
|
||||||
|
: status === 503 || status === 504 || status === 522 || status === 524
|
||||||
|
? `The server aborted processing (HTTP ${status}). This is a timeout: processing took longer than the proxy in front of it allows.`
|
||||||
: `The server responded unexpectedly (HTTP ${status}) — no usable reply. Please report this number to your technical contact.`,
|
: `The server responded unexpectedly (HTTP ${status}) — no usable reply. Please report this number to your technical contact.`,
|
||||||
uploadErrorNetwork: "The connection to the server was interrupted during the upload.",
|
uploadErrorNetwork: "The connection to the server was interrupted during the upload.",
|
||||||
},
|
},
|
||||||
@@ -1156,8 +1158,10 @@ export const en: Dictionary = {
|
|||||||
uploadErrorStatus: (status) =>
|
uploadErrorStatus: (status) =>
|
||||||
status === 413
|
status === 413
|
||||||
? `The video was rejected because it is too large (HTTP 413). It never reached the shop — that limit comes from the reverse proxy or CDN in front of it, not from the shop itself.`
|
? `The video was rejected because it is too large (HTTP 413). It never reached the shop — that limit comes from the reverse proxy or CDN in front of it, not from the shop itself.`
|
||||||
: status === 502 || status === 503 || status === 504 || status === 522 || status === 524
|
: status === 502
|
||||||
? `The server aborted processing (HTTP ${status}). For videos this is almost always a timeout: re-encoding for the web takes longer than the proxy in front of it waits for a response. A shorter or smaller video usually gets through.`
|
? `The server aborted processing (HTTP 502). The backend dropped the connection without answering — for videos usually because it ran out of memory while processing. Please report this number to your technical contact.`
|
||||||
|
: status === 503 || status === 504 || status === 522 || status === 524
|
||||||
|
? `The server aborted processing (HTTP ${status}). This is a timeout: the upload took longer than the proxy in front of it allows. A shorter or smaller video usually gets through.`
|
||||||
: `The server responded unexpectedly (HTTP ${status}) — no usable reply. Please report this number to your technical contact.`,
|
: `The server responded unexpectedly (HTTP ${status}) — no usable reply. Please report this number to your technical contact.`,
|
||||||
uploadErrorNetwork: "The connection to the server was interrupted during the upload.",
|
uploadErrorNetwork: "The connection to the server was interrupted during the upload.",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user