Module: Nodejs
Nodejs·124·5 MIN READ

124: Native Node HTTP/HTTPS, Fetch, URLs, Headers, Keep-Alive, and Consuming APIs

TOPICS COVERED: Native Node HTTP/HTTPS, Fetch, URLs, Headers, Keep-Alive, and Consuming APIs

Learning objectives

You will learn to:

  • understand the HTTP request/response lifecycle in Node;
  • create an HTTP server with node:http;
  • parse URLs and query parameters;
  • work with request/response headers;
  • understand HTTP bodies as streams;
  • set status codes and content types;
  • use fetch to consume HTTP APIs;
  • use AbortSignal/timeouts;
  • understand connection reuse/keep-alive at a practical level;
  • distinguish transport errors from HTTP errors;
  • understand redirects, compression, proxies, and TLS at a high level;
  • build an API client boundary.

Why native HTTP before Express

Express builds on Node HTTP concepts.

If you understand:

text
IncomingMessage
ServerResponse
method
URL
headers
body stream
status
socket

then Express is a convenience layer, not magic.

First server

js
import http from 'node:http';

const server = http.createServer((request, response) => {
  response.statusCode = 200;
  response.setHeader('content-type', 'text/plain; charset=utf-8');
  response.end('Hello\n');
});

server.listen(3000, '127.0.0.1', () => {
  console.log('http://127.0.0.1:3000');
});

The callback runs for each request.

IncomingMessage

request includes:

text
method
url
headers
socket
body stream
js
console.log(request.method);
console.log(request.url);
console.log(request.headers);

Do not trust headers because they came from the network.

URL parsing

request.url is relative path/query.

Build URL with known origin:

js
const url = new URL(
  request.url,
  `http://${request.headers.host ?? 'localhost'}`,
);

For security-sensitive origin/host logic behind proxies, do not blindly trust the Host header. Use deployment-aware configuration.

Read:

js
url.pathname
url.searchParams.get('page')

Routing manually

js
if (request.method === 'GET' && url.pathname === '/health') {
  response.writeHead(200, {
    'content-type': 'application/json; charset=utf-8',
  });

  response.end(JSON.stringify({ ok: true }));
  return;
}

Manual routing becomes cumbersome. That is one reason frameworks exist.

JSON response helper

js
function sendJson(response, status, body) {
  const text = JSON.stringify(body);

  response.writeHead(status, {
    'content-type': 'application/json; charset=utf-8',
    'content-length': Buffer.byteLength(text),
  });

  response.end(text);
}

Content-Length is bytes.

Do not use string length.

Reading request body with a limit

js
async function readJson(request, maxBytes = 1_000_000) {
  const chunks = [];
  let total = 0;

  for await (const chunk of request) {
    total += chunk.length;

    if (total > maxBytes) {
      const error = new Error('Request body too large');
      error.status = 413;
      throw error;
    }

    chunks.push(chunk);
  }

  const text = Buffer.concat(chunks).toString('utf8');

  return text ? JSON.parse(text) : null;
}

This still buffers up to limit, which is acceptable for small JSON.

Large uploads should stream.

Content-Type

Do not parse arbitrary request bodies as JSON solely because endpoint expects JSON.

Check media type:

text
application/json

and reject unsupported types with appropriate status such as 415.

Real content-type parsing includes parameters:

text
application/json; charset=utf-8

Use robust parsing/framework utilities.

Status codes

Common:

text
200 OK
201 Created
202 Accepted
204 No Content
400 Bad Request
401 Unauthorized (actually unauthenticated in common API usage)
403 Forbidden
404 Not Found
409 Conflict
413 Content Too Large
415 Unsupported Media Type
422 Unprocessable Content
429 Too Many Requests
500 Internal Server Error
503 Service Unavailable

Choose status from semantics.

Do not return 200 { success:false } for every failure.

Headers

Node normalizes access, but HTTP header names are case-insensitive.

Examples:

js
request.headers.authorization
request.headers['content-type']

Response:

js
response.setHeader('cache-control', 'no-store');

Never reflect arbitrary user header values into response headers without validation; header injection/splitting protections and semantics matter.

Cookies

Cookie is an HTTP header:

text
Cookie: session=...

Set:

text
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax

Use a mature cookie/session library for production parsing/signing.

Security attributes matter.

HTTPS

node:https supports TLS.

In production, TLS often terminates at:

  • reverse proxy;
  • load balancer;
  • CDN;
  • ingress.

Your Node app may receive plain HTTP from a trusted local proxy.

Understand where TLS terminates and how client scheme/IP is forwarded.

Do not trust X-Forwarded-* from arbitrary internet clients unless proxy trust is configured.

Keep-alive

HTTP connection reuse avoids TCP/TLS setup per request.

Modern Node fetch and HTTP internals manage connection pooling/keep-alive.

Do not create a brand-new custom Agent for every outbound request; that can defeat pooling.

When using third-party HTTP clients, understand their connection-pool lifecycle.

fetch

Modern Node provides global fetch.

js
const response = await fetch('https://api.example.com/tasks');

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const body = await response.json();

Critical:

fetch() rejects on network/transport failures, not on HTTP 404/500.

You must check response.ok or status.

Timeout/cancellation

js
const controller = new AbortController();

const timer = setTimeout(() => {
  controller.abort(new Error('upstream timeout'));
}, 3000);

try {
  const response = await fetch(url, {
    signal: controller.signal,
  });
} finally {
  clearTimeout(timer);
}

Use supported AbortSignal timeout helpers where appropriate.

Timeout must cancel request, not only stop waiting.

Upstream API client

js
export class HttpError extends Error {
  constructor(message, { status, body, cause } = {}) {
    super(message, { cause });

    this.name = 'HttpError';
    this.status = status;
    this.body = body;
  }
}

export async function requestJson(
  url,
  {
    signal,
    headers,
    ...options
  } = {},
) {
  let response;

  try {
    response = await fetch(url, {
      ...options,
      signal,
      headers: {
        accept: 'application/json',
        ...headers,
      },
    });
  } catch (cause) {
    throw new HttpError('Upstream request failed', {
      cause,
    });
  }

  const contentType = response.headers.get('content-type') ?? '';

  const body = contentType.includes('application/json')
    ? await response.json()
    : await response.text();

  if (!response.ok) {
    throw new HttpError('Upstream returned an error', {
      status: response.status,
      body,
    });
  }

  return body;
}

Now components/services do not repeat transport parsing.

Redirects

Fetch follows redirects by default according to its API.

For security-sensitive server-side fetch of user-provided URLs, redirects can become SSRF paths.

Example:

text
user URL initially public
→ redirect to 169.254.169.254 / internal service

Do not build arbitrary URL fetch proxies without SSRF protections.

SSRF

Server-Side Request Forgery occurs when attacker controls server outbound target.

Protection may include:

  • allowlist destinations;
  • parse URL;
  • enforce scheme;
  • resolve/validate IPs;
  • block private/link-local/loopback ranges;
  • revalidate redirects;
  • network egress policy.

A regex like:

js
url.startsWith('https://')

is not sufficient.

API retries

Retry safe/idempotent operations carefully.

Potential retry:

text
GET

Often safe.

POST /payments can duplicate side effects unless server provides idempotency semantics.

Use exponential backoff/jitter and retry budgets to avoid retry storms.

HTTP caching

Response headers can express:

text
Cache-Control
ETag
Last-Modified
Vary

Do not implement cache policy randomly.

Private/authenticated responses need careful cache directives.

Conditional requests can reduce bandwidth.

Compression

Servers can compress:

text
gzip
br

but reverse proxies/CDNs often handle this more efficiently.

Compression has CPU cost and security considerations.

HTTP streaming

Request/response are streams.

Streaming JSON arrays is not ordinary JSON until complete.

For streaming structured data, use formats/protocols such as:

  • NDJSON;
  • SSE;
  • chunked text;
  • WebSocket.

Document client behavior.

Server-Sent Events

SSE uses long-lived HTTP response:

text
Content-Type: text/event-stream

Good for one-way server→client updates.

Requires heartbeat/reconnect/proxy timeout considerations.

WebSocket is bidirectional and is a different protocol upgrade.

Node roadmap focuses HTTP fundamentals; realtime can be an advanced extension.

Client disconnect

Request/response can close early.

Long work should support cancellation if client no longer needs result, especially expensive upstream/database operations.

Do not assume every accepted request remains connected.

Graceful server close

Native server can stop accepting new connections.

Production shutdown must account for keep-alive/in-flight sockets and deadlines. Covered later.

Common mistakes

  • no request body limit;
  • using string length for content length;
  • fetch 500 treated as success;
  • timeout without abort;
  • trust proxy headers blindly;
  • outbound URL SSRF;
  • retry unsafe writes;
  • buffer huge request/response;
  • return raw internal error;
  • custom HTTP client creates pools repeatedly.

Exercises

  1. Build native /health and /tasks GET routes.
  2. Add JSON response helper.
  3. Parse JSON with 64 KB limit.
  4. Reject wrong Content-Type.
  5. Consume an external API with fetch and explicit HTTP error handling.
  6. Add abort timeout.
  7. Simulate redirect/SSRF threat and design allowlist.
  8. Add ETag concept to one read endpoint.
  9. Stream a large file response.

Mastery checklist

Explain:

  • native HTTP server;
  • request/response streams;
  • URL parsing;
  • body limits;
  • headers/status;
  • fetch network versus HTTP errors;
  • cancellation;
  • connection reuse;
  • TLS/proxies;
  • SSRF;
  • retries/idempotency;
  • HTTP caching.

Official references