Understanding HTTP Responses and Streams
Table of Contents 1. Understanding Response Objects 2. Different Ways to Consume Responses What is Binary Data? What's ASCII? What is ArrayBuffer? What is Uint8Array? 3. Streams Creating Your Own ReadableStream Why Streams? Combining Chunks Transform Streams What's Handled for You Best Practices 1. Memory Efficiency 2. Cancellation is Important 3. Common Stream Pipeline Recap of Practical Uses 1. Understanding Response Objects A Response object represents an HTTP response from a request. The most basic way to create a Response: const response = new Response("Hello world"); // Key properties that tell us about the response: console.log(response.status); // 200 (default) console.log(response.statusText); // OK console.log(response.ok); // true (status in 200-299 range) console.log(response.headers); // Headers object The Response interface gives us information about the HTTP response through its props: status: The HTTP status code (200, 404, 500, etc.) ok: Boolean indicating if status is in the successful range (200-299) headers: Access to the response headers An important concept is that Response bodies can only be consumed once: const response = new Response('{"message": "hello"}'); // This works const data = await response.json(); console.log(data); // { message: 'hello' } // This fails try { const data2 = await response.json(); } catch (error) { console.log("Error: Body already consumed!"); } If you need to read the body multiple times, use the clone() method before the first consumption: const response = new Response('{"message": "hello"}'); const clone = response.clone(); // Create copy before consuming const data1 = await response.json(); const data2 = await clone.json(); // Works! Using the clone Most of the time, you'll usually get Response objects from fetch: // Fetch returns a Promise const response = await fetch("https://api.example.com/data"); // Check response status if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // Check and handle content type const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { const data = await response.json(); // Process your JSON data } If you know it's JSON (a lot of the times you do), no need to check the content type. 2. Different Ways to Consume Responses We've seen .json() and .clone() in action already. Let's explore all consumption methods: // .text() for raw text data const textResponse = new Response("Hello, world"); const text = await textResponse.text(); console.log(text); // "Hello, world" // .blob() for binary data like images const imageResponse = await fetch("image.png"); const blob = await imageResponse.blob(); const imageUrl = URL.createObjectURL(blob); // .arrayBuffer() for raw binary data const buffer = await imageResponse.arrayBuffer(); const uint8Array = new Uint8Array(buffer); // .formData() for form data const formResponse = new Response("first_name=John&last_name=Doe", { headers: { "Content-Type": "application/x-www-form-urlencoded" }, }); const formData = await formResponse.formData(); console.log(formData.get("first_name")); // 'John' Key points about these methods: Each method returns a Promise that resolves to the appropriate data type Once you use any of these methods, the response body is consumed! Choose the method based on the type of data you're expecting: .json() for JSON data .text() for plain text, HTML, XML, etc. .blob() for files, images .arrayBuffer() when you need to process binary data directly .formData() for form submissions What is Binary Data? Data in its raw form of 1s and 0s. Everything in computers are binary data interpreted differently. Example: the letter 'A' in binary is 01000001 (65 in decimal). What's ASCII (American Standard Code for Information Interchange)? ASCII is a character encoding standard where each letter/symbol maps to a number (0-127). Modern systems mostly use UTF-8 (which is ASCII-compatible for 0-127) const a = 97; // lowercase 'a' in ASCII const A = 65; // uppercase 'A' in ASCII const text = "hello"; // Converts string to Uint8Array using UTF-8 encoding const asArray = new TextEncoder().encode(text); console.log(asArray); // Uint8Array: [104, 101, 108, 108, 111] What is ArrayBuffer? ArrayBuffer is raw binary data buffer, just bytes in memory. You can't manipulate ArrayBuffer directly. It's just raw memory. You need to use a view (a way to read and write to the ArrayBuffer) to access it. const buffer = new ArrayBuffer(4); // 4 bytes of memory const view = new Uint8Array(buffer); view[0] = 104; // 'h' view[1] = 105; // 'i' What is Uint8Array? A Uint8Array is a typed array that handles 8-bit unsigned integers. Each element is 8 bits (1 byte), allowing values from 0 to 255. const arr

Table of Contents
- 1. Understanding Response Objects
-
2. Different Ways to Consume Responses
- What is Binary Data?
- What's ASCII?
- What is ArrayBuffer?
- What is Uint8Array?
-
3. Streams
- Creating Your Own ReadableStream
- Why Streams?
- Combining Chunks
- Transform Streams
- What's Handled for You
-
Best Practices
- 1. Memory Efficiency
- 2. Cancellation is Important
- 3. Common Stream Pipeline
- Recap of Practical Uses
1. Understanding Response Objects
A Response object represents an HTTP response from a request.
The most basic way to create a Response:
const response = new Response("Hello world");
// Key properties that tell us about the response:
console.log(response.status); // 200 (default)
console.log(response.statusText); // OK
console.log(response.ok); // true (status in 200-299 range)
console.log(response.headers); // Headers object
The Response interface gives us information about the HTTP response through its props:
- status: The HTTP status code (200, 404, 500, etc.)
- ok: Boolean indicating if status is in the successful range (200-299)
- headers: Access to the response headers
An important concept is that Response bodies can only be consumed once:
const response = new Response('{"message": "hello"}');
// This works
const data = await response.json();
console.log(data); // { message: 'hello' }
// This fails
try {
const data2 = await response.json();
} catch (error) {
console.log("Error: Body already consumed!");
}
If you need to read the body multiple times, use the clone()
method before the first consumption:
const response = new Response('{"message": "hello"}');
const clone = response.clone(); // Create copy before consuming
const data1 = await response.json();
const data2 = await clone.json(); // Works! Using the clone
Most of the time, you'll usually get Response objects from fetch:
// Fetch returns a Promise
const response = await fetch("https://api.example.com/data");
// Check response status
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check and handle content type
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const data = await response.json();
// Process your JSON data
}
If you know it's JSON (a lot of the times you do), no need to check the content type.
2. Different Ways to Consume Responses
We've seen .json()
and .clone()
in action already.
Let's explore all consumption methods:
// .text() for raw text data
const textResponse = new Response("Hello, world");
const text = await textResponse.text();
console.log(text); // "Hello, world"
// .blob() for binary data like images
const imageResponse = await fetch("image.png");
const blob = await imageResponse.blob();
const imageUrl = URL.createObjectURL(blob);
// .arrayBuffer() for raw binary data
const buffer = await imageResponse.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
// .formData() for form data
const formResponse = new Response("first_name=John&last_name=Doe", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
const formData = await formResponse.formData();
console.log(formData.get("first_name")); // 'John'
Key points about these methods:
- Each method returns a Promise that resolves to the appropriate data type
- Once you use any of these methods, the response body is consumed!
- Choose the method based on the type of data you're expecting:
-
.json()
for JSON data -
.text()
for plain text, HTML, XML, etc. -
.blob()
for files, images -
.arrayBuffer()
when you need to process binary data directly -
.formData()
for form submissions
-
What is Binary Data?
Data in its raw form of 1s and 0s. Everything in computers are binary data interpreted differently.
Example: the letter 'A' in binary is 01000001 (65 in decimal).
What's ASCII (American Standard Code for Information Interchange)?
ASCII is a character encoding standard where each letter/symbol maps to a number (0-127).
Modern systems mostly use UTF-8 (which is ASCII-compatible for 0-127)
const a = 97; // lowercase 'a' in ASCII
const A = 65; // uppercase 'A' in ASCII
const text = "hello";
// Converts string to Uint8Array using UTF-8 encoding
const asArray = new TextEncoder().encode(text);
console.log(asArray); // Uint8Array: [104, 101, 108, 108, 111]
What is ArrayBuffer?
ArrayBuffer is raw binary data buffer, just bytes in memory.
You can't manipulate ArrayBuffer directly. It's just raw memory. You need to use a view (a way to read and write to the ArrayBuffer) to access it.
const buffer = new ArrayBuffer(4); // 4 bytes of memory
const view = new Uint8Array(buffer);
view[0] = 104; // 'h'
view[1] = 105; // 'i'
What is Uint8Array?
A Uint8Array is a typed array that handles 8-bit unsigned integers.
Each element is 8 bits (1 byte), allowing values from 0 to 255.
const array = new Uint8Array([104, 101, 108, 108, 111]); // "hello" in ASCII
console.log(array[0]); // 104 (ASCII code for 'h')
// Common ways to create
const empty = new Uint8Array(5); // Creates array of 5 zeros
const fromArray = new Uint8Array([1, 2, 3]); // From regular array
const fromBuffer = new Uint8Array(someArrayBuffer); // From ArrayBuffer
// You can't store values outside 0-255
const array2 = new Uint8Array([256, -1, 1.5]);
console.log(array2); // [0, 0, 1] (values are converted)
They're often used for:
- Processing network requests
- Reading/writing files
- Working with streams
- Handling images or audio data
The reason they're befitting for such use cases is because they're fixed-size arrays of 8-bit unsigned integers. Why 8 bits is good:
- Matches how data is commonly transmitted over networks (byte by byte)
- Most basic unit of data in computers is a byte (8 bits)
3. Streams
We've seen that Response.body is a ReadableStream.
// Basic stream reading
const response = await fetch("large-file.mp4");
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// value is a Uint8Array chunk
processChunk(value);
}
A ReadableStream represents a source of data that you can read from piece by piece.
- Read operations return an object with
{done, value}
-
value
is typically a Uint8Array chunk -
done
becomes true when stream is finished
Creating Your Own ReadableStream
You can create your own ReadableStream:
const stream = new ReadableStream({
start(controller) {
controller.enqueue("First chunk");
controller.enqueue("Second chunk");
controller.close(); // Close the stream
},
});
Why Streams?
If streams didn't exist, you'd have to load the entire file into memory.
const response = await fetch("huge-file.mp4");
const blob = await response.blob(); // Memory: