Skip to main content
The Fetch loader service provides resilient HTTP fetching with automatic retry logic and exponential backoff for downloading HLS fragments and playlists.

Overview

This service handles:
  • Downloading text-based playlists and manifests
  • Downloading binary fragment data as ArrayBuffers
  • Automatic retry with exponential backoff for transient network errors
  • HTTP error detection and propagation

API

The FetchLoader object exports two methods:
export const FetchLoader = {
  fetchText,
  fetchArrayBuffer,
};

Methods

fetchText

Fetches a resource as text with automatic retry.
async function fetchText(url: string, attempts: number = 1): Promise<string>

Parameters

url
string
required
URL of the resource to fetch
attempts
number
default:"1"
Maximum number of fetch attempts before giving up

Returns

Promise<string>
Promise resolving to the response body as text

Behavior

  • Uses the Fetch API to retrieve the resource
  • Checks response.ok and throws HttpError if status indicates failure
  • Retries on network errors (but not HTTP errors)
  • Applies exponential backoff starting at 100ms, increasing by 15% per retry

Example

import { FetchLoader } from "./fetch-loader";

// Fetch with 3 retry attempts
const playlist = await FetchLoader.fetchText(
  "https://example.com/playlist.m3u8",
  3
);
console.log(playlist);

fetchArrayBuffer

Fetches a resource as an ArrayBuffer with automatic retry.
async function fetchArrayBuffer(
  url: string,
  attempts: number = 1
): Promise<ArrayBuffer>

Parameters

url
string
required
URL of the resource to fetch
attempts
number
default:"1"
Maximum number of fetch attempts before giving up

Returns

Promise<ArrayBuffer>
Promise resolving to the response body as an ArrayBuffer

Behavior

  • Uses the Fetch API to retrieve the resource
  • Checks response.ok and throws HttpError if status indicates failure
  • Retries on network errors (but not HTTP errors)
  • Applies exponential backoff starting at 100ms, increasing by 15% per retry

Example

import { FetchLoader } from "./fetch-loader";

// Download fragment with retry
const fragmentData = await FetchLoader.fetchArrayBuffer(
  "https://example.com/segment0.ts",
  5
);
console.log(fragmentData.byteLength);

Retry mechanism

The retry logic is implemented in the internal fetchWithRetry function:
async function fetchWithRetry<Data>(
  fetchFn: FetchFn<Data>,
  attempts: number = 1
): Promise<Data>

Retry behavior

Initial delay
100ms
First retry waits 100ms after failure
Backoff multiplier
1.15
Each subsequent retry increases the delay by 15%
HTTP errors
no retry
HTTP errors (4xx, 5xx) are not retried and throw immediately
Network errors
retry
Network failures, timeouts, and other non-HTTP errors trigger retries

Retry timeline example

For attempts = 5:
  1. Initial attempt fails (network error)
  2. Wait 100ms, retry (fails)
  3. Wait 115ms (100 × 1.15), retry (fails)
  4. Wait 132ms (115 × 1.15), retry (fails)
  5. Wait 152ms (132 × 1.15), retry (succeeds)
If all retry attempts are exhausted, the last error is thrown to the caller.

Error handling

HttpError

Custom error class for HTTP status errors.
class HttpError extends Error {
  constructor(readonly status: number)
}

Properties

status
number
HTTP status code (e.g., 404, 500)
name
string
Always set to "HttpError"
message
string
Error message in format "HTTP {status}"

Example

try {
  await FetchLoader.fetchText("https://example.com/missing.m3u8", 3);
} catch (error) {
  if (error instanceof Error && error.name === "HttpError") {
    console.error("HTTP error:", (error as any).status);
  }
}

Error propagation

  • HTTP errors (4xx, 5xx): Thrown immediately as HttpError, no retries
  • Network errors: Retried up to the specified number of attempts
  • After all retries fail: The last error is thrown
  • Invalid attempts parameter: Throws "Attempts less then 1" if attempts < 1
HTTP errors are considered permanent failures and will not be retried. This includes 404 (Not Found), 403 (Forbidden), and 500 (Internal Server Error) responses.

Usage patterns

Fetching playlists

import { FetchLoader } from "./fetch-loader";

// Master playlist with 3 retries
const masterPlaylist = await FetchLoader.fetchText(
  "https://example.com/master.m3u8",
  3
);

// Media playlist with 5 retries
const mediaPlaylist = await FetchLoader.fetchText(
  "https://example.com/stream.m3u8",
  5
);

Downloading fragments

import { FetchLoader } from "./fetch-loader";

// Download multiple fragments with retry
const fragmentUrls = [
  "https://example.com/seg0.ts",
  "https://example.com/seg1.ts",
  "https://example.com/seg2.ts",
];

const fragments = await Promise.all(
  fragmentUrls.map((url) => FetchLoader.fetchArrayBuffer(url, 5))
);

Error handling with retries

import { FetchLoader } from "./fetch-loader";

try {
  const data = await FetchLoader.fetchArrayBuffer(
    "https://example.com/segment.ts",
    5
  );
  console.log("Downloaded:", data.byteLength, "bytes");
} catch (error) {
  if (error instanceof Error) {
    if (error.name === "HttpError") {
      console.error("HTTP error:", (error as any).status);
    } else {
      console.error("Network error after retries:", error.message);
    }
  }
}

Performance considerations

  • Concurrency: The loader itself does not limit concurrency. Use Promise.all with care to avoid overwhelming the server
  • Retry overhead: With 5 attempts and exponential backoff, total retry time can exceed 500ms
  • Memory: fetchArrayBuffer loads the entire response into memory as an ArrayBuffer
For production use, consider implementing concurrency limits using p-limit or similar libraries when downloading many fragments in parallel.

Source location

~/workspace/source/src/background/src/services/fetch-loader.ts:14

Build docs developers (and LLMs) love