fetch em profundidade
TL;DR
fetché a API nativa para HTTP no browser: baseada em Promises, retorna umResponseque tem um body como stream — você precisa chamar.json(),.text()ou.blob()para consumir. Um detalhe contra-intuitivo:fetchsó rejeita a Promise em erro de rede — respostas HTTP 4xx e 5xx são resolvidas com sucesso. Sempre verificarresponse.ok.
A anatomia de uma requisição fetch
const response = await fetch(url, options);const options = {
method: 'POST', // 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
},
body: JSON.stringify(data), // string | Blob | BufferSource | FormData | URLSearchParams | ReadableStream
// Controle de cache
cache: 'default', // 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache'
// Referrer
referrerPolicy: 'strict-origin-when-cross-origin',
// Credenciais (cookies)
credentials: 'same-origin', // 'omit' | 'same-origin' | 'include'
// CORS
mode: 'cors', // 'cors' | 'no-cors' | 'same-origin'
// Redirect
redirect: 'follow', // 'follow' | 'error' | 'manual'
// Fetch priority (hint para o browser)
priority: 'auto', // 'high' | 'low' | 'auto'
// AbortController
signal: controller.signal,
};O objeto Response
const response = await fetch('/api/user');
// Metadata
response.ok; // true se status 200-299
response.status; // 200, 404, 500, etc.
response.statusText; // 'OK', 'Not Found', etc.
response.url; // URL final (após redirects)
response.redirected; // true se houve redirect
response.type; // 'basic' | 'cors' | 'opaque'
// Headers
response.headers.get('Content-Type');
response.headers.get('X-Rate-Limit');
for (const [key, value] of response.headers) { ... }
// Body (cada método consome o stream — só pode ser chamado uma vez)
const json = await response.json(); // parsear como JSON
const text = await response.text(); // como string
const blob = await response.blob(); // como Blob (imagens, binários)
const buffer = await response.arrayBuffer(); // como ArrayBuffer
const form = await response.formData(); // como FormData
// Verificar se o body ainda pode ser lido
response.bodyUsed; // true se já foi consumido
response.body; // ReadableStream (para streaming)O erro mais comum: fetch não rejeita em erros HTTP
// ❌ Armadilha: 404 e 500 resolvem a Promise!
const response = await fetch('/api/user/999');
// response.status === 404, mas não entrou no catch!
// ✅ Sempre verificar response.ok
const response = await fetch('/api/user/999');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();Wrapper com tratamento de erro padrão
class ApiError extends Error {
constructor(status, statusText, body) {
super(`${status} ${statusText}`);
this.name = 'ApiError';
this.status = status;
this.body = body;
}
}
async function apiFetch(url, options = {}) {
const response = await fetch(url, {
headers: { 'Accept': 'application/json', ...options.headers },
...options,
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new ApiError(response.status, response.statusText, body);
}
// Respostas sem body (204 No Content)
const contentType = response.headers.get('Content-Type') ?? '';
if (response.status === 204 || !contentType.includes('application/json')) {
return null;
}
return response.json();
}
// Uso
try {
const user = await apiFetch('/api/users/1');
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
console.log('Usuário não encontrado');
} else if (error instanceof ApiError && error.status === 401) {
redirectToLogin();
} else {
throw error; // re-throw erros inesperados
}
}CORS e credenciais
// Request cross-origin com cookies (credenciais)
const response = await fetch('https://api.outro-dominio.com/data', {
credentials: 'include', // enviar cookies na request cross-origin
// O servidor precisa responder com:
// Access-Control-Allow-Origin: https://meu-dominio.com (não pode ser '*')
// Access-Control-Allow-Credentials: true
});
// 'same-origin' (padrão): enviar cookies só para mesma origem
// 'omit': nunca enviar cookies
// 'include': sempre enviar cookies (inclusive cross-origin)Verificar se fetch está disponível (legado)
if (!window.fetch) {
// Polyfill: github.com/github/fetch
}fetch está disponível em todos os browsers modernos desde 2017. Não é mais necessário verificar em projetos que suportam apenas browsers modernos.
Para fixar
- Quando
fetchrejeita a Promise? O que não faz rejeitar?- Por que você não pode chamar
response.json()duas vezes?- Qual a diferença entre
credentials: 'same-origin'ecredentials: 'include'?- O que é
response.ok? Quais status codes fazem ela sertrue?- Por que uma resposta 500 “resolveria” a Promise de um fetch?
Veja também
- 02 — JSON, FormData e tipos de body — próxima
- 07 — AbortController — cancelar requests
- Networking — índice