From 76ad744581d57de926c5e99e9b203fd6c04d9190 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sun, 2 Aug 2026 18:27:25 -0400 Subject: [PATCH] =?UTF-8?q?Convert=20every=20POST=20off=20multipart=20?= =?UTF-8?q?=E2=80=94=2021=20call=20sites=20across=207=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multipart POST to the plugin API hangs and never completes on this host: no status code, and no server-side trace of any kind. The correlation was exact — every page using URLSearchParams worked, every page using FormData hung, which is why scheduler and partnership appeared fine while docker, rsync, settings, auth, arrs, fallback and monitor did not. URLSearchParams has the same append API and fetch sets the urlencoded content type for it, so each site is a one-token change with the payload logic untouched. --- Plugin/unraid/README-unraid.md | 27 +++++++++++++++++++++++++++ Plugin/unraid/pages/arrs.php | 6 +++--- Plugin/unraid/pages/auth.php | 2 +- Plugin/unraid/pages/docker.php | 4 ++-- Plugin/unraid/pages/fallback.php | 4 ++-- Plugin/unraid/pages/monitor.php | 4 ++-- Plugin/unraid/pages/rsync.php | 14 +++++++------- Plugin/unraid/pages/settings.php | 8 ++++---- 8 files changed, 48 insertions(+), 21 deletions(-) diff --git a/Plugin/unraid/README-unraid.md b/Plugin/unraid/README-unraid.md index 05b8747..edae6b1 100644 --- a/Plugin/unraid/README-unraid.md +++ b/Plugin/unraid/README-unraid.md @@ -159,6 +159,33 @@ is already covered by a different platform mechanism: dynamix's `BodyInlineJS.ph hidden `csrf_token` input to every form on the page, which is what makes `VaraverkSettings.page` work without any of this. +### Never send a POST as multipart/form-data + +**`FormData` is banned in this plugin.** A `multipart/form-data` POST to the plugin API *hangs +and never completes* on this host — confirmed 2026-08-02. + +The failure is completely silent, which is what makes it dangerous: + +- The browser sends it correctly, valid token and all (verified in the Network tab). +- The Network row shows **no status code at all** — not 403, not 500. It never completes. +- Server side there is nothing: no CSRF termination in syslog, no fatal in `/var/log/phplog`, + and no output from a log statement that is the literal first line of the endpoint. + +Use `URLSearchParams`. `fetch` sets `application/x-www-form-urlencoded` for it automatically, +and it has the same `append()` / `set()` API, so it is a drop-in for any payload of strings: + +```js +const fd = new URLSearchParams(); // NOT new FormData() +fd.append('action', 'save'); +fetch(url, { method: 'POST', body: fd }); +``` + +The correlation across the plugin was exact: every page using `URLSearchParams` worked, every +page using `FormData` hung. All 21 call sites were converted in one pass. Suspected to date from +the Unraid 7.3.1→7.3.2 upgrade; root cause in nginx/php-fpm was never identified, only the +workaround. If a POST ever hangs with no status code and leaves no server-side trace whatsoever, +**suspect the encoding first** — not CSRF, not auth, not the endpoint. + ### api/webhook.php is dead code It receives arr download events — but an arr has no WebGUI session and no CSRF token, so it is diff --git a/Plugin/unraid/pages/arrs.php b/Plugin/unraid/pages/arrs.php index 8acbba0..c1e468b 100644 --- a/Plugin/unraid/pages/arrs.php +++ b/Plugin/unraid/pages/arrs.php @@ -435,7 +435,7 @@ setInterval(vvArrsLoad, 60000); function vvArrsRefreshRemote(host) { const btn = document.getElementById('vv-arr-rfsh-' + host); if (btn) { btn.disabled = true; btn.textContent = '↻…'; } - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'refresh_remote'); fd.append('host', host); fetch('/plugins/varaverk/api/arrs.php', { method: 'POST', body: fd }) @@ -452,7 +452,7 @@ function vvArrsRefreshRemote(host) { function vvArrToggle(track, key, file) { const on = !track.classList.contains('on'); track.classList.toggle('on', on); - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'arrs'); fd.append('changes', JSON.stringify([{ file, key, value: on ? 'true' : 'false', type: 'scalar' }])); fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd }) @@ -468,7 +468,7 @@ function vvArrSaveAge() { const fb = document.getElementById('vv-arr-age-fb'); if (!val) return; btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'arrs'); fd.append('changes', JSON.stringify([{ file: 'master.conf', key: 'ARR_IMPORT_RECOVERY_AGE', value: val, type: 'scalar' diff --git a/Plugin/unraid/pages/auth.php b/Plugin/unraid/pages/auth.php index 8489fc3..817f9eb 100644 --- a/Plugin/unraid/pages/auth.php +++ b/Plugin/unraid/pages/auth.php @@ -297,7 +297,7 @@ function _get(action, cb) { } function _post(params, cb) { - const fd = new FormData(); + const fd = new URLSearchParams(); for (const [k, v] of Object.entries(params)) fd.append(k, v); fetch(API, { method: 'POST', body: fd }) .then(r => r.json()).then(cb) diff --git a/Plugin/unraid/pages/docker.php b/Plugin/unraid/pages/docker.php index 3e19f30..9acc220 100644 --- a/Plugin/unraid/pages/docker.php +++ b/Plugin/unraid/pages/docker.php @@ -143,7 +143,7 @@ let _popTarget = null; // ── API ─────────────────────────────────────────────────────────────────────── function _api(params, cb) { - const fd = new FormData(); + const fd = new URLSearchParams(); for (const [k,v] of Object.entries(params)) fd.append(k, v); fetch('/plugins/varaverk/api/docker.php', {method:'POST', body:fd}) .then(r => r.json()).then(cb) @@ -310,7 +310,7 @@ function _render(data) { let _activeJobs = {}; // { ctrName: intervalId } function _actApi(params, cb) { - const fd = new FormData(); + const fd = new URLSearchParams(); for (const [k, v] of Object.entries(params)) fd.append(k, v); fetch('/plugins/varaverk/api/docker_action.php', {method: 'POST', body: fd}) .then(r => r.json()).then(cb) diff --git a/Plugin/unraid/pages/fallback.php b/Plugin/unraid/pages/fallback.php index 9768727..cf89c2d 100644 --- a/Plugin/unraid/pages/fallback.php +++ b/Plugin/unraid/pages/fallback.php @@ -398,7 +398,7 @@ function vvFbLoad() { window.vvFbToggle = function(track, key) { const on = !track.classList.contains('on'); track.classList.toggle('on', on); - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'fallback'); fd.append('changes', JSON.stringify([{ file: 'master.conf', key, value: on ? 'true' : 'false', type: 'scalar' }])); fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd }) @@ -423,7 +423,7 @@ window.vvFbSaveSettings = function() { } btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'fallback'); fd.append('changes', JSON.stringify([ { file: 'master.conf', key: 'FALLBACK_CHECK_INTERVAL', value: String(interval), type: 'scalar' }, diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 8e77234..c48f576 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -612,7 +612,7 @@ function vvWdRsyncToggle(el) { el.style.color = on ? '#4caf50' : '#333'; el.style.background = on ? '#0f1a0f' : '#111'; el.style.borderColor = on ? '#1a3a1a' : '#222'; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('name', flag); fd.append('enabled', on ? '1' : '0'); fetch('/plugins/varaverk/api/flag_toggle.php', { method: 'POST', body: fd }) @@ -2229,7 +2229,7 @@ function vvDockerAction(action, name, webui) { encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true'; return; } - const fd = new FormData(); + const fd = new URLSearchParams(); fd.set('action', action); fd.set('name', name); fetch('/plugins/varaverk/api/docker_action.php', { method: 'POST', body: fd }) diff --git a/Plugin/unraid/pages/rsync.php b/Plugin/unraid/pages/rsync.php index 4fea997..a4d88e2 100644 --- a/Plugin/unraid/pages/rsync.php +++ b/Plugin/unraid/pages/rsync.php @@ -1004,7 +1004,7 @@ function vvRyWinSave(key) { if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; } if (fb) fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'save'); fd.append('win_key', key); fd.append('scripts', JSON.stringify(state.scripts)); @@ -1247,7 +1247,7 @@ function _vvRyLoadProfiles(key) { function vvRyToggle(trackEl, name) { const on = !trackEl.classList.contains('on'); trackEl.classList.toggle('on', on); - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('name', name); fd.append('enabled', on ? '1' : '0'); fetch('/plugins/varaverk/api/flag_toggle.php', { method: 'POST', body: fd }) @@ -1269,7 +1269,7 @@ function vvRySaveDefaults() { btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'rsync'); fd.append('changes', JSON.stringify(changes)); fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd }) @@ -1441,7 +1441,7 @@ function vvRpSave() { const btn = document.querySelector('#vv-rp-card .vv-ry-save-btn'); btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'save'); fd.append('name', name); fd.append('rsync_opts', document.getElementById('vv-rp-rsync_opts')?.value?.trim() || ''); @@ -1478,7 +1478,7 @@ function vvRpDelete() { const del = document.getElementById('vv-rp-del-btn'); del.disabled = true; fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'delete'); fd.append('name', name); @@ -1521,7 +1521,7 @@ function vvMsStop() { if (!token) return; const btn = document.getElementById('vv-ms-stop-btn'); btn.textContent = '⟳ Stopping…'; btn.disabled = true; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'stop'); fd.append('token', token); fetch('/plugins/varaverk/api/manual_sync.php', { method: 'POST', body: fd }) @@ -1768,7 +1768,7 @@ function vvMsRun() { badge.style.color = '#4a9eff'; badge.style.border = '1px solid #1a3a5a'; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'run'); fd.append('local', local); fd.append('host', slot); diff --git a/Plugin/unraid/pages/settings.php b/Plugin/unraid/pages/settings.php index 3d1f47f..07a49c1 100644 --- a/Plugin/unraid/pages/settings.php +++ b/Plugin/unraid/pages/settings.php @@ -286,7 +286,7 @@ function vvStorMigrate() { if (fb) fb.textContent = ''; if (out) { out.textContent = 'Starting migration…\n'; out.style.display = ''; } - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'migrate'); fd.append('to', to); @@ -323,7 +323,7 @@ function vvNtfToggle(track, key) { track.classList.toggle('on', on); const file = (key === 'NOTIFY_UNRAID' || key === 'ENABLE_LOGGING') ? 'master.conf' : '.conf'; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'settings'); fd.append('changes', JSON.stringify([{ file, key, value: on ? 'true' : 'false', type: 'scalar' }])); fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd }) @@ -337,7 +337,7 @@ function vvNtfSaveWebhook() { const fb = document.getElementById('vv-ntf-fb'); const btn = document.getElementById('vv-ntf-save'); btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('id', 'settings'); fd.append('changes', JSON.stringify([{ file: '.conf', @@ -400,7 +400,7 @@ function vvApiRenew() { btn.disabled = true; btn.textContent = 'Renewing…'; fb.textContent = ''; out.textContent = ''; out.style.display = ''; - const fd = new FormData(); + const fd = new URLSearchParams(); fd.append('action', 'setup_apikeys'); fetch('/plugins/varaverk/api/storage.php', { method: 'POST', body: fd }) .then(r => r.json())