---
title: 'HTTP Requests'
source: 'https://academia.sh/en/courses/asynchronous-javascript/http-requests'
course: 'Asynchronous JavaScript and the Runtime'
language: en
updated: '2026-08-17T18:09:44+00:00'
license: 'CC BY-SA 4.0'
---

# HTTP Requests

The structure of request and response objects, why a status code is not treated as an error, the body being readable only once, and processing a streaming body chunk by chunk with asynchronous iteration.

Up to this point, the measurement source was always mimicked with a timer in the same
process. A real source lives outside the process: the request travels over the network,
the response comes back in pieces, and there is an uncontrollable delay in between.

This lesson moves the source outside the process. The request–response structure
covered at the protocol level in the How the Internet Works course is taken up here in
its JavaScript form; the measurement stream also becomes readable from an HTTP service.

The server used in the examples is started at a loopback address inside the same
program. Port zero is given; the host environment picks a free port, and the chosen
value is variable, so it is never printed anywhere.

## Request and Response Are Objects

A request and a response are represented by two objects independent of the details
sitting below the network layer. Both can also be created without a network, which
makes examining their structure easier.

```js
const request = new Request("http://example.invalid/measurement/B2", {
  method: "POST",
  headers: { "content-type": "application/json", "x-client": "measurement-collector" },
  body: JSON.stringify({ station: "B2", value: 19.8 }),
});

console.log("method:", request.method);
console.log("path:", new URL(request.url).pathname);
console.log("header:", request.headers.get("x-client"));
console.log("body:", await request.json());

const response = new Response(JSON.stringify({ state: "recorded" }), {
  status: 201,
  headers: { "content-type": "application/json" },
});

console.log("response status:", response.status, response.ok);
console.log("response body:", await response.json());
```

```
method: POST
path: /measurement/B2
header: measurement-collector
body: { station: 'B2', value: 19.8 }
response status: 201 true
response body: { state: 'recorded' }
```

Three things stand out. Headers are held not in a plain object but in a separate
structure offering name–value access. The methods that read the body (`json`, `text`)
return a promise, because the body may not have fully arrived yet. The status code and
the `ok` field travel together: `ok` states whether the status code falls in the
success range.

## A Network Error and a Failed Status Code Are Separate

The function that sends a request rejects only when it **cannot deliver** the
request: a connection could not be established, a name could not be resolved, the
connection dropped. If the server returned a response — whatever the status code — the
returned promise fulfills.

```js
import { createServer } from "node:http";

const SOURCE = {
  A1: { delay: 30, value: 21.4 },
  B2: { delay: 10, value: 19.8 },
  C3: { delay: 20, value: 23.1 },
};

const server = createServer((request, response) => {
  const path = new URL(request.url, "http://local").pathname;
  const station = path.replace("/measurement/", "");
  const record = SOURCE[station];

  if (record === undefined) {
    response.writeHead(404, { "content-type": "application/json" });
    response.end(JSON.stringify({ error: "unknown station", station }));
    return;
  }

  setTimeout(() => {
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify({ station, value: record.value }));
  }, record.delay);
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const response = await fetch(`${base}/measurement/B2`);
console.log("status:", response.status, "ok:", response.ok);
console.log("content type:", response.headers.get("content-type"));
console.log("body:", await response.json());

const notFound = await fetch(`${base}/measurement/Z9`);
console.log("status:", notFound.status, "ok:", notFound.ok);
console.log("body:", await notFound.json());

server.close();
```

```
status: 200 ok: true
content type: application/json
body: { station: 'B2', value: 19.8 }
status: 404 ok: false
body: { error: 'unknown station', station: 'Z9' }
```

The unknown-station request did not **throw** an error; it returned a valid response
with a 404 status. This is where a common mistake comes from: a `try`/`catch` block
alone does not catch failed requests.

The correct pattern is to explicitly check the status code and turn a failed response
into an error. Once the check is pulled into its own function, it does not need to be
repeated at every call site:

```js
function check(response) {
  if (!response.ok) throw new Error(`request failed: ${response.status}`);
  return response;
}

const success = new Response("data", { status: 200 });
const failure = new Response("not found", { status: 404 });

console.log("successful response passed:", check(success).status);

try {
  check(failure);
} catch (error) {
  console.log("check caught:", error.message);
}
```

```
successful response passed: 200
check caught: request failed: 404
```

The distinction is meaningful: a network error means "it is not known whether the
request reached the server"; a failed status code means "it reached, the server
evaluated it, and rejected it." The two call for different recovery strategies — this
distinction becomes decisive in the retry lesson.

## The Body Is Read Once

A response body is a stream; it is exhausted once read. A second read attempt produces
an error.

```js
const response = new Response(JSON.stringify({ station: "B2", value: 19.8 }), {
  headers: { "content-type": "application/json" },
});

console.log("body used:", response.bodyUsed);
console.log("first read:", await response.json());
console.log("body used:", response.bodyUsed);

try {
  await response.json();
} catch (error) {
  console.log("second read error:", error.constructor.name);
}
```

```
body used: false
first read: { station: 'B2', value: 19.8 }
body used: true
second read error: TypeError
```

This limit is a direct consequence of the body not being held in memory. If the same
body needs to be looked at twice, there are two paths: storing the result in a
variable, or duplicating the response with `clone`. Duplicating means two readers each
consume the same data separately, and it keeps a copy in memory; this is why storing
the result is the default choice.

## Processing the Body as a Stream

Waiting for the entire body costs both delay and memory on large responses. The body is
an asynchronous iterator: the previous lesson's `for await` loop applies directly.

```js
import { createServer } from "node:http";

const server = createServer((request, response) => {
  response.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
  const lines = ["A1;21.4", "B2;19.8", "C3;23.1"];
  let index = 0;

  function nextLine() {
    if (index === lines.length) {
      response.end();
      return;
    }
    response.write(`${lines[index]}\n`);
    index += 1;
    setTimeout(nextLine, 20);
  }
  nextLine();
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const base = `http://127.0.0.1:${server.address().port}`;

const response = await fetch(`${base}/stream`);
const decoder = new TextDecoder();
let chunkCount = 0;

for await (const chunk of response.body) {
  chunkCount += 1;
  console.log("chunk", chunkCount, "—", decoder.decode(chunk, { stream: true }).trim());
}
console.log("total chunks:", chunkCount);

server.close();
```

```
chunk 1 — A1;21.4
chunk 2 — B2;19.8
chunk 3 — C3;23.1
total chunks: 3
```

The server wrote the three lines twenty milliseconds apart; the client processed each
line the moment it arrived, without waiting for the last one. The measurement stream
thereby became a stream in the real sense: processing can begin before the data is
complete.

Two details matter for portability. First, chunk boundaries are **not the same as data
boundaries**; the network can split or merge lines. In this example, chunks lined up
with lines because a delay was placed between each write, but in the general case they
do not line up. The correct implementation is a parser that appends incoming chunks to
a buffer and searches for line endings.

Second, the `stream` option given to the decoder correctly handles the case where a
multi-byte character is split across two chunks. This is the direct practical
counterpart of the character encoding topic from the How Computers Work course: a byte
boundary and a character boundary are not the same thing.

## The Remaining Gap

An assumption was hidden in this lesson's programs: the server responds sooner or
later. If it does not, `await` waits forever; neither a result nor an error ever comes.
This gap was pointed out in the promise-concept lesson and is still open.

In the same way, there is no way to give up on the result of a request that has been
started: as seen in the combinators lesson, not awaiting something is not the same as
canceling it. The request continues, the response arrives, memory is held.

## Summary

- Request and response are objects; headers are held in a separate structure offering
  name–value access, and body-reading methods return a promise.
- The function that sends a request rejects only on a delivery failure; a failed status
  code is a valid response and should be checked through the `ok` field.
- The body is a stream and is read once; a second read throws an error.
- The body is an asynchronous iterator and can be processed chunk by chunk with
  `for await`.
- Chunk boundaries do not line up with data boundaries; parsing has to be done with a
  buffer.

## Next Step

The next lesson closes the two gaps left open together: putting a time limit on a
request, and actually stopping a request that has already started. The abort signal
concept will be introduced, the timeout will be built on top of that signal, and it
will be shown why a race-based solution that only cuts the waiting falls short.
