The Anatomy of an HTTP Request

The Anatomy of an HTTP Request

When your browser wants a web page, it sends an HTTP Request. But what's actually inside that request? It's not just "Give me the page!" — it's a carefully structured message that follows a precise set of rules defined in the HTTP specification.


1. The Four Parts of Every HTTP Request

┌─────────────────────────────────────────────────────────┐
│  REQUEST LINE                                           │
│  POST /api/users HTTP/1.1                               │
├─────────────────────────────────────────────────────────┤
│  HEADERS                                                │
│  Host: api.nandhoo.com                                 │
│  Content-Type: application/json                         │
│  Authorization: Bearer eyJhbGci...                      │
│  Accept: application/json                               │
│  User-Agent: Mozilla/5.0 (Macintosh; Intel...)          │
├─────────────────────────────────────────────────────────┤
│  BLANK LINE  (separates headers from body)              │
│                                                         │
├─────────────────────────────────────────────────────────┤
│  BODY  (optional — only for POST, PUT, PATCH)           │
│  {"username": "Nandhoo", "email": "hi@nandhoo.com"}   │
└─────────────────────────────────────────────────────────┘

2. The Request Line

The first line of every HTTP request contains three things:

METHOD  PATH  HTTP-VERSION
  │       │       │
  GET   /api/users  HTTP/1.1

Method

The verb that describes the action you want to perform. Common methods:

MethodPurposeHas Body?Idempotent?
GETRetrieve a resourceNoYes
POSTCreate a new resourceYesNo
PUTReplace an existing resource entirelyYesYes
PATCHUpdate part of a resourceYesNo
DELETERemove a resourceOptionalYes
HEADSame as GET but returns headers onlyNoYes
OPTIONSAsk which methods a server supportsNoYes

Idempotent means calling the method multiple times has the same effect as calling it once. DELETE /users/123 twice still results in the user being deleted — same end state.

Path

The specific resource you want to access on the server.

/api/users/42/posts?page=2&limit=10
│              │    │
│              │    └── Query string (extra parameters)
│              └──────── Resource identifier
└─────────────────────── Base path

HTTP Version

VersionYearKey feature
HTTP/1.01996New connection for every request
HTTP/1.11997Persistent connections (keep-alive), chunked transfer
HTTP/22015Multiplexing, header compression, server push
HTTP/32022Built on QUIC/UDP, even faster

3. The URL and its Components

A full URL has several distinct parts:

https://api.nandhoo.com:443/v1/users/42?tab=badges&sort=asc#section-1
  │           │           │   │           │                    │
  │           │           │   │           │                    └── Fragment (not sent to server)
  │           │           │   │           └────────────────────── Query string
  │           │           │   └────────────────────────────────── Path
  │           │           └────────────────────────────────────── Port (optional if default)
  │           └────────────────────────────────────────────────── Host (domain)
  └────────────────────────────────────────────────────────────── Scheme / Protocol

Query Strings

Query strings pass extra parameters to the server without changing the path:

/api/users?sort=desc&page=3&limit=20&search=alice

Key-value pairs:
  sort  = desc
  page  = 3
  limit = 20
  search= alice

In JavaScript, use URLSearchParams to safely build query strings:

const params = new URLSearchParams({
  sort: 'desc',
  page: '3',
  limit: '20',
  search: 'alice'
});

const url = `https://api.nandhoo.com/users?${params}`;
// → https://api.nandhoo.com/users?sort=desc&page=3&limit=20&search=alice

fetch(url).then(r => r.json()).then(console.log);

URL Encoding

Special characters in URLs must be percent-encoded:

CharacterEncoded
Space%20 or +
&%26
=%3D
#%23
/%2F
@%40

encodeURIComponent() handles this for you:

const search = 'coffee & cake';
const safe   = encodeURIComponent(search); // 'coffee%20%26%20cake'

4. Request Headers — The Metadata Envelope

Headers give the server extra context. Each is a Key: Value pair on its own line.

Common Request Headers

HeaderExamplePurpose
Hostapi.nandhoo.comWhich server to talk to (required in HTTP/1.1+)
User-AgentMozilla/5.0 (Mac)Browser and OS info
Acceptapplication/jsonWhat response format the client wants
Accept-Languageen-GB,en;q=0.9Preferred language(s)
Accept-Encodinggzip, deflate, brCompression algorithms the client supports
Content-Typeapplication/jsonFormat of the request body
Content-Length47Size of the request body in bytes
AuthorizationBearer eyJhbGci...Authentication token or credentials
CookiesessionId=abc123Cookies to send with the request
Refererhttps://google.comURL of the previous page
Originhttps://nandhoo.comWhere the request originated (CORS)
Cache-Controlno-cacheCaching directives
Connectionkeep-aliveKeep the TCP connection open for reuse

The Accept Header and Content Negotiation

The Accept header lets the client tell the server what format it can handle. The server picks the best match.

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Meaning:
  text/html             → quality 1.0 (most preferred)
  application/xhtml+xml → quality 1.0
  application/xml       → quality 0.9
  */*                   → quality 0.8 (accept anything as last resort)

5. The Request Body

The body carries the actual data for POST, PUT, and PATCH requests. GET and DELETE requests rarely have a body.

Body Format 1: JSON (Most Common for APIs)

fetch('https://api.nandhoo.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    username: 'Nandhoo',
    email:    'hi@nandhoo.com',
    level:    'beginner'
  })
});

Raw request on the wire:

POST /users HTTP/1.1
Host: api.nandhoo.com
Content-Type: application/json
Content-Length: 62

{"username":"Nandhoo","email":"hi@nandhoo.com","level":"beginner"}

Body Format 2: Form Data (HTML Form Submissions)

<form method="POST" action="/login">
  <input name="email"    type="email">
  <input name="password" type="password">
  <button type="submit">Log in</button>
</form>

The browser sends:

POST /login HTTP/1.1
Host: www.nandhoo.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 41

email=hi%40nandhoo.com&password=secret123

Body Format 3: Multipart Form Data (File Uploads)

<form method="POST" action="/upload" enctype="multipart/form-data">
  <input name="avatar" type="file">
  <button type="submit">Upload</button>
</form>
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123

------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

[binary JPEG data here...]
------WebKitFormBoundaryABC123--

Body Format 4: Plain Text

fetch('/api/notes', {
  method: 'POST',
  headers: { 'Content-Type': 'text/plain' },
  body: 'This is my plain text note.'
});

6. A Complete Real HTTP Request

Here is an actual raw request as it travels across the network:

GET /api/v1/courses?level=beginner HTTP/1.1
Host: api.nandhoo.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
Accept: application/json
Accept-Language: en-GB,en;q=0.9
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.X5k
Connection: keep-alive

Notice there's no body — this is a GET request.


7. CORS — Cross-Origin Requests

When your JavaScript fetches a URL on a different origin (different domain, protocol, or port), the browser adds an Origin header and the server must respond with permission.

// Your page is on: https://nandhoo.com
// This request goes to a DIFFERENT origin:
fetch('https://api.otherdomain.com/data');
// Browser adds: Origin: https://nandhoo.com

The server must include Access-Control-Allow-Origin: https://nandhoo.com (or *) in its response, otherwise the browser blocks the response entirely.

Preflight Requests

For non-simple requests (e.g., PUT with a custom header), the browser sends a preflight request first:

OPTIONS /api/resource HTTP/1.1
Host: api.otherdomain.com
Origin: https://nandhoo.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type

If the server responds with appropriate Access-Control-Allow-* headers, the actual request proceeds.


8. Building Requests with fetch — Full Reference

const response = await fetch('https://api.nandhoo.com/courses', {
  method: 'POST',                          // HTTP method

  headers: {                               // Request headers
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token,
    'Accept': 'application/json',
    'X-App-Version': '1.2.0'              // Custom headers start with X-
  },

  body: JSON.stringify({                   // Request body (auto-serialised)
    title:    'Intro to HTTP',
    level:    'beginner',
    chapters: 10
  }),

  credentials: 'include',                  // Send cookies cross-origin
  mode:        'cors',                     // CORS mode
  cache:       'no-cache',                 // Don't use cached response
  signal:      AbortSignal.timeout(5000)   // Cancel after 5 seconds
});

9. Practical Patterns

Pattern 1 — GET with Query Parameters

async function searchCourses(query, level = 'all', page = 1) {
  const params = new URLSearchParams({ q: query, level, page });
  const res = await fetch(`/api/courses?${params}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

const results = await searchCourses('javascript', 'beginner', 2);

Pattern 2 — POST with Error Handling

async function createUser(userData) {
  const res = await fetch('/api/users', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(userData)
  });

  if (res.status === 409) throw new Error('Username already taken');
  if (res.status === 422) {
    const errors = await res.json();
    throw new Error('Validation failed: ' + JSON.stringify(errors));
  }
  if (!res.ok) throw new Error(`Unexpected error: ${res.status}`);

  return res.json(); // 201 Created with new user object
}

Pattern 3 — File Upload with Progress

async function uploadAvatar(file) {
  const formData = new FormData();
  formData.append('avatar', file);
  formData.append('description', 'Profile photo');

  // Note: DO NOT set Content-Type header manually —
  // the browser adds the correct multipart boundary automatically
  const res = await fetch('/api/upload/avatar', {
    method: 'POST',
    body:   formData
  });

  return res.json();
}

10. Common Mistakes

MistakeWhat goes wrongFix
Forgetting Content-Type: application/json on a POSTServer receives garbled data or ignores the bodyAlways set Content-Type when sending JSON
Setting Content-Type: multipart/form-data manuallyBrowser can't add the required boundary parameterNever set Content-Type for FormData uploads — let the browser do it
Putting sensitive data in the URL query stringURLs appear in server logs, browser history, and the Referer headerUse the request body or Authorization header for secrets
Not handling non-2xx responses.json() on an error response crashes or shows confusing dataAlways check response.ok or response.status before parsing
Building query strings with string concatenationSpecial characters break the URLUse URLSearchParams

11. Review Questions

  1. What are the four parts of an HTTP request?
  2. What does Content-Type: application/json tell the server?
  3. What does an Authorization: Bearer header contain?
  4. When should you use a request body, and for which HTTP methods?
  5. Why should you never put a password in a query string?
  6. What is URL encoding and why is it needed?
  7. What happens in a CORS preflight OPTIONS request?