Files
xamxam/docs/filepond-crash-analysis.md
Pontoporeia d8d925243e docs: add filepond crash analysis report
Documents the 'can't access property main, n.status is undefined'
crash in FilePond 4.32.12. Root cause: vendor code in filepond.min.js
has a property name mismatch — createResponse objects use .code but the
load-file-error handler reads .status. When action.status is undefined,
the view writers crash.

Proposes Option B (custom load function) as the cleanest fix.
2026-06-10 00:18:49 +02:00

10 KiB
Raw Permalink Blame History

FilePond crash analysis — TFE upload forms

Status: unresolved — analysis complete, root cause identified in vendor code.
Hand this doc (and the whole repo) to another agent for implementing the fix.


Errors observed (Firefox, dev server 127.0.0.1:8000)

Trigger: adding an image to "Image de couverture" (cover queue) or any TFE file upload form.

InstallTrigger is deprecated and will be removed in the future.           content.js:1
Failed to execute 'postMessage' on 'DOMWindow': target origin mismatch    2 20260609-...
htmx:targetError                                                          htmx.min.js:1
Uncaught TypeError: can't access property "main", n.status is undefined   filepond.min.js:9
    Wt  filepond.min.js:9
    A   filepond.min.js:9
    A   filepond.min.js:9
    _write  filepond.min.js:9       (×16)
    <anonymous>  filepond.min.js:9
    <anonymous>  filepond.min.js:9
    e  filepond.min.js:9
    e  filepond.min.js:9
    u  filepond.min.js:9           (× many — retry loop)
    e  filepond.min.js:9
    u  ...

[filepond:event] error  Object { pond: {…}, error: null, file: {…} }    file-upload-filepond.js:587

Rows 13 are noise: InstallTrigger/postMessage are Firefox internals; htmx:targetError is an unrelated HTMX issue. The real crash is rows 4+.


Root cause

The crash

At filepond.min.js:9:60852, FilePond 4.32.12 crashes inside its view system's _write method. Two view writers dereference action.status.main:

FilePond unminified (file-status view), line 7847:

var error = function error(_ref8) {
    var root = _ref8.root,
        action = _ref8.action;
    text(root.ref.main, action.status.main);   // ← crashes if action.status is undefined
    text(root.ref.sub, action.status.sub);
};

FilePond unminified (assistant view), line 10735:

var itemError = function itemError(_ref6) {
    var root = _ref6.root,
        action = _ref6.action;
    var item = root.query('GET_ITEM', action.id);
    var filename = item.filename;
    assist(root, action.status.main + ' ' + filename + ' ' + action.status.sub);
};

Neither function guards against action.status === undefined.

How action.status becomes undefined

FilePond's internal response objects use the property name code, not status:

// line 4700
var createResponse = function createResponse(type, code, body, headers) {
    return {
        type: type,
        code: code,     // ← "code", not "status"
        body: body,
        headers: headers,
    };
};

But the load-file-error event handler accesses error.status:

// line 6777-6784
item.on('load-file-error', function(error) {
    dispatch('DID_THROW_ITEM_INVALID', {
        id: id,
        error: error.status,    // ← .status is undefined on createResponse objects!
        status: error.status,   // ← dispatches undefined as the status
    });
    failure({ error: error.status, file: createItemAPI(item) });
});

Because the error object has .code (not .status), both error: error.status and status: error.status are undefined. When the dispatched action reaches the view writer, action.status is undefined → crash.

When does load-file-error fire?

The load-file-error event is emitted in the item _load method when the LOAD_FILE filter chain rejects:

// line 5855-5863
loader.on('load', function(file) {
    var error = function error(result) {
        state.file = file;
        fire('load-meta');
        setStatus(ItemStatus.LOAD_ERROR);
        fire('load-file-error', result);    // ← fires when filter chain rejects
    };

    if (state.serverFileReference) {
        success(file);    // ← existing files take this safe path
        return;
    }

    onload(file, success, error);   // ← new files take this path
});

For existing DB files (edit mode), state.serverFileReference is set → success(file) is called directly → load-file-error never fires.

For newly added files (no serverId yet), onload(file, success, error) runs the LOAD_FILE filter chain. The FilePond FileValidateType plugin (v1.2.8) registers a LOAD_FILE filter:

// plugin line 132
addFilter('LOAD_FILE', function(file, _ref3) {
    // ...
    var handleRejection = function handleRejection() {
        reject({
            status: { main: '...', sub: '...' }   // ← plugin rejects with proper status object
        });
    };
    // ...
});

The plugin rejects with { status: { main, sub } }. This is CORRECT. The rejected value flows into the error(result) callback → result.status IS { main, sub }. So when load-file-error fires from a plugin rejection, error.status is actually a proper object, NOT undefined. This particular path is safe.

However, load-file-error can also fire from the DID_LOAD_ITEM filter chain catch handler at line 6878:

.catch(function(e) {
    if (!e || !e.error || !e.status) return handleAdd(false);
    dispatch('DID_THROW_ITEM_INVALID', {
        id: id,
        error: e.error,
        status: e.status,   // ← e.status could be anything
    });
});

This dispatches directly to DID_THROW_ITEM_INVALID (bypassing load-file-error), but it still copies e.status into the action. If e.status is undefined or not an object with main/sub, same crash.

How raw createResponse objects reach load-file-error

There IS one path where a raw createResponse object (with .code, no .status) reaches load-file-error:

When the server returns an HTTP error for a load request but the XHR onload handler treats it as success:

// line 4652
xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        api.onload(xhr);   // → blob processed
    } else {
        api.onerror(xhr);  // → error callback
    }
};

If xhr.status is 0 (aborted XHR), it goes to api.onerror. But if the XHR is in some intermediate state, or if there's a race, it might not reach either path cleanly. Firefox's behavior with aborted XHRs and responseType: 'blob' can produce edge cases where xhr.response is a malformed blob and FilePond's blob processing triggers internal errors that propagate differently.

Summary of the bug

Component Issue
createResponse() (line 4700) Uses .code, not .status
load-file-error handler (line 6777) Reads .status on a createResponse object → undefined
Error view writer (line 7847) No guard: crashes on undefined.status.main
Assistant view writer (line 10735) Same: crashes on undefined.status.main

The bug is in FilePond 4.32.12 vendor code. We cannot modify filepond.min.js.


Proposed fix

Option A: Patch the minified JS (risky but direct)

Find the load-file-errorDID_THROW_ITEM_INVALID dispatch in filepond.min.js and add a guard. Difficult because the code is minified and version-pinned via cache-busting query params.

Option B: Replace server.load with a custom function (cleanest)

In file-upload-filepond.js, replace the server.load URL string with a custom function that:

  1. Makes its own fetch/XHR to load.php
  2. On success: calls load(blob) — safe because serverFileReference is set for existing files
  3. On error: calls error('message') — safe because this goes through load-request-error (NOT load-file-error) which properly creates { status: { main, sub } }
  4. Never lets FilePond's internal createFetchFunction create a createResponse object with .code

This completely bypasses the buggy code path.

load: function(source, load, error, progress, abort, headers) {
    var xhr = new XMLHttpRequest();
    var url = base + '/load.php?id=' + encodeURIComponent(source);
    xhr.open('GET', url);
    xhr.responseType = 'blob';
    xhr.onload = function() {
        if (xhr.status >= 200 && xhr.status < 300) {
            load(xhr.response);
        } else {
            error('Fichier introuvable (HTTP ' + xhr.status + ')');
        }
    };
    xhr.onerror = function() {
        error('Erreur réseau');
    };
    xhr.onabort = abort;
    xhr.onprogress = function(e) {
        if (e.lengthComputable) progress(e.lengthComputable, e.loaded, e.total);
    };
    xhr.send();
    return { abort: function() { xhr.abort(); } };
},

Option C: Abort in-flight loads before destroying (defense in depth)

The destroyFilePondsIn() function in file-upload-filepond.js should abort in-flight loads/processing before calling pond.destroy(). Already partially attempted in commit znunoqpw but needs clean implementation.


Files involved

File Role
app/public/assets/js/vendor/filepond.min.js FilePond 4.32.12 — contains the bug (unmodifiable)
app/public/assets/js/app/file-upload-filepond.js Our FilePond wrapper — where the fix goes
app/src/FilepondHandler.php Server-side FilePond endpoints (process, load, revert, remove)
app/public/admin/actions/filepond/load.php Admin load endpoint
app/public/admin/actions/filepond/process.php Admin process endpoint
app/public/partage/actions/filepond/load.php Partage load endpoint
app/public/partage/actions/filepond/process.php Partage process endpoint

Reproduction

  1. just dev (PHP dev server on 127.0.0.1:8000)
  2. Open Firefox (Firefox triggers this more readily than Chromium due to different XHR abort behavior)
  3. Go to /admin/edit.php?id=<any> or /admin/add.php
  4. Click "Parcourir" on the "Image de couverture" FilePond input
  5. Select an image file → crash in console
  6. Or: drag a file to the "TFE" FilePond input → same crash if the load fails or races with HTMX swaps

What commit znunoqpw already did (insufficient)

  • Added Content-Type: text/plain headers to all FilepondHandler error responses
  • Fixed server.process.onerror to not access .status on a string
  • Converted server.load from a URL string to an object with onload/onerror
  • Added pre-destroy abort in destroyFilePondsIn()

These changes address server response format and cleanup ordering, but do not bypass the buggy load-file-erroraction.status path inside FilePond's internal code. The crash still reproduces.