Repro on v3.18.0:
- Authenticate; go to /admin/drafts.
- Click Edit on any draft → /compose/edit/.
- Click Upload banner image, pick a PNG.
POST /compose/upload/ returns 403; status panel shows "Draft saving unavailable — copy your work before leaving".
Cause: the page captures meta[name=csrf-token] at initial render, but compose.go's edit-mode GET path appears to rotate the _csrf cookie (calls to refreshCSRFToken visible in compose.go around lines 220/240/292/310/329). The submitted X-CSRF-Token header is the page's stale meta value; the cookie has the new value → middleware.CSRF mismatch → 403.
Workaround: hard-reload /compose/edit/<slug> immediately before clicking Upload. Verified 100% reproducible and 100% fixed by the reload.
Reasonable fix: have the frontend refresh meta from the response on every CSRF-rotating GET, or stop rotating on idempotent edit-mode GETs, or have the upload XHR read the cookie directly (double-submit on body, not just meta-captured header).
Repro on v3.18.0:
POST /compose/upload/ returns 403; status panel shows "Draft saving unavailable — copy your work before leaving".
Cause: the page captures
meta[name=csrf-token]at initial render, butcompose.go's edit-mode GET path appears to rotate the_csrfcookie (calls torefreshCSRFTokenvisible in compose.go around lines 220/240/292/310/329). The submittedX-CSRF-Tokenheader is the page's stale meta value; the cookie has the new value →middleware.CSRFmismatch → 403.Workaround: hard-reload
/compose/edit/<slug>immediately before clicking Upload. Verified 100% reproducible and 100% fixed by the reload.Reasonable fix: have the frontend refresh meta from the response on every CSRF-rotating GET, or stop rotating on idempotent edit-mode GETs, or have the upload XHR read the cookie directly (double-submit on body, not just meta-captured header).