Networking in C · intermediate · ~20 min

HTTP/1.1 protocol structure

Read and parse HTTP/1.1 request and response frames.

Overview

HTTP/1.1 messages are made of ASCII text lines. Each line ends with \r\n (carriage return + line feed). An empty line marks the end of the headers, and the body follows after it.

Parsing the request line and the headers correctly is the foundation of every HTTP server, proxy, and security gateway.

Why it matters

Almost every web server you will read source for contains a hand-written HTTP parser.

Bugs in those parsers have caused serious failures in production systems. Well-known examples include the Apache Range flaw, the nginx alias flaw, and h2c request smuggling.

Core concepts

Request line

Format: METHOD SP TARGET SP HTTP/1.x CRLF (where SP is a single space).

The method is one of GET, HEAD, POST, PUT, DELETE, OPTIONS, or PATCH. Per the RFC, methods must be strictly uppercase.

Headers

Each header is Name: Value CRLF, and headers repeat line after line.

  • Header names are case-insensitive.
  • Header values can contain almost any characters.

Body

The body is framed in one of two ways:

  • Content-Length gives the body size in bytes, or
  • Transfer-Encoding: chunked splits the body into chunks.

Never accept both at once. If both arrive together, you have a request-smuggling vector. Refuse the request.

Pentester mindset

Request smuggling exploits parser divergence between two servers. For example, the front-end honours Content-Length while the back-end honours Transfer-Encoding (or the reverse). The two servers then disagree on where one request ends and the next begins.

A defensive server should refuse any ambiguous frame.

Defensive coding habits

  • Allow-list the methods you accept.
  • Cap the maximum header length.
  • Require CRLF, not a bare LF.
  • When a frame looks wrong, return 400 and close the connection.

Syntax notes

Split lines on \r\n exactly. An empty line (just \r\n on its own) ends the header block.

Lesson

HTTP/1.1 is ASCII text carried over TCP.

A request has three parts:

  1. A line with the method, path, and version.
  2. Header lines.
  3. An empty line, then an optional body.

Responses have the same shape, except the first line carries a status code instead of a method.

Code examples

GET /path HTTP/1.1\r\n
Host: example.com\r\n
\r\n

Line by line

/* Parse the request line: METHOD SP TARGET SP HTTP/1.x */
const char *sp1 = strchr(line, ' ');
const char *sp2 = strchr(sp1 + 1, ' ');
if (!sp1 || !sp2 || strncmp(sp2+1, "HTTP/", 5)) return -1;
/* method = [line, sp1)   path = (sp1, sp2)   version = sp2+1 .. CR */

Common mistakes

  • Treating a bare \n as a line terminator. The terminator must be \r\n.
  • Trusting both Content-Length and Transfer-Encoding at the same time. This enables request smuggling.

Debugging tips

  • curl -v prints the full request and response, including every header line.
  • nc -l 8080 listens on a port so you can see exactly what a client sends.

Memory safety

Cap the length of every header line.

Most real attacks on HTTP parsers begin with a pathological header of 64 KB or more, designed to overflow a buffer or exhaust memory.

Real-world uses

Every web server, load balancer, WAF (web application firewall), and CDN edge node.

Practice tasks

  1. Parse a request line into its method, target, and version.
  2. Build an HTTP GET request as a string.
  3. Refuse a request that carries both Content-Length and Transfer-Encoding.

Summary

  • HTTP/1.1 is ASCII lines terminated by CRLF; an empty line ends the header block.
  • The body uses Content-Length or chunked Transfer-Encoding, never both.
  • Defend by capping lengths, allow-listing methods, and refusing ambiguous framing.

Practice with these exercises