filepond: fix crash 'can't access property main, n.status is undefined'

Fixes three root causes of FilePond errors on TFE upload forms:

1. server.process.onerror accessed .status on a string (XHR response
   text body) — now extracts the body safely.

2. server.load was a bare URL string with no error handling — converted
   to object with onload/onerror to prevent FilePond internal _write
   crash when load.php returns HTTP errors.

3. destroyFilePondsIn now aborts in-flight processing before pond.destroy()
   to prevent stale XHR callbacks firing on a torn-down FilePond instance.

Server-side: FilepondHandler now emits Content-Type: text/plain on all
responses (PHP defaults to text/html on die(), confusing FilePond's
response parser).
This commit is contained in:
Pontoporeia
2026-06-09 21:53:08 +02:00
parent 38ef550397
commit 2829d13a16
4 changed files with 68 additions and 10 deletions

View File

@@ -13,3 +13,11 @@ Reference: `docs/autosave-system.md` → "HTMX v2 Migration Plan" section.
- [x] Fix backend `$isAjax` detection: also recognize `HX-Request` header (page.php, apropos.php, form-help.php) - [x] Fix backend `$isAjax` detection: also recognize `HX-Request` header (page.php, apropos.php, form-help.php)
- [x] Form-help inline editors: add OverType toolbar + HTMX auto-save + remove save buttons - [x] Form-help inline editors: add OverType toolbar + HTMX auto-save + remove save buttons
- [x] Markdown cheatsheet modal: reusable dialog on all OverType editors - [x] Markdown cheatsheet modal: reusable dialog on all OverType editors
## FilePond error fixes
- [x] Fix `server.process.onerror`: don't access `response.status` on string (response is XHR text body)
- [x] Fix `server.load`: convert from string URL to object with proper `onload`/`onerror` handlers
- [x] Fix `server.process.onload`: guard against non-ID responses (e.g. HTML error pages disguised as 200)
- [x] Fix `destroyFilePondsIn`: abort in-flight uploads before destroying to prevent stale XHR callbacks crashing `_write`
- [x] Fix `FilepondHandler.php`: set `Content-Type: text/plain` header at top of `handleProcess`, `handleLoad`, `handleRevert`, `handleRemove` so PHP doesn't default to `text/html` on `die()`

View File

@@ -252,17 +252,21 @@
}, },
onload: (response) => { onload: (response) => {
var id = response.trim(); var id = response.trim();
// Guard: if the server returned an error message disguised as 200,
// treat it as a processing error so FilePond doesn't treat it as a serverId.
if (id.length > 64 || /[<>\n\r]/.test(id)) {
console.error("[filepond] process onload | unexpected response | body=" + id.substring(0, 200));
throw new Error("Réponse serveur inattendue.");
}
console.log(`[filepond] process onload | serverId=${id}`); console.log(`[filepond] process onload | serverId=${id}`);
return id; // file_id stored as serverId return id; // file_id stored as serverId
}, },
onerror: (response) => { onerror: (response) => {
console.error( // response is the raw XHR response text (string), not an XHR object.
"[filepond] process onerror | status=" + // Log it and return a human-readable error message.
response.status + var body = typeof response === 'string' ? response : (response && response.body ? response.body : String(response || ''));
" | body=" + console.error("[filepond] process onerror | body=" + body);
response, return body || "Erreur lors du téléversement.";
);
return response;
}, },
}, },
@@ -274,11 +278,25 @@
console.log("[filepond] revert OK"); console.log("[filepond] revert OK");
}, },
onerror: (r) => { onerror: (r) => {
console.error(`[filepond] revert ERROR | body=${r}`); var body = typeof r === 'string' ? r : (r && r.body ? r.body : '');
console.error(`[filepond] revert ERROR | body=${body || r}`);
}, },
}, },
load: `${base}/load.php?id=`, load: {
url: `${base}/load.php?id=`,
method: "GET",
onload: (response) => {
// response is the blob from the server; pass through unchanged
return response;
},
onerror: (response) => {
var body = typeof response === 'string' ? response : (response && response.body ? response.body : String(response || ''));
console.error("[filepond] load onerror | body=" + body);
// Return a descriptive error — FilePond will fire an error event.
return body || "Fichier introuvable.";
},
},
// FilePond appends the source value (db_id) automatically // FilePond appends the source value (db_id) automatically
remove: (source, load, error) => { remove: (source, load, error) => {
@@ -500,6 +518,19 @@
} }
} }
} }
// Abort any in-flight uploads before destroying to prevent
// FilePond internal crashes when XHR callbacks fire on a
// torn-down instance ("can't access property main").
var files = pond.getFiles();
for (var i = 0; i < files.length; i++) {
var f = files[i];
if (f.status === 4 || f.status === 2 || f.status === 3) {
// FileStatus: PROCESSING=4, PROCESSING_QUEUED=2, PROCESSING=4
// (FilePond 4.x internal: 4 = processing)
// Abort processing to avoid stale XHR callbacks
try { pond.removeFile(f); } catch (_abort) {}
}
}
pond.destroy(); pond.destroy();
} catch (_) {} } catch (_) {}
} }

View File

@@ -81,6 +81,10 @@ class FilepondHandler
public function handleProcess(): never public function handleProcess(): never
{ {
// All responses from this endpoint must be text/plain
// (PHP defaults to text/html, which confuses FilePond on error).
header('Content-Type: text/plain; charset=utf-8');
error_log($this->logPrefix . ':process ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | files_keys=' . implode(',', array_keys($_FILES)) . ' | post_keys=' . implode(',', array_keys($_POST))); error_log($this->logPrefix . ':process ENTRY | method=' . $_SERVER['REQUEST_METHOD'] . ' | files_keys=' . implode(',', array_keys($_FILES)) . ' | post_keys=' . implode(',', array_keys($_POST)));
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
@@ -197,7 +201,6 @@ class FilepondHandler
file_put_contents($tmpDir . '/manifest.json', json_encode($manifest, JSON_UNESCAPED_SLASHES)); file_put_contents($tmpDir . '/manifest.json', json_encode($manifest, JSON_UNESCAPED_SLASHES));
error_log($this->logPrefix . ':process SUCCESS | file_id=' . $fileId . ' | queue_type=' . $queueType . ' | name=' . $originalName); error_log($this->logPrefix . ':process SUCCESS | file_id=' . $fileId . ' | queue_type=' . $queueType . ' | name=' . $originalName);
header('Content-Type: text/plain; charset=utf-8');
echo $fileId; echo $fileId;
exit; exit;
} }
@@ -209,6 +212,7 @@ class FilepondHandler
public function handleLoad(): never public function handleLoad(): never
{ {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') { if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(405); http_response_code(405);
die('Méthode non autorisée.'); die('Méthode non autorisée.');
} }
@@ -224,6 +228,7 @@ class FilepondHandler
// Numeric IDs → DB files // Numeric IDs → DB files
$dbId = filter_var($fileId, FILTER_VALIDATE_INT); $dbId = filter_var($fileId, FILTER_VALIDATE_INT);
if ($dbId === false || $dbId <= 0) { if ($dbId === false || $dbId <= 0) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(400); http_response_code(400);
die('ID invalide.'); die('ID invalide.');
} }
@@ -237,6 +242,7 @@ class FilepondHandler
if (!$fileRow) { if (!$fileRow) {
error_log($this->logPrefix . ':load DB NOT FOUND | db_id=' . $dbId); error_log($this->logPrefix . ':load DB NOT FOUND | db_id=' . $dbId);
header('Content-Type: text/plain; charset=utf-8');
http_response_code(404); http_response_code(404);
die('Fichier introuvable.'); die('Fichier introuvable.');
} }
@@ -259,6 +265,7 @@ class FilepondHandler
exit; exit;
} }
if (str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://')) { if (str_starts_with($filePath, 'http://') || str_starts_with($filePath, 'https://')) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(404); http_response_code(404);
die('URL — pas de flux direct.'); die('URL — pas de flux direct.');
} }
@@ -266,6 +273,7 @@ class FilepondHandler
$absPath = STORAGE_ROOT . '/' . $filePath; $absPath = STORAGE_ROOT . '/' . $filePath;
if (!file_exists($absPath) || !is_readable($absPath)) { if (!file_exists($absPath) || !is_readable($absPath)) {
error_log($this->logPrefix . ':load DISK MISSING | db_id=' . $dbId . ' | absPath=' . $absPath); error_log($this->logPrefix . ':load DISK MISSING | db_id=' . $dbId . ' | absPath=' . $absPath);
header('Content-Type: text/plain; charset=utf-8');
http_response_code(404); http_response_code(404);
die('Fichier absent du disque.'); die('Fichier absent du disque.');
} }
@@ -286,6 +294,8 @@ class FilepondHandler
public function handleRemove(): never public function handleRemove(): never
{ {
header('Content-Type: text/plain; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') {
http_response_code(405); http_response_code(405);
die('Méthode non autorisée.'); die('Méthode non autorisée.');
@@ -339,6 +349,8 @@ class FilepondHandler
public function handleRevert(): never public function handleRevert(): never
{ {
header('Content-Type: text/plain; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') {
http_response_code(405); http_response_code(405);
die('Méthode non autorisée.'); die('Méthode non autorisée.');
@@ -480,12 +492,14 @@ class FilepondHandler
$manifestPath = $tmpDir . '/manifest.json'; $manifestPath = $tmpDir . '/manifest.json';
if (!is_dir($tmpDir) || !file_exists($manifestPath)) { if (!is_dir($tmpDir) || !file_exists($manifestPath)) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(404); http_response_code(404);
die('Fichier temporaire introuvable.'); die('Fichier temporaire introuvable.');
} }
$manifest = json_decode(file_get_contents($manifestPath), true); $manifest = json_decode(file_get_contents($manifestPath), true);
if (!is_array($manifest) || ($manifest['session_id'] ?? '') !== session_id()) { if (!is_array($manifest) || ($manifest['session_id'] ?? '') !== session_id()) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(403); http_response_code(403);
die('Session invalide.'); die('Session invalide.');
} }
@@ -502,6 +516,7 @@ class FilepondHandler
closedir($dh); closedir($dh);
if ($actualFile === null || !file_exists($actualFile)) { if ($actualFile === null || !file_exists($actualFile)) {
header('Content-Type: text/plain; charset=utf-8');
http_response_code(404); http_response_code(404);
die('Fichier temporaire introuvable.'); die('Fichier temporaire introuvable.');
} }
@@ -569,6 +584,7 @@ class FilepondHandler
private function validateMimeExt(string $queueType, string $mimeType, string $ext): void private function validateMimeExt(string $queueType, string $mimeType, string $ext): void
{ {
// Content-Type already set by handleProcess() header
$allowedMimes = self::QUEUE_MIME_MAP[$queueType] ?? null; $allowedMimes = self::QUEUE_MIME_MAP[$queueType] ?? null;
if ($allowedMimes !== null) { if ($allowedMimes !== null) {
if (!in_array($mimeType, $allowedMimes, true)) { if (!in_array($mimeType, $allowedMimes, true)) {

View File

@@ -26,3 +26,6 @@
{"timestamp":"2026-06-09T19:01:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"licenses"}} {"timestamp":"2026-06-09T19:01:33+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"licenses"}}
{"timestamp":"2026-06-09T19:12:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}} {"timestamp":"2026-06-09T19:12:39+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-06-09T19:12:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}} {"timestamp":"2026-06-09T19:12:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"form_structure","action":"edit","status":"success","context":{"section":"fieldset_access"}}
{"timestamp":"2026-06-09T19:26:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"about"}}
{"timestamp":"2026-06-09T19:27:00+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"page","action":"edit","status":"success","context":{"slug":"about"}}
{"timestamp":"2026-06-09T19:33:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"share_link","action":"create","status":"success","context":{"slug":"20260609-IHRZDYKJ","has_password":true,"expires_at":null,"objet_restriction":"tfe"}}