Перейти к содержимому

Fetch API

Fetch — современный стандарт HTTP-запросов из браузера, построенный на Promises; ключевая ловушка: fetch не отклоняет Promise при HTTP-ошибках 4xx/5xx — это один из самых частых источников production-багов и проверяется на каждом собеседовании.

// Правильная обработка ошибок: проверяем res.ok
async function apiFetch(url, options = {}) {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options,
});
if (!res.ok) { // !ok = статус >= 400
const err = new Error(`HTTP ${res.status}: ${res.statusText}`);
err.status = res.status;
try { err.body = await res.json(); } catch {}
throw err;
}
return res.json();
}
// POST с JSON
await apiFetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Alice', role: 'admin' }),
});
// Заголовки ответа
const res = await fetch('/api/resource');
console.log(res.headers.get('Content-Type'));
console.log(res.headers.get('X-RateLimit-Remaining')); // только если CORS-exposed
// AbortController: таймаут и отмена
async function fetchWithTimeout(url, ms = 5000, options = {}) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(new Error('Timeout')), ms);
try {
const res = await fetch(url, { ...options, signal: ctrl.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
if (e.name === 'AbortError') throw new Error(`Request timed out after ${ms}ms`);
throw e;
} finally {
clearTimeout(timer);
}
}
// React: отмена при unmount
useEffect(() => {
const ctrl = new AbortController();
fetchWithTimeout('/api/data', 5000, { signal: ctrl.signal })
.then(setData)
.catch(e => { if (e.name !== 'AbortError') setError(e); });
return () => ctrl.abort(); // cleanup при unmount/re-render
}, []);
// Streaming response (ReadableStream) — например, LLM-стриминг
async function streamText(url, onChunk) {
const res = await fetch(url);
const reader = res.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
onChunk(decoder.decode(value, { stream: true }));
}
} finally {
reader.releaseLock();
}
}
// multipart/form-data (загрузка файлов)
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('meta', JSON.stringify({ name: 'doc' }));
// Content-Type: multipart/form-data + boundary выставляется браузером автоматически
await fetch('/upload', { method: 'POST', body: form });
// credentials: управление cookies cross-origin
// 'same-origin' (default) — cookies только для same-origin запросов
// 'include' — всегда (нужно Access-Control-Allow-Credentials: true)
// 'omit' — никогда
fetch('https://api.other.com/data', { credentials: 'include' });

Итог: Fetch: network error → reject; HTTP 4xx/5xx → resolve (res.ok === false); AbortController управляет отменой; ReadableStream — download-стриминг; upload-прогресс через Fetch недоступен — нужен XHR.