fix: logout servi « hors ligne » par le SW + drag&drop upload en 403 CSRF (v7.45.1)
- SW : le fetch event de navigation arrive en redirect:'manual' → toute 302 du serveur (logout → /auth/login) se lisait opaqueredirect (status 0) → « bad status » → page hors ligne. networkFirst rejoue la requête en redirect:'follow' et sert une 302 synthétique vers l'URL finale (Chromium refuse une response 'redirected' servie à une navigation → ERR_FAILED). timeoutFetch annule désormais réellement (AbortController branchée). - Local workspace : _doUpload (upload/upload-folder) et toggleFavorite n'envoyaient pas X-CSRF-Token → 403 systématique depuis A19 (derniers appels mutants du front, balayage complet refait). - Porte : e2e/regression_logout_dnd.spec.js (2 tests verts, joués sur l'image rebuildée) · pytest 1094 passed · ruff OK · OpenAPI 7.45.1.
This commit is contained in:
@@ -628,7 +628,7 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
|
||||
if (!node) return;
|
||||
try {
|
||||
var method = node.favorited ? 'DELETE' : 'POST';
|
||||
var r = await fetch('/board/api/favorites/' + node.id, { method: method });
|
||||
var r = await fetch('/board/api/favorites/' + node.id, { method: method, headers: {'X-CSRF-Token': getCsrf()} });
|
||||
if (r.ok) {
|
||||
node.favorited = !node.favorited;
|
||||
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
|
||||
@@ -1763,7 +1763,7 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
|
||||
var url = folderHandled ? '/api/local-workspace/upload-folder' : '/api/local-workspace/upload';
|
||||
|
||||
try {
|
||||
var r = await fetch(url, { method: 'POST', body: formData });
|
||||
var r = await fetch(url, { method: 'POST', headers: {'X-CSRF-Token': getCsrf()}, body: formData });
|
||||
this.uploadProgress = 80;
|
||||
var d = await r.json();
|
||||
if (r.ok) {
|
||||
|
||||
+18
-5
@@ -156,9 +156,22 @@ async function cacheFirst(request, { cacheName = CACHE_NAME } = {}) {
|
||||
async function networkFirst(request, { cacheName = CACHE_NAME, fallbackToShell = false, timeoutMs = 4000 } = {}) {
|
||||
const cache = await caches.open(cacheName);
|
||||
try {
|
||||
const res = await timeoutFetch(request, timeoutMs);
|
||||
let res = await timeoutFetch(request, timeoutMs);
|
||||
// Les navigations interceptées arrivent en redirect:'manual' : une 302 du
|
||||
// serveur se lit opaqueredirect (status 0) → on rejoue la requête avec suivi
|
||||
// explicite. Sans ça, tout logout / redirection rendait la page hors ligne.
|
||||
if (res.type === 'opaqueredirect') {
|
||||
// Navigation en redirect:'manual' : on suit la redirection à la main.
|
||||
res = await timeoutFetch(new Request(request, { redirect: 'follow' }), timeoutMs);
|
||||
if (res && res.ok && res.redirected) {
|
||||
// Chromium refuse une response 'redirected' servie à une navigation
|
||||
// (ERR_FAILED) → on émet une vraie redirection vers l'URL finale.
|
||||
return new Response(null, { status: 302, headers: { Location: res.url } });
|
||||
}
|
||||
}
|
||||
if (res && res.ok) {
|
||||
cache.put(request, res.clone());
|
||||
// Ne pas mettre en cache une réponse de redirection sous l'URL d'origine.
|
||||
if (!res.redirected) cache.put(request, res.clone());
|
||||
return res;
|
||||
}
|
||||
throw new Error('bad status');
|
||||
@@ -176,12 +189,12 @@ async function networkFirst(request, { cacheName = CACHE_NAME, fallbackToShell =
|
||||
|
||||
function timeoutFetch(request, ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
ctrl.abort();
|
||||
reject(new Error('timeout'));
|
||||
}, ms);
|
||||
fetch(request).then(
|
||||
fetch(request, { signal: ctrl.signal }).then(
|
||||
(res) => { clearTimeout(timer); resolve(res); },
|
||||
(err) => { clearTimeout(timer); reject(err); }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user