HTTP/1.1 Protocol: A Complete Reference Guide
What Is HTTP/1.1?
HTTP/1.1 (Hypertext Transfer Protocol version 1.1) is the most widely adopted version of the HTTP protocol that powers the World Wide Web. Standardized in RFC 2616 in 1999 and later refined by RFC 7230-7235 in 2014, it defines the rules for how clients (browsers, mobile apps, curl) and servers exchange data over TCP connections. HTTP/1.1 is a text-based, request-response protocol operating at Layer 7 of the OSI model, built on top of a reliable transport layer (typically TCP).
At its core, HTTP/1.1 transmits messages in plain ASCII text following a strict structure: a start line (request or status line), a series of key-value headers separated by CRLF (\r\n), an empty line marking the end of headers, and an optional message body. This human-readable format is one reason HTTP became so successful — you can literally debug it by reading raw bytes off a socket.
Why HTTP/1.1 Matters
Even in 2025, with HTTP/2 and HTTP/3 gaining traction, HTTP/1.1 remains the lingua franca of the internet. Here's why it continues to matter:
- Universal compatibility: Every web server, proxy, load balancer, and CDN speaks HTTP/1.1. It is the lowest common denominator that guarantees interoperability across the entire internet infrastructure.
- Simplicity and debuggability: The plain-text nature makes it trivial to inspect, troubleshoot, and learn. You can telnet to a server and type requests manually.
- Foundation for modern protocols: HTTP/2 and HTTP/3 build on the same semantics (methods, status codes, headers) introduced by HTTP/1.1. Understanding 1.1 means you understand the semantic layer of all HTTP versions.
- RESTful API backbone: REST architecture relies on HTTP verbs, status codes, and headers — all defined by HTTP/1.1. Most APIs still default to HTTP/1.1.
- Proxy and caching infrastructure: The caching model (Cache-Control, ETag, Last-Modified) was perfected in HTTP/1.1 and remains the standard for CDNs and reverse proxies worldwide.
Core Concepts: The Request-Response Cycle
Every HTTP/1.1 interaction follows the same pattern: the client opens a TCP connection, sends a request message, and waits for the server's response. With persistent connections (keep-alive), multiple request-response pairs can flow over a single TCP socket, significantly reducing latency compared to HTTP/1.0's one-request-per-connection model.
HTTP/1.1 Request Structure
An HTTP request consists of four parts:
- Request Line — method, request target, and protocol version
- Headers — zero or more
Name: Valuepairs - Empty Line — a bare CRLF separating headers from body
- Body — optional payload data
Here is the canonical format:
METHOD /path/to/resource HTTP/1.1\r\n
Host: example.com\r\n
Header-Name: Header-Value\r\n
\r\n
[optional body content]
Request Methods
HTTP/1.1 defines eight methods. Each conveys a different semantic intent:
- GET — Retrieve a representation of a resource. Safe and idempotent. Must not change server state.
- POST — Submit data to be processed by the target resource. Typically creates a new subordinate resource. Not idempotent.
- PUT — Replace the target resource entirely with the payload provided. Idempotent.
- DELETE — Remove the target resource. Idempotent.
- HEAD — Identical to GET but the server must not return a body. Useful for checking headers (content-length, cache validity) without downloading.
- OPTIONS — Discover which methods are supported for a given URL. The response includes an
Allowheader. - TRACE — Echoes back the request for diagnostic purposes. Rarely enabled in production due to security concerns.
- CONNECT — Establish a tunnel (used by forward proxies for HTTPS).
Additionally, the PATCH method (RFC 5789) is widely used in modern APIs for partial updates, though it was not in the original HTTP/1.1 specification.
HTTP/1.1 Response Structure
A response mirrors the request structure:
- Status Line — protocol version, status code, and reason phrase
- Headers — server metadata about the response
- Empty Line — CRLF separator
- Body — the actual content
HTTP/1.1 200 OK\r\n
Content-Type: text/html; charset=utf-8\r\n
Content-Length: 42\r\n
Cache-Control: public, max-age=3600\r\n
\r\n
<html><body>Hello, World!</body></html>
Status Code Classes
HTTP/1.1 status codes are three-digit numbers grouped into five classes:
- 1xx Informational: Provisional response before the final status. Rare in HTTP/1.1 except
100 Continuefor large uploads. - 2xx Success: The request was understood and accepted.
200 OK,201 Created,204 No Contentare the workhorses. - 3xx Redirection: Further action needed.
301 Moved Permanently,302 Found,304 Not Modified(critical for caching). - 4xx Client Error: The request is invalid or cannot be fulfilled.
400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found,405 Method Not Allowed. - 5xx Server Error: The server failed to fulfill a valid request.
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable.
Essential HTTP/1.1 Headers
Headers are the metadata backbone of HTTP. Here are the most important ones you'll encounter daily:
General Headers
Date— Timestamp of when the response was generated. Mandatory in responses unless the server has no clock.Cache-Control— Directives for all caching mechanisms along the request/response chain (public,private,no-store,max-age=seconds).Connection— Controls whether the TCP connection stays open after the current transaction.keep-aliveorclose.Transfer-Encoding— Tells the recipient how the body is transmitted.chunkedis the only value defined in HTTP/1.1.Upgrade— Used to switch protocols (e.g., HTTP to WebSocket). Requires a101 Switching Protocolsresponse.
Request Headers
Host— Mandatory in HTTP/1.1. Specifies the domain name the client wants to reach. Enables virtual hosting (multiple websites on one IP).Accept,Accept-Encoding,Accept-Language— Content negotiation: client tells server what formats, compressions (gzip, br), and languages it prefers.Authorization— Credentials for authentication, typicallyBearer tokenorBasic base64creds.If-Modified-Since,If-None-Match— Conditional request headers that make caching efficient. The server responds with304 Not Modifiedif the resource hasn't changed.User-Agent— Identifies the client software. Used for analytics and sometimes for content adaptation (though frowned upon).
Response Headers
Content-Type— MIME type of the body (text/html,application/json,image/png).Content-Length— Size of the body in bytes. Must be accurate; proxies rely on it to delimit messages.ETag— An opaque identifier for a specific version of a resource. Used with conditional requests.Last-Modified— Timestamp of when the resource was last changed.Location— Used in 3xx redirects and201 Createdresponses to tell the client where to go next.Set-Cookie— Instructs the client to store a cookie for subsequent requests.Access-Control-Allow-Origin— CORS header controlling cross-origin access from browsers.
Connection Management and Keep-Alive
One of HTTP/1.1's biggest improvements over 1.0 is persistent connections. In HTTP/1.0, each request required opening a new TCP connection, completing the three-way handshake, and then tearing it down with a four-way close. For a webpage with 10 images, that meant 11 separate TCP connections — each with its own slow-start phase.
HTTP/1.1 defaults to keeping the connection open after a response, allowing multiple requests to be pipelined or sent sequentially over the same socket. This is controlled by the Connection header:
GET /index.html HTTP/1.1
Host: www.example.com
Connection: keep-alive
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 2048
[body data]
# Second request on same connection:
GET /style.css HTTP/1.1
Host: www.example.com
Connection: keep-alive
HTTP Pipelining (sending multiple requests without waiting for responses) was specified in HTTP/1.1 but proved problematic in practice — head-of-line blocking meant a slow response would stall all subsequent responses. Most browsers disabled it. HTTP/2 solved this with multiplexing.
To gracefully close a connection, a client or server sends a Connection: close header on the final message. The peer then knows to close the socket after consuming that message.
Chunked Transfer Encoding
When the server doesn't know the complete size of the response body at the time it starts sending headers (dynamic content, streaming, compression on-the-fly), it cannot send a Content-Length header. Instead, it uses chunked transfer encoding. The body is sent as a series of chunks, each prefixed with its size in hexadecimal, followed by CRLF. The end of the body is signaled by a zero-length chunk.
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/plain
7\r\n
Hello, \r\n
6\r\n
World!\r\n
0\r\n
\r\n
Each chunk is self-contained: size in hex, then CRLF, then the chunk data, then CRLF. The final 0\r\n\r\n tells the client the body is complete. This allows the server to flush data progressively — the client can start processing the first chunk before the entire response is generated.
Caching and Conditional Requests
HTTP/1.1's caching model is one of the most sophisticated parts of the protocol. It saves bandwidth, reduces latency, and offloads server processing. The model involves:
- Freshness: Determined by
Cache-Control: max-age=3600(cache for one hour) orExpiresheader (absolute timestamp, less preferred). A response is "fresh" if its age is less than max-age. - Validation: When a cached response becomes stale, the cache can ask the origin server if it's still valid using conditional headers —
If-None-Matchwith an ETag orIf-Modified-Sincewith a timestamp. If the resource hasn't changed, the server returns304 Not Modifiedwith no body, saving bandwidth. - Heuristic caching: If no explicit freshness headers exist, caches may compute an estimated age using
DateandLast-Modifiedheaders. This is discouraged but widely implemented.
Here is a complete caching negotiation example:
# First request
GET /api/users/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
ETag: "abc123"
Last-Modified: Tue, 15 Jan 2025 08:00:00 GMT
Date: Tue, 15 Jan 2025 09:00:00 GMT
{"id":42,"name":"Alice"}
# 65 seconds later (cache is now stale)
GET /api/users/42 HTTP/1.1
Host: api.example.com
If-None-Match: "abc123"
If-Modified-Since: Tue, 15 Jan 2025 08:00:00 GMT
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
ETag: "abc123"
Date: Tue, 15 Jan 2025 09:01:05 GMT
[no body — client uses its cached copy]
Content Negotiation
HTTP/1.1 allows clients and servers to negotiate the best representation of a resource. The client sends Accept* headers describing its preferences, and the server examines them to select (or dynamically generate) the appropriate variant. The response includes a Vary header telling caches which request headers were used for selection.
GET /document HTTP/1.1
Host: www.example.com
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
Accept-Encoding: gzip, deflate, br;q=0.9
Accept-Language: en-US, en;q=0.9, fr;q=0.7
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Language: en-US
Vary: Accept-Encoding, Accept-Language, Accept
[gzip-compressed HTML body]
The q parameter (quality value, 0 to 1) indicates relative preference. The Vary header is crucial: it prevents a cache from serving a gzipped English HTML page to a client that requested uncompressed French JSON.
The Host Header and Virtual Hosting
The Host header is the single most important addition in HTTP/1.1. It is mandatory — a request without it must be rejected with 400 Bad Request. Before HTTP/1.1, one IP address could only serve one website because the server had no way to know which domain the client intended. With the Host header, a single server (or reverse proxy like nginx) can host thousands of websites on one IP, dispatching requests based on the requested domain name.
GET / HTTP/1.1
Host: my-blog.example.com
GET / HTTP/1.1
Host: shop.example.com
# Both arrive at the same IP, but the server routes them differently
# based on the Host header.
Practical Code Examples
Example 1: Raw HTTP Client in Python Using Sockets
This example demonstrates the protocol at the lowest level — opening a TCP socket, crafting a valid HTTP/1.1 request by hand, and parsing the response. No HTTP library is used; you see the exact bytes sent and received.
import socket
def raw_http_get(host, port, path):
"""Send a minimal HTTP/1.1 GET request and print the raw response."""
# Establish TCP connection
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
# Craft the HTTP/1.1 request manually
# Note: Each line ends with \r\n, and headers end with a blank \r\n line
request = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
"User-Agent: RawSocketClient/1.0\r\n"
"Accept: text/html,application/json;q=0.9,*/*;q=0.8\r\n"
"Accept-Encoding: gzip\r\n"
"Connection: close\r\n"
"\r\n"
)
# Send the request
sock.sendall(request.encode('utf-8'))
# Receive the response in chunks
response_data = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response_data += chunk
sock.close()
# Split headers from body at the double-CRLF boundary
raw_text = response_data.decode('utf-8', errors='replace')
header_section, _, body = raw_text.partition("\r\n\r\n")
print("=== STATUS LINE + HEADERS ===")
print(header_section)
print("\n=== BODY (first 500 chars) ===")
print(body[:500] if len(body) > 500 else body)
return response_data
# Usage
raw_http_get("example.com", 80, "/")
Example 2: Minimal HTTP/1.1 Server in Node.js
This server parses raw HTTP/1.1 requests from a TCP socket, demonstrates header parsing, chunked response encoding, and proper status code handling. It supports both GET and POST, validates the Host header, and handles conditional requests with ETags.
const net = require('net');
const SERVER_PORT = 8080;
// In-memory resource store with ETags for caching demonstration
const resources = {
'/hello': {
data: JSON.stringify({ message: 'Hello, HTTP/1.1 world!' }),
contentType: 'application/json',
etag: '"v1-hello-abc"',
lastModified: 'Wed, 21 Oct 2025 07:28:00 GMT',
},
'/time': {
data: null, // dynamic — always fresh
contentType: 'text/plain',
etag: null, // no caching for dynamic endpoints
lastModified: null,
},
};
function parseRequest(rawData) {
const text = rawData.toString('utf-8');
const headerEnd = text.indexOf('\r\n\r\n');
if (headerEnd === -1) return null; // incomplete request
const headerBlock = text.substring(0, headerEnd);
const bodyRaw = text.substring(headerEnd + 4);
const lines = headerBlock.split('\r\n');
const requestLine = lines[0];
const [method, path, protocol] = requestLine.split(' ');
const headers = {};
for (let i = 1; i < lines.length; i++) {
const colonIndex = lines[i].indexOf(':');
if (colonIndex > 0) {
const key = lines[i].substring(0, colonIndex).trim().toLowerCase();
const value = lines[i].substring(colonIndex + 1).trim();
headers[key] = value;
}
}
// Parse Content-Length body if present
let body = '';
const contentLength = parseInt(headers['content-length'], 10);
if (contentLength && contentLength > 0) {
body = bodyRaw.substring(0, contentLength);
}
return { method, path, protocol, headers, body };
}
function buildResponse(statusCode, statusText, headers, body) {
let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`;
// Always include Date header (RFC 7231 requirement for origin servers)
response += `Date: ${new Date().toUTCString()}\r\n`;
for (const [key, value] of Object.entries(headers)) {
response += `${key}: ${value}\r\n`;
}
response += '\r\n';
if (body) {
response += body;
}
return response;
}
const server = net.createServer((socket) => {
let rawData = '';
socket.on('data', (chunk) => {
rawData += chunk.toString('utf-8');
const parsed = parseRequest(rawData);
if (!parsed) return; // wait for more data
const { method, path, headers, body } = parsed;
// Validate mandatory Host header (RFC 7230 Section 5.4)
if (!headers['host']) {
const resp = buildResponse(400, 'Bad Request', {
'Content-Type': 'text/plain',
'Connection': 'close',
}, 'Missing Host header — required by HTTP/1.1');
socket.write(resp);
socket.end();
return;
}
console.log(`${method} ${path} from Host: ${headers['host']}`);
// Route the request
if (method === 'GET' && path === '/time') {
// Dynamic endpoint — no caching, use chunked encoding
const now = new Date().toISOString();
const responseHeaders = {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked',
'Cache-Control': 'no-store',
};
let chunkedResponse = `HTTP/1.1 200 OK\r\n`;
chunkedResponse += `Date: ${new Date().toUTCString()}\r\n`;
for (const [k, v] of Object.entries(responseHeaders)) {
chunkedResponse += `${k}: ${v}\r\n`;
}
chunkedResponse += '\r\n';
// Chunked body: send hex length, then data, then CRLF
const bodyText = `Server time: ${now}\n`;
const hexLength = bodyText.length.toString(16);
chunkedResponse += `${hexLength}\r\n${bodyText}\r\n`;
chunkedResponse += `0\r\n\r\n`; // terminating chunk
socket.write(chunkedResponse);
socket.end();
} else if (method === 'GET' && resources[path]) {
const resource = resources[path];
const responseHeaders = {
'Content-Type': resource.contentType,
'Cache-Control': 'public, max-age=30',
'Connection': 'keep-alive',
};
// Handle conditional request with ETag
if (resource.etag && headers['if-none-match'] === resource.etag) {
const resp = buildResponse(304, 'Not Modified', {
'ETag': resource.etag,
'Cache-Control': 'public, max-age=30',
}, '');
socket.write(resp);
return;
}
if (resource.etag) responseHeaders['ETag'] = resource.etag;
if (resource.lastModified) responseHeaders['Last-Modified'] = resource.lastModified;
responseHeaders['Content-Length'] = resource.data.length.toString();
const resp = buildResponse(200, 'OK', responseHeaders, resource.data);
socket.write(resp);
} else if (method === 'POST' && path === '/echo') {
// Echo back the posted body with CORS header for browser access
const responseHeaders = {
'Content-Type': 'application/json',
'Content-Length': body.length.toString(),
'Access-Control-Allow-Origin': '*',
'Connection': 'keep-alive',
};
const resp = buildResponse(201, 'Created', responseHeaders, body);
socket.write(resp);
} else if (method === 'OPTIONS') {
// CORS preflight or method discovery
const responseHeaders = {
'Allow': 'GET, POST, OPTIONS',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Length': '0',
};
const resp = buildResponse(204, 'No Content', responseHeaders, '');
socket.write(resp);
} else {
const responseHeaders = {
'Content-Type': 'text/plain',
'Content-Length': '9',
'Connection': 'close',
};
const resp = buildResponse(404, 'Not Found', responseHeaders, 'Not Found');
socket.write(resp);
socket.end();
}
rawData = ''; // reset for potential keep-alive reuse
});
socket.on('end', () => {
console.log('Client disconnected');
});
socket.on('error', (err) => {
console.error('Socket error:', err.message);
});
});
server.listen(SERVER_PORT, () => {
console.log(`HTTP/1.1 server listening on port ${SERVER_PORT}`);
console.log('Test with: curl -v http://localhost:8080/hello');
console.log('Or conditional: curl -v -H "If-None-Match: \\"v1-hello-abc\\"" http://localhost:8080/hello');
});
Example 3: Complete cURL Debugging Session
cURL's -v (verbose) flag exposes the raw HTTP/1.1 conversation. This is the single most valuable debugging technique for HTTP APIs.
# Verbose GET with conditional headers
$ curl -v -H "If-None-Match: \"abc123\"" https://api.example.com/users/42
* Connected to api.example.com (1.2.3.4) port 443
> GET /users/42 HTTP/1.1
> Host: api.example.com
> User-Agent: curl/8.5.0
> Accept: */*
> If-None-Match: "abc123"
>
< HTTP/1.1 304 Not Modified
< Date: Tue, 15 Jan 2025 09:01:05 GMT
< ETag: "abc123"
< Cache-Control: public, max-age=60
< Connection: keep-alive
<
* Connection #0 to host api.example.com left intact
# POST with JSON body, showing Content-Type negotiation
$ curl -v -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/plain;q=0.9" \
-d '{"name":"Bob","email":"bob@example.com"}' \
https://api.example.com/users
> POST /users HTTP/1.1
> Host: api.example.com
> Content-Type: application/json
> Accept: application/json, text/plain;q=0.9
> Content-Length: 45
>
< HTTP/1.1 201 Created
< Location: /users/123
< Content-Type: application/json
< Content-Length: 52
<
{"id":123,"name":"Bob","email":"bob@example.com"}
Best Practices for HTTP/1.1 Development
- Always include the Host header — it's mandatory. Clients must send it; servers must reject requests without it. Even when connecting directly to an IP address, specify the intended domain.
- Use Content-Length accurately — If you know the body size before sending headers, include
Content-Length. It allows clients to allocate buffers efficiently and detect truncated responses. If you don't know the size (streaming), useTransfer-Encoding: chunked— never both on the same message. - Set the Date header on responses — RFC 7231 requires origin servers to include a
Dateheader on all responses unless they genuinely have no clock. Caches use it to calculate freshness age. - Implement conditional request handling — Support
If-None-MatchandIf-Modified-Since. Return304 Not Modifiedwhen the resource hasn't changed. This single optimization can cut bandwidth by 50-90% for repeat visitors. - Use appropriate Cache-Control directives — For public static assets:
public, max-age=31536000, immutable. For private API responses:private, no-cache(which means "revalidate before reuse," not "never cache"). For sensitive data:no-store. - Include a Vary header when content negotiation is active — If your response depends on
Accept-Encoding,Accept, or other request headers, list them inVary. Otherwise, caches may serve the wrong variant. - Validate and sanitize all incoming headers — Header injection attacks are real. Never echo raw request header values into response headers without filtering CRLF sequences. A malicious
User-Agentvalue containing\r\ncan split headers and inject arbitrary response headers. - Close connections gracefully — When you want to end a persistent connection, send
Connection: closeon the final message. Don't just close the socket mid-stream — that causes TCP RST and may lose data in flight. - Handle the 100 Continue flow correctly — For large POST/PUT bodies, clients may send an
Expect: 100-continueheader and wait for the server's100 Continueresponse before sending the body. Servers should either respond with100 Continueto accept the body or reject immediately with a 4xx status to save bandwidth. - Limit request header size — HTTP/1.1 has no intrinsic limit on header size, but most servers impose one (typically 8KB to 16KB). Excessively large headers (often from oversized cookies) can cause
431 Request Header Fields Too Largeor silent rejection by intermediaries. - Use keep-alive wisely — While persistent connections reduce latency, holding too many idle connections consumes server resources. Set reasonable
Keep-Alive: timeout=N, max=Mheaders and configure server-side idle connection timeouts (common values: 5-15 seconds timeout, 100 requests max). - Prefer absolute URIs in proxy requests — When sending requests to a forward proxy, use the absolute form:
GET http://example.com/path HTTP/1.1so the proxy knows the destination. For origin servers, the path-only form withHostheader is correct:GET /path HTTP/1.1.
Common Pitfalls and How to Avoid Them
- Double CRLF confusion: The boundary between headers and body is exactly
\r\n\r\n. A single\n\nmay work with tolerant parsers but violates the spec. Always use\r\nconsistently. - Chunked encoding parsing errors: Many naive parsers forget that chunk extensions (semicolon after size) are allowed:
1A;some-extension\r\n. A robust parser must handle this. - Silent header folding: HTTP/1.1 originally allowed multi-line headers with continuation lines (starting with whitespace). RFC 7230 deprecated this. Never generate folded headers, and be cautious parsing them from legacy systems.
- Misinterpreting 304: A
304 Not Modifiedresponse must not include a body (except possibly for Content-Length: 0). Including a body with 304 confuses caches and breaks the conditional request mechanism. - Ignoring the TE header: While
Transfer-Encodingis a response header, the request counterpart isTE(Transfer-Encoding negotiation). Most clients don't send it, but if you seeTE: trailers, the client wants trailing headers after the chunked body. -
🚀 Need a reliable AI agent for your project?
Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.
Get Started — $23.99/mo