hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses HTTP from scratch

What is HTTP

  • The request-response model
  • Anatomy of an HTTP request
  • Anatomy of an HTTP response

Methods

  • GET and HEAD
  • POST
  • PUT, PATCH, and DELETE
  • OPTIONS and CORS preflight

Status codes

  • 2xx success
  • 3xx redirection
  • 4xx client errors
  • 5xx server errors

Headers

  • Request headers
  • Response headers
  • Custom headers

The body

  • JSON
  • Form data and multipart
  • No body

Connections

  • TCP, DNS, and TLS
  • HTTP/1.1 vs HTTP/2
  • Cookies and state

Putting it all together

  • Building a server from scratch
  • From scratch to framework

Custom headers

Making your own headers

The last two lessons covered standard headers that the HTTP spec defines. But sometimes you need to send metadata that no standard header covers. Maybe you want to tell the client how many API requests they have left. Maybe you want to attach a unique request ID for debugging. Maybe you want to include timing information. HTTP lets you add your own custom headers for exactly these situations.

The X- prefix and why it is deprecated

You will see a lot of custom headers that start with X-:

X-Request-Id: req_a1b2c3
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42

The X- prefix was originally meant to mark headers as “non-standard.” The idea was that if everyone prefixed their custom headers with X-, there would be no confusion with official HTTP headers.

The problem? Some X- headers became so popular that they became de facto standards. X-Forwarded-For is a good example. Now there is an awkward situation: do you standardize it as Forwarded-For (breaking all existing software) or keep the X- prefix (making the naming convention meaningless)?

RFC 6648 (published in 2012) deprecated the X- convention. New custom headers should just use a descriptive name without the prefix. But you will still see X- headers everywhere, and that is fine. Existing ones are not going away.

Rate limit headers

One of the most common uses for custom headers is rate limiting. These headers tell the client how they are doing against the API’s rate limits:

X-RateLimit-Limit: 100         -> Maximum requests allowed in this window
X-RateLimit-Remaining: 42      -> Requests remaining before you hit the limit
X-RateLimit-Reset: 1705312800  -> When the window resets (Unix timestamp)

These are incredibly useful for clients. Instead of blindly sending requests until they get a 429, they can check their remaining quota and back off proactively.

[!NOTE] The Securing Your API course adds these headers to rate-limited endpoints. The REST API Design course includes them in API response conventions.

Request ID

A request ID is a unique identifier generated by the server for each request:

X-Request-Id: req_a1b2c3d4

Why is this useful? Imagine a user reports a bug: “My request failed.” Without a request ID, your team has to search through millions of log entries to find the right one. With a request ID, the user says “my request req_a1b2c3d4 failed,” and your team can find it instantly. The server includes this ID in all log entries for that request, and the client receives it in the response header.

Timing headers

You can also tell the client how long things took:

Server-Timing: db;dur=12.5, cache;dur=0.2
X-Response-Time: 15ms

Server-Timing is actually a standard header that breaks down where time was spent. X-Response-Time is a custom header showing total processing time. Both are useful for performance monitoring.

Setting custom headers on the server

route.get("/books", {
  resolve: (c) => {
    const requestId = crypto.randomUUID();
    const start = Date.now();

    const books = db.prepare("SELECT ...").all();

    return new Response(JSON.stringify(books), {
      headers: {
        "Content-Type": "application/json",
        "X-Request-Id": requestId,
        "X-Response-Time": `${Date.now() - start}ms`,
      },
    });
  },
});

crypto.randomUUID() generates a unique ID for this request. Date.now() before and after the database query gives us the processing time. Both values are included as response headers.

Reading custom headers on the client

const response = await fetch("https://api.example.com/books");

const remaining = response.headers.get("x-ratelimit-remaining");
const requestId = response.headers.get("x-request-id");

if (parseInt(remaining) < 10) {
  console.warn("Approaching rate limit");
}

One thing to watch out for: this code works when the client and server are on the same origin. But what about cross-origin requests?

[!WARNING] Custom headers in cross-origin responses are not visible to JavaScript by default. The server must explicitly list them in Access-Control-Expose-Headers:

Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining

Without this, response.headers.get("x-request-id") returns null, even though the header was sent. The browser hides it. This catches a lot of people off guard.

That wraps up headers. We have covered request headers, response headers, and custom headers. The next section digs into the body of HTTP messages, starting with the format you will use the most: JSON.

Exercises

Exercise 1: Add an X-Request-Id header to every response from your server. Use it to find specific requests in your server logs.

Exercise 2: Add rate limit headers to a response. Read them on the client side and log a warning when the remaining count is low.

Exercise 3: Add Access-Control-Expose-Headers and verify your custom headers are readable from JavaScript in a cross-origin request.

Why was the X- prefix for custom headers deprecated?

← Response headers JSON →

© 2026 hectoday. All rights reserved.