DEV Community

Cover image for HTTP/1.1 Deep Dive: Headers, Methods & Status Codes for API Builders
ali ehab algmass
ali ehab algmass

Posted on

HTTP/1.1 Deep Dive: Headers, Methods & Status Codes for API Builders

You've built APIs. You've debugged mysterious 400s at 2am. You know HTTP "well enough" — until you don't. This article is the reference I wish existed when I was stepping up from "it works" to "I understand why it works."

We're going deep on headers, methods, status codes, and the internals of HTTP/1.1 — all grounded in real API scenarios.

Tip: Use the table of contents on the right to jump to any section. This is a long one — bookmark it and come back.

Headers — Deep Dive

Content Negotiation

The Accept family lets clients declare preferences. The server picks the highest-quality match or returns 406 Not Acceptable.

GET /api/users/42 HTTP/1.1
Host: api.example.com
Accept: application/json, text/html;q=0.9, */*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

Enter fullscreen mode Exit fullscreen mode

The q parameter is a quality factor from 0–1. Accept-Encoding is the one you should always enable on your server — it's free bandwidth savings via transparent compression.

Caching Headers

Two mechanisms: expiration-based (avoid the request entirely) and validation-based (check if the cached copy is still good).

Header Direction Meaning
Cache-Control: max-age=3600 Response Fresh for 3600 seconds
Cache-Control: no-cache Both Must revalidate — not "don't cache"
Cache-Control: no-store Response Never cache — use for sensitive data
Cache-Control: private Response Only client may cache, not CDN
ETag: "abc123" Response Resource fingerprint for validation
If-None-Match: "abc123" Request Sends ETag back; get 304 if unchanged

For APIs, the sweet spot is often Cache-Control: no-cache + ETag. The client always validates, but if nothing changed, you skip the body entirely — just a cheap 304.

CORS Headers

CORS is enforced by browsers, not servers — but the server controls the policy. Get this wrong and your frontend devs will hate you.

Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

Enter fullscreen mode Exit fullscreen mode

When Allow-Credentials: true, you cannot use Allow-Origin: * — it must be a specific origin. Browsers will block it.

Preflight: For any "non-simple" request (custom headers like Authorization, or non-GET/POST methods), browsers first send an OPTIONS request. Your API must respond correctly or the real request never happens.

Security Headers Worth Adding Today

Header What it does
Strict-Transport-Security Forces HTTPS for max-age seconds after first visit
X-Content-Type-Options: nosniff Prevents MIME-sniffing attacks
X-Frame-Options: DENY Blocks clickjacking
Content-Security-Policy Fine-grained resource load control

Debugging Headers You Should Add to Every API

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000  # trace across your entire stack
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1740823200
Retry-After: 30                            # return with 429 or 503
Sunset: Sat, 01 Jan 2027 00:00:00 GMT       # RFC 8594 deprecation

Enter fullscreen mode Exit fullscreen mode

Methods — Semantics & Edge Cases

POST vs PUT vs PATCH — Finally Clear

This is the source of most REST API design arguments. Here's the authoritative breakdown:

Method URL Idempotent? Full replace? Use when
POST Server-determined Creating a new resource
PUT Client-specified Yes Full replacement of a known resource
PATCH Client-specified Depends No Partial update (common in practice)

PUT gotcha: The body must represent the complete resource. Omitting a field means that field is nulled out. If you only want to update a name, use PATCH — not PUT.

Idempotency Keys for POST

POST is not idempotent — calling it twice creates two records. For critical operations (payments, job submissions), add application-layer idempotency:

POST /api/payments HTTP/1.1
Authorization: Bearer sk_live_...
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

# Server stores the key + result.
# Duplicate key within 24h = return cached response.
# No double charges. Safe to retry on network failure.

Enter fullscreen mode Exit fullscreen mode

Status Codes — Edge Cases & Usage

Quick Reference

  • 200 OK — standard success
  • 201 Created — add Location header
  • 202 Accepted — async processing
  • 204 No Content — DELETE / PUT
  • 301 Moved Permanently
  • 304 Not Modified — cache hit
  • 400 Bad Request — explain why
  • 401 Unauthorized — who are you?
  • 403 Forbidden — not allowed
  • 404 Not Found
  • 409 Conflict — duplicate / ETag
  • 422 Unprocessable — validation
  • 429 Too Many Requests
  • 500 Internal Server Error
  • 502 Bad Gateway — proxy issue

The 401 vs 403 Confusion

401 Unauthorized means "I don't know who you are — send credentials." 403 Forbidden means "I know exactly who you are, and you can't do this."

Returning 403 to an unauthenticated user leaks the fact that the resource exists. For truly private resources, return 404 to unauthenticated users — deny the resource's existence entirely.

202 Accepted — The Underused Gem

# Client submits a long-running report job
POST /api/reports HTTP/1.1

# Server immediately responds:
HTTP/1.1 202 Accepted
Location: /api/jobs/abc-123
Content-Type: application/json

{ "jobId": "abc-123", "status": "queued", "pollUrl": "/api/jobs/abc-123" }

# Client polls:
GET /api/jobs/abc-123
HTTP/1.1 200 OK
{ "status": "completed", "resultUrl": "/api/reports/xyz-789" }

Enter fullscreen mode Exit fullscreen mode

HTTP/1.1 Internals

Persistent Connections & Connection Pools

HTTP/1.1 keeps TCP connections alive by default. The performance implication: always use a connection pool in your service-to-service calls.

# ❌ New TCP connection every request
import requests
response = requests.get("https://api.example.com/users")

# ✅ Reuses connections — much faster under load
with requests.Session() as session:
    response = session.get("https://api.example.com/users")

Enter fullscreen mode Exit fullscreen mode

Chunked Transfer Encoding

When the server doesn't know the body size upfront, it uses Transfer-Encoding: chunked.

HTTP/1.1 200 OK
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
0\r\n           ← zero-length chunk signals end
\r\n

Enter fullscreen mode Exit fullscreen mode

Head-of-Line Blocking

HTTP/1.1 pipelining sends multiple requests before getting responses — but responses must come back in order. This is head-of-line blocking. Browsers work around this by opening 6 parallel TCP connections per origin.


Debugging Checklist

Symptom First thing to check
Mysterious 404 Host header — wrong virtual host routing
Auth failing randomly Authorization stripped by proxy?
CORS errors Access-Control-Allow-Headers — missing custom headers?
Slow uploads Expect: 100-continue — proxy buffering issues?
502s under load Connection pool exhaustion

Add X-Request-ID to every request/response from day one. When something breaks in production at 3am, having a single ID to grep across all your logs is worth its weight in gold.


Top comments (0)