Home / Blog / JavaScript & TypeScript

API & webhooks

Fetch Data from a URL in JavaScript with a UTF-8 Size Limit

Read a bounded UTF-8 response stream with Fetch, preserve characters split across chunks, and reject oversized or malformed text without keeping partial output.

Define the bytes and text that the application accepts

Fetching data from a URL is only the start of reading it safely into an interface. A small text preview may need a strict size limit so an unexpectedly large response cannot be accumulated without bound. This example accepts UTF-8 text from a trusted endpoint and limits the bytes delivered through its response stream.

The limit is measured before decoding those stream chunks. It is not a character count, a compressed transfer-size measurement or a guarantee about every browser buffer. State that boundary precisely when reporting why a response was rejected.

Count chunks and decode them incrementally

async function readSmallUtf8(url, limit = 100000, fetcher = fetch) {
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000000) {
    throw new Error("Use a byte limit from 1 through 1000000.");
  }
  const response = await fetcher(url);
  if (!response.ok) {
    try { await response.body?.cancel(); } catch {}
    throw new Error(`HTTP ${response.status}`);
  }
  if (!response.body) return "";
  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8", {fatal: true});
  let bytes = 0, text = "";
  try {
    while (true) {
      const {done, value} = await reader.read();
      if (done) break;
      bytes += value.byteLength;
      if (bytes > limit) throw new Error("Response exceeds the byte limit.");
      text += decoder.decode(value, {stream: true});
    }
    return text + decoder.decode();
  } catch (error) {
    try { await reader.cancel(error); } catch {}
    throw error;
  } finally {
    reader.releaseLock();
  }
}

The function returns text only after the complete permitted stream has decoded. If a later chunk exceeds the limit or contains invalid UTF-8, no partial string is returned as a successful preview. The caller should show the error instead of retaining a previous result under the new URL.

Keep a character split across chunks intact

A multibyte UTF-8 character can begin in one chunk and finish in another. Decoding each chunk independently can introduce replacement characters or errors even when the complete byte sequence is valid. The streaming option preserves the decoder's state between chunks.

The final decode call flushes the remaining state. With fatal mode enabled, an incomplete or malformed ending is rejected instead of silently replacing bytes. The TextDecoder decode documentation explains these options.

This is deliberately a UTF-8 contract. A legacy-encoded text file needs an explicitly supported decoder and a corresponding content policy. Do not reinterpret arbitrary binary data as text and claim that every character has been preserved.

The decoder's default handling treats an initial UTF-8 byte-order mark as an encoding signature and omits it from the returned text. If a particular application needs that character preserved, select and test the appropriate decoder option. Keep this decision separate from ordinary whitespace: the helper does not trim spaces or line endings from the decoded content.

Render the completed string through textContent or an escaped text binding. A byte limit and valid UTF-8 do not make an HTML-looking response safe to insert as markup. Text that contains tags should remain visible text in a general preview.

Do not rely solely on Content-Length

A response may omit Content-Length, and transfer encoding or compression can make a header length unsuitable for the exact limit being enforced here. Counting the delivered chunks checks what this reader is actually accumulating.

The Response body documentation describes the readable stream. The browser can perform decompression before those bytes reach application code, so distinguish this application limit from a measurement of network traffic.

Release resources when a read fails

The catch branch attempts to cancel the reader when the function rejects, and finally releases its lock. Preserve the original failure if cancellation itself fails. A caller can then make a fresh request without accidentally reusing the consumed response.

This helper does not implement a timeout or user cancellation signal. Add those requirements through the fetch options and caller's ownership model if the application needs them. A size cap cannot stop a peer from delaying an otherwise small response indefinitely.

Test byte boundaries independently from character counts

Use an ASCII fixture, a string containing π split across chunks, an exact-limit response and a response one byte over the limit. Include an incomplete multibyte ending and a readable HTTP error. Verify that rejected cases expose no successful partial text.

After rejection, run a different valid fixture and check its exact text. The REST API Testing Tool can inspect authorized HTTP responses, but its general response display is separate from this helper's streaming byte policy. Keep those two capabilities distinct when explaining the result.