Send one successful response, write its body progressively, and finish the message normally. This Mac Terminal walkthrough shows how to deliver a complete synthetic document without treating each chunk as a separate response or using 206 Partial Content.
The task is to serve one HTML page intended for public hosting, then verify that a client receives its complete content. The demonstration runs locally and does not publish anything. You need Terminal, curl, and a supported Node.js LTS release from the official Node.js download page.
Why chunks do not mean partial responses
A response’s meaning and its delivery framing answer different questions. For an ordinary successful GET, 200 OK represents successful retrieval. 206 Partial Content means the server is fulfilling a range request by transferring selected portions of a representation. Splitting delivery into several writes does not create a range request. RFC 9110: HTTP Semantics, 206 Partial Content.
For example, a synthetic guide containing an introduction, a description, and a closing paragraph can arrive progressively while remaining one document. Send its response headers once, followed by those body sections in order. Do not prepend a new HTTP status line to each section.
HTTP/1.1 chunked transfer coding wraps body bytes with hexadecimal lengths and ends with a zero-sized chunk and the required terminating empty line. Those framing bytes delimit delivery; they are not article content. A sender must not combine Content-Length with Transfer-Encoding. RFC 9112: Chunked Transfer Coding, Content-Length.
The practical rule is to let the HTTP library frame the message. Your application supplies document content and signals when that content is finished.
Which streaming mechanism should you use?
The supported mechanism depends on the HTTP version. Streaming a complete body remains possible across all three versions below, but their framing differs.
| HTTP version | How body content travels | Completion mechanism |
|---|---|---|
| HTTP/1.1 with chunked coding | Length-prefixed chunks | Terminal zero-sized chunk and completed trailer section |
| HTTP/2 | DATA frames |
END_STREAM on the concluding frame, including completion of any header block |
| HTTP/3 | DATA frames |
Completion of the response and closure of the stream’s sending direction |
HTTP/2 explicitly disallows HTTP/1.1 chunked transfer coding. HTTP/3 does not define transfer codings and prohibits the Transfer-Encoding header. Let the server implement its protocol’s framing instead of forcing Transfer-Encoding: chunked onto every connection. RFC 9113: HTTP Message Exchanges, RFC 9114: HTTP Message Framing.
For a small document that is already assembled, sending it normally is usually the easiest implementation. Progressive writes become useful when content is produced over time. The example below deliberately spaces three writes apart so you can inspect the behavior; its delays are illustrative, not a publishing recommendation.
Start a complete streaming document on your Mac
Open Terminal and check the prerequisites:
node --version
curl --version
If node is unavailable, install a supported LTS release using the Node.js download page, then open a new Terminal window. The program uses only built-in modules and needs no npm packages.
Paste the complete block below into Terminal. It starts an HTTP/1.1 server at the demonstration address http://127.0.0.1:8080/document. The three strings together form one complete synthetic page.
node <<'JS'
const http = require('node:http');
const parts = [
'<!doctype html>\n<html lang="en"><head><meta charset="utf-8">' +
'<title>Paper Shapes</title></head><body><main>\n' +
'<h1>Paper Shapes</h1>\n',
'<p>A circle, a square, and a triangle make this sample collection.</p>\n',
'<p>End of the paper shapes guide.</p>\n</main></body></html>\n'
];
const server = http.createServer((req, res) => {
res.on('error', error => console.error(error.message));
if (req.method !== 'GET' || req.url !== '/document') {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('No sample document at this path.\n');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
let index = 0;
const timer = setInterval(() => {
if (res.destroyed) {
clearInterval(timer);
return;
}
res.write(parts[index++]);
if (index === parts.length) {
clearInterval(timer);
res.end();
}
}, 300);
res.on('close', () => clearInterval(timer));
});
server.on('error', error => {
console.error(error.message);
process.exitCode = 1;
});
server.listen(8080, '127.0.0.1', () => {
console.log('Sample: http://127.0.0.1:8080/document');
});
JS
The application calls writeHead() once, write() three times, and end() once. Node documents successive writes as parts of the same response body and requires end() to complete each response. It handles the HTTP framing; do not add chunk lengths or a terminal zero to the strings. Node.js HTTP documentation.
This is a bounded demonstration with three small strings. For a large or continuous producer, respect backpressure: when write() returns false, pause production until drain, while also handling closure and errors. Node.js response writing documentation.
Leave this Terminal window running. If it reports EADDRINUSE, port 8080 is occupied; choose another available port in the server and change the port in every client command below. Stop the demonstration with Control-C when finished.
Inspect both decoded content and HTTP chunk framing
In a second Terminal window, request the document:
curl --http1.1 --include --no-buffer --max-time 5 \
http://127.0.0.1:8080/document
Expect one 200 OK response, a Content-Type of text/html; charset=utf-8, and the assembled page content. For this HTTP/1.1 example, the response should use Transfer-Encoding: chunked. Curl normally decodes transfer framing, so its ordinary output shows the document rather than hexadecimal chunk sizes.
To inspect the framing, repeat the request with --raw:
curl --http1.1 --include --no-buffer --raw --max-time 5 \
http://127.0.0.1:8080/document
Curl documents --raw as disabling internal decoding of content and transfer encodings. --no-buffer disables curl’s output buffering, and --max-time 5 bounds the operation. These options make this a short diagnostic rather than an indefinitely waiting command. curl manual.
In raw output, size lines surround the content, followed by the terminating zero-sized chunk. Without that terminator, a chunked HTTP/1.1 body is incomplete. With fixed-length framing instead, receiving fewer bytes than the declared length is incomplete. RFC 9112: Handling Incomplete Messages.
Use raw output only to examine transport. Do not copy its size lines into a page file or treat them as document sections.
Receive the whole document before processing it
If your next step needs the complete page, consume the whole body before parsing, saving, or otherwise accepting it. WHATWG defines a response body as a stream; its text() method fully reads that body and returns a string, rejecting when body consumption fails. Receiving response headers alone does not establish that the body has finished. WHATWG Fetch Standard: Body mixin, Processing responses.
With the server still running, paste this complete client into the second Terminal window:
node --input-type=module <<'JS'
try {
const response = await fetch('http://127.0.0.1:8080/document', {
signal: AbortSignal.timeout(5000)
});
if (response.status !== 200) {
throw new Error(`Expected 200; received ${response.status}`);
}
const html = await response.text();
const marker = 'End of the paper shapes guide.';
if (!html.includes(marker) || !html.endsWith('</html>\n')) {
throw new Error('Expected sample ending is missing');
}
console.log(html);
console.log('Complete sample document received.');
} catch (error) {
console.error('Document not accepted:', error.message);
process.exitCode = 1;
}
JS
Node provides a browser-compatible fetch() implementation. Its AbortSignal.timeout() creates a signal that aborts after the specified milliseconds; here, five seconds is an illustrative limit for this tiny local example. Node.js global objects documentation, AbortSignal timeout documentation.
The client checks two different things: successful body consumption and the expected sample ending. That second check is an application-specific diagnostic. HTTP can successfully transfer a document that your application accidentally shortened before calling end(). A completion marker helps catch that particular mistake, but does not validate every paragraph or constitute general HTML validation.
This client buffers the entire page in memory. For very large documents, choose incremental processing with an explicit document format and completion rules. Do not assume that each read delivers a complete paragraph or an independently meaningful response.
Diagnose completion before changing the status code
Use this sequence to locate the problem. It separates transport completion from the application’s intended content, so an apparently successful download does not conceal an omitted ending.
| Observation | Next action |
|---|---|
| The local command cannot connect | Confirm the server is running and the port matches. |
| Curl times out | Check that the producer finishes and calls end(); retain the error output. |
| Transfer finishes but the ending check fails | Inspect the generated content and the point where the application ends the response. |
| The complete document arrives together | Compare raw and decoded output; do not infer multiple responses from display timing. |
A normal full-page request receives 206 |
Check whether range handling was applied unintentionally; progressive delivery alone does not justify it. |
The completion checks are recommendations derived from the protocol and Fetch behavior, rather than a separate HTTP feature. If the body-reading operation fails, the client above reports failure and exits unsuccessfully instead of printing a success message for the content received so far.
What does this mean for a public page and Google Search?
Keep the useful document content in the HTML response itself. Google explains that crawling and parsing works well for pages whose response HTML contains all their content, and recommends server-side rendering or prerendering because not every bot executes JavaScript. The sample follows that approach: its text arrives directly in the document body. Google Search Central: Understand the JavaScript SEO basics.
Google’s crawler documentation says a 200 response passes received content to the next processing stage; it does not guarantee indexing. The cited documentation supplies no special chunk size or delay that ensures a page will be indexed. Treat streaming as a delivery implementation and verify the finished content separately. Google: How HTTP status codes affect Google's crawlers.
When adapting this demonstration to public hosting, repeat the checks against the actual document endpoint. Confirm the successful status, consume the entire body, and compare its final content with the intended page. Keep the acceptance check independent of how many writes, frames, or visible updates carried the document.
Sources
- RFC 9110: HTTP Semantics — RFC Editor / IETF.
- RFC 9112: HTTP/1.1 — RFC Editor / IETF.
- RFC 9113: HTTP/2 — RFC Editor / IETF.
- RFC 9114: HTTP/3 — RFC Editor / IETF.
- Fetch Standard — WHATWG.
- HTTP — Node.js.
- Global objects — Node.js.
- Download Node.js — Node.js.
- curl — How To Use — curl.
- Understand the JavaScript SEO basics — Google Search Central.
- How HTTP status codes affect Google's crawlers — Google for Developers.