DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Fetch API: Streaming, Cancellation, CORS, and Retry Patterns

The Fetch API is the modern way to make HTTP requests in the browser. It replaced XMLHttpRequest with a cleaner, Promise-based interface. Firefox has full Fetch support including streaming, request cancellation, and custom headers. This guide covers practical patterns beyond the basics.

Basic Fetch

// Simple GET
const response = await fetch('/api/users');
const users = await response.json();

// Always check the status — fetch only rejects on network errors, not HTTP errors
if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
Enter fullscreen mode Exit fullscreen mode
// POST with JSON body
const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCsrfToken(),
  },
  body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
});
Enter fullscreen mode Exit fullscreen mode

The Response Object

const response = await fetch('/api/data');

response.status      // HTTP status code (200, 404, etc.)
response.ok          // true if status is 200-299
response.statusText  // "OK", "Not Found", etc.
response.headers     // Headers object

// Body reading methods (each can only be called once)
await response.json()      // Parse as JSON
await response.text()      // Get as string
await response.blob()      // Get as Blob (file data)
await response.arrayBuffer() // Raw bytes
await response.formData()  // Form data

// Stream the body (advanced)
const reader = response.body.getReader();
Enter fullscreen mode Exit fullscreen mode

Sending Different Body Types

// JSON
fetch('/api/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
});

// Form data (multipart, for file uploads)
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', 'My Document');
fetch('/api/upload', { method: 'POST', body: formData });
// Note: Don't set Content-Type header — browser sets it with boundary automatically

// URL-encoded form data
const params = new URLSearchParams({ key: 'value', other: '123' });
fetch('/api/form', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: params,
});

// Raw binary
fetch('/api/binary', {
  method: 'POST',
  headers: { 'Content-Type': 'application/octet-stream' },
  body: arrayBuffer,
});
Enter fullscreen mode Exit fullscreen mode

Cancellation with AbortController

const controller = new AbortController();

// Cancel after 5 seconds
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('/api/slow-endpoint', {
    signal: controller.signal,
  });
  clearTimeout(timeoutId);
  return await response.json();
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Request was cancelled');
  } else {
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode
// Cancel previous search when user types
let searchController = null;

async function search(query) {
  searchController?.abort(); // Cancel previous request
  searchController = new AbortController();

  try {
    const resp = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: searchController.signal,
    });
    return await resp.json();
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
    return null; // Cancelled, ignore
  }
}
Enter fullscreen mode Exit fullscreen mode

Streaming Responses

For large responses, stream them instead of loading everything into memory:

async function streamLargeFile(url, onProgress) {
  const response = await fetch(url);
  const contentLength = response.headers.get('content-length');
  const total = parseInt(contentLength, 10);

  const reader = response.body.getReader();
  const chunks = [];
  let received = 0;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    chunks.push(value);
    received += value.length;
    onProgress(received / total);
  }

  const allChunks = new Uint8Array(received);
  let position = 0;
  for (const chunk of chunks) {
    allChunks.set(chunk, position);
    position += chunk.length;
  }

  return allChunks.buffer;
}
Enter fullscreen mode Exit fullscreen mode

Server-Sent Events via Fetch

Stream data from the server in real-time:

async function streamAI(prompt, onToken) {
  const response = await fetch('/api/ai/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const text = decoder.decode(value, { stream: true });
    // Parse SSE format: "data: {...}\n\n"
    const lines = text.split('\n');
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        if (data.token) onToken(data.token);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CORS and Fetch Modes

// Default: 'cors' — follows CORS rules, sends credentials if same-origin
fetch('https://api.example.com/data');

// 'no-cors': can't read response, but useful for firing analytics
fetch('https://analytics.example.com/track', {
  method: 'POST',
  mode: 'no-cors',
  body: JSON.stringify(event),
});

// 'same-origin': fail if cross-origin
fetch('/api/data', { mode: 'same-origin' });

// Sending cookies cross-origin
fetch('https://api.example.com/user', {
  credentials: 'include', // Always send cookies
});

// 'same-origin' credentials (default): send cookies only if same origin
Enter fullscreen mode Exit fullscreen mode

Retry Logic

async function fetchWithRetry(url, options, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (response.status === 429) {
        // Rate limited: wait and retry
        const retryAfter = parseInt(response.headers.get('retry-after') || '1', 10);
        await new Promise(r => setTimeout(r, retryAfter * 1000));
        continue;
      }
      if (response.status >= 500 && attempt < maxRetries) {
        // Server error: retry with backoff
        await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
        continue;
      }
      return response;
    } catch (err) {
      if (err.name === 'AbortError') throw err; // Don't retry cancellations
      lastError = err;
      if (attempt < maxRetries) {
        await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
      }
    }
  }

  throw lastError;
}
Enter fullscreen mode Exit fullscreen mode

Debugging Fetch in Firefox DevTools

Firefox's Network panel shows every fetch call:

  1. Open DevTools (F12) → Network tab
  2. Click any request to see:
    • Request headers and body
    • Response headers and body
    • Timing: DNS lookup, connection, SSL handshake, TTFB, content download
  3. Right-click → Copy as Fetch: copies the request as a JavaScript fetch() call you can run in the Console
  4. Right-click → Resend to replay the request (with modifications)

The "XHR" filter shows only fetch/XMLHttpRequest calls, filtering out images/CSS/JS.

Top comments (0)