diff --git a/public/index.html b/public/index.html index 0e2c0d7..2cbb6c2 100644 --- a/public/index.html +++ b/public/index.html @@ -34,15 +34,15 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/public/js/api.js b/public/js/api.js index c5c8a3f..4413fb4 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -41,41 +41,50 @@ const Store = { }; async function api(path, opts = {}, _retry = 0) { - opts.headers = opts.headers || {}; - if (Store.access && !opts.headers.Authorization) { - opts.headers.Authorization = `Bearer ${Store.access}`; + // rebuild opts on every retry (body may be already stringified) + const o = { ...opts, headers: { ...(opts.headers || {}) } }; + if (Store.access && !o.headers.Authorization) { + o.headers.Authorization = `Bearer ${Store.access}`; } - if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) { - opts.headers['Content-Type'] = 'application/json'; - opts.body = JSON.stringify(opts.body); + if (o.body && typeof o.body === 'object' && !(o.body instanceof FormData)) { + o.headers['Content-Type'] = 'application/json'; + o.body = JSON.stringify(o.body); } let r; + // timeout 15s — иначе UI висит вечно + const controller = new AbortController(); + const tid = setTimeout(() => controller.abort(), 15000); try { - r = await fetch(API_BASE + path, opts); + r = await fetch(API_BASE + path, { ...o, signal: controller.signal }); } catch (networkErr) { - // сеть упала — ретрай через 2с + clearTimeout(tid); + console.warn('fetch network error:', networkErr); + if (networkErr.name === 'AbortError') { + throw new Error('Сервер не отвечает (таймаут 15с)'); + } if (_retry < 1) { await new Promise((res) => setTimeout(res, 2000)); return api(path, opts, _retry + 1); } throw new Error('Нет связи с сервером'); } + clearTimeout(tid); if (r.status === 401 && Store.refresh && !opts._isRefresh) { - // refresh - const r2 = await fetch(API_BASE + '/api/v1/auth/refresh', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ Refresh: Store.refresh }), - _isRefresh: true, - }); - if (r2.ok) { - const tokens = await r2.json(); - Store.save(tokens); - opts.headers.Authorization = `Bearer ${Store.access}`; - return api(path, opts, _retry + 1); - } else { - Store.clear(); - } + try { + const r2 = await fetch(API_BASE + '/api/v1/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Refresh: Store.refresh }), + }); + if (r2.ok) { + const tokens = await r2.json(); + Store.save(tokens); + o.headers.Authorization = `Bearer ${Store.access}`; + return api(path, opts, _retry + 1); + } else { + Store.clear(); + } + } catch (e) { console.warn('refresh failed', e); } } if (r.status === 502 || r.status === 503 || r.status === 504) { if (_retry < 1) {