Get E-Book
HTTP Servers, Clients & Proxies

HTTP/1.1 Wire Format and Semantics

Ishtmeet Singh @ishtms/June 10, 2026/41 min read
#nodejs#http#http11#networking#protocols

GET /users?id=1 HTTP/1.1 is already HTTP.

Those bytes are no longer random readable data coming from a TCP socket. They follow HTTP rules. The connected TCP stream gives Node ordered bytes. HTTP tells the client and server how to read those bytes as a request or response.

Chapter 9 stopped at readable bytes on a connected socket. This chapter starts at the next layer. HTTP begins when those bytes are read as a message with a start line, headers, an optional body, and request or response meaning.

Here is a small HTTP request after decoding the visible ASCII bytes -

http
GET /users?id=1 HTTP/1.1
Host: api.example.test
Accept: application/json

The blank line is important. On the wire, each displayed line ends with \r\n. That means the carriage-return byte 0x0d, followed by the line-feed byte 0x0a.

The empty line is another \r\n. It marks the end of the header section. If the message has body bytes, they begin after that point.

Most terminal tools hide \r\n when they print HTTP messages. That makes the request easier to read, but it can also hide where the headers actually end. When you are debugging raw HTTP, look for this four-byte sequence -

text
0d 0a 0d 0a

That is the marker between the header section and the bytes that may follow it.

Here is the same request with the CRLF markers shown directly -

text
GET /users?id=1 HTTP/1.1\r\n
Host: api.example.test\r\n
Accept: application/json\r\n
\r\n

Everything before the first \r\n is the request line. Header fields come next, one per line. The empty line ends the metadata. Body bytes follow only when HTTP framing says there is a body.

HTTP gives these bytes shared meaning between a client and a server. HTTP semantics are the meanings assigned to methods, status codes, field names, and message content. HTTP wire format is the byte syntax used to carry that meaning over a connection. This chapter focuses on HTTP/1.1 - text start lines, text header fields, CRLF line endings, and body framing layered on top of a reliable TCP stream.

The start line is the first thing the parser reads. It tells the receiver whether the message is a request or a response. In a request, it contains the method, target, and HTTP version. In a response, it contains the HTTP version, status code, and reason phrase.

The header section comes after that. Headers carry host information, content metadata, body framing, content preferences, and connection options.

After the empty line, the parser has to decide whether body bytes follow, and if they do, how many bytes belong to the current message. That decision has to be correct before the same connection can be used for another HTTP message.

TCP gives Node a stream of ordered bytes. HTTP message shape comes from the HTTP parser.

A single socket 'data' event can contain half a request line, one full request, two complete requests, or the end of one body plus the beginning of the next start line. The socket chunk does not decide the message shape. The HTTP parser does.

That parser behavior controls what Node can safely expose. Once the parser has the request line and header section, Node can create an IncomingMessage. Once the parser knows how the body ends, Node can decide when the current message is complete. Once the current message is complete, the same connection can carry another message. If the parser cannot find where the body ends, the connection cannot safely move to the next message.

Keep the order in your head like this -

text
TCP bytes
  -> start line
  -> header section
  -> empty line
  -> optional body bytes
  -> next message or close

Every later Node HTTP API builds on that order. req.method, req.url, req.headers, res.statusCode, keep-alive behavior, parser errors, proxy forwarding, and streaming bodies all come from those pieces.

The bytes also arrive incrementally. The first socket read might end after GET /use. The next read might finish the request line and include a few headers. Another read might deliver body bytes. HTTP/1.1 parsing is stateful because the parser has to remember where it is between reads.

A parser may move through states like these -

text
reading start line
reading header fields
reading fixed-length body
reading chunks
message complete

Those are parser states. They are not the same as your JavaScript handler phases. Your handler can start running while the request body is still arriving. The request metadata is available first, then the body continues through the request stream.

Partial reads are normal. A socket might deliver one request in pieces like this -

text
GET /use
rs?id=1 HTTP/1.1\r\nHost:
 api.example.test\r\n\r\n

After the first fragment, the request line is still incomplete because the CRLF has not arrived yet. After the second fragment, the parser has the full request line and part of the Host field. After the third fragment, it has the full header section and can complete a request with no body.

The JavaScript chunk shape is not the HTTP message shape.

That is why string-splitting individual socket chunks breaks under real traffic. A socket chunk is just how bytes happened to arrive from the transport. HTTP syntax decides where the request line ends, where headers end, and where the body ends. Node's HTTP parser sits between those two layers and turns byte fragments into protocol events.

HTTP Starts After TCP

A connected socket is only the transport. HTTP gives the bytes a message model.

An HTTP message is one complete protocol unit. In HTTP/1.1, a message has a start line, zero or more header fields, an empty line, and an optional message body. The message is either a request sent by a client or a response sent by a server.

"Optional body" means the body exists only when HTTP semantics and framing say it exists. The blank line only ends the header section. It tells the parser where metadata ends. The body length comes from method rules, status rules, and framing fields such as Content-Length or Transfer-Encoding.

Some responses have zero body bytes by rule. Some responses use the connection close as the body end. Chunked transfer coding uses a final zero-size chunk to mark completion.

Node's net.Socket layer sees bytes. The HTTP layer reads those bytes in order. It reads the start line, reads header fields until the empty line, then applies the body-length rules.

Later subchapters get into Node's HTTP objects and the llhttp parser. For now, keep this mental model simple. Your JavaScript code receives an HTTP request only after lower layers have parsed enough bytes to identify that request.

HTTP/1.1 also adds connection behavior on top of TCP. One TCP connection can carry multiple HTTP request and response exchanges in sequence. The parser can only move to the next exchange after it has completed the current one.

That is why framing fields affect connection reuse. A correct Content-Length lets the receiver count exact body bytes. Chunked transfer coding gives explicit chunk sizes and a final marker. Connection-close framing consumes the rest of the connection as the body.

The protocol syntax is byte-oriented. Displaying the start line and fields as strings is convenient, but a parser works over bytes first. Delimiters, invalid bytes, and body content have to be handled before application string parsing begins.

Header field values can later be decoded and interpreted. Body bytes may be JSON, text, compressed data, image bytes, or an empty sequence. The parser's first job is to understand the HTTP structure.

That structure explains why HTTP parsing sits below ordinary application code. A route handler can decide whether /users?id=1 maps to a route. A body parser can decide whether the body is valid JSON. Before either of those runs, the HTTP parser has to identify the method token, request target, header lines, and body framing.

The wire format also explains why raw net.Socket examples can speak HTTP with plain strings. HTTP/1.1 starts with text-like control bytes. A minimal client can write a valid request by sending the request line, the Host header, and the empty line -

js
socket.write([
  'GET / HTTP/1.1',
  'Host: example.com',
  '',
  '',
].join('\r\n'));

That code is useful for inspection, not for production clients. It shows the header ending clearly. The last two empty strings create the CRLF after the Host line and the CRLF for the empty line.

Raw captures help because they keep the structure visible. When a Node HTTP issue looks strange, write down the received bytes in this order -

text
start line
header field line
header field line
empty line
body bytes, if any

If that layout is unclear, debug parsing or framing first. Routing, middleware, handlers, and JSON parsing come later.

Request Message Shape

An HTTP request starts with a request line -

http
GET /users?id=1 HTTP/1.1
Host: api.example.test

The request line has three pieces - method, request target, and HTTP version. In this request, GET is the method, /users?id=1 is the request target, and HTTP/1.1 is the version.

The spaces are part of the syntax. In the normal request line, one space separates the method from the target, and another separates the target from the version -

text
method SP request-target SP HTTP-version CRLF

SP is the space byte. CRLF is the two-byte line ending. A parser may allow a few historical edge cases, but your working model should stay strict - method, target, version, line ending, then headers.

The HTTP method is a token that declares the protocol action. GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and CONNECT are familiar method tokens. At this layer, the parser first treats the method as syntax. Application meaning comes later.

GET normally asks for a representation. HEAD asks for the response metadata that GET would have produced, with no response body bytes. POST sends request content for the target resource to process. That is enough method meaning for this chapter. Resource modeling, idempotency, and API contracts come later.

The request target identifies what the request is aimed at inside the HTTP interaction. For ordinary origin-server requests, it is usually a path plus an optional query string -

http
GET /users?id=1 HTTP/1.1
Host: api.example.test

The target is /users?id=1. The Host header supplies the authority, which is the host and optional port. Together, the receiver can reconstruct the target URI for routing and validation.

Framework routing comes later. At the wire level, the request line carries the path and query in the common form, while Host carries the authority.

The query string is part of the request target bytes. HTTP does not parse query parameters for you at this stage. Later, Node or your code can feed the target into URL and URLSearchParams. The raw request line still carries one target token.

The HTTP version token says which HTTP/1.x grammar the sender used. HTTP/1.1 uses the case-sensitive name HTTP, a slash, and major/minor digits. The version affects connection defaults and required fields. A Node server still has to parse the raw bytes before it can expose useful values to your handler.

The version token is also a useful debugging clue. If a capture starts with PRI * HTTP/2.0, you are looking at the HTTP/2 connection preface, and Chapter 11 owns that path. If a request starts with GET / HTTP/1.0, connection defaults and required fields change. Bytes that do not fit request-line syntax are rejected by Node's HTTP parser before routing code runs.

After the request line, header fields carry metadata -

http
POST /users HTTP/1.1
Host: api.example.test
Content-Type: application/json
Content-Length: 17

The empty line after these fields ends the header section. The Content-Length: 17 field says that 17 body bytes follow. The body stays separate from the header section, even when the body contains text that looks like a header.

Now add the body -

http
POST /users HTTP/1.1
Host: api.example.test
Content-Type: application/json
Content-Length: 17

{"name":"Ada"}

The JSON text is easy to count wrong by eye. Body length is counted in bytes, not by how the text looks in a code block. This example says 17, so a receiver following the field waits for 17 bytes. If fewer bytes arrive and the sender stops, the message is incomplete. If extra bytes arrive, the receiver treats them as part of the body until the declared count is satisfied.

That is the parser-sensitive part. Application code wants objects and strings. The wire gives bytes and declared lengths. The parser has to reconcile them before the handler sees a request stream.

Use a matching length -

http
POST /users HTTP/1.1
Host: api.example.test
Content-Type: application/json
Content-Length: 14

{"name":"Ada"}

Now the body ends after the closing } byte. If another request starts immediately after it on the same connection, the next byte belongs to the next start line.

HTTP request meaning starts with the method and target. Body handling starts with framing. A GET request with no body framing fields has zero body bytes. A POST request with Content-Length: 0 also has zero body bytes. The method name alone does not give the parser enough information to read a body.

That behavior shows up directly in Node. req.method and req.url are available after the start line is parsed. The request body is a readable stream that may arrive later. The message has one request line and one header section. The body is flow-controlled data that comes after the metadata.

For a request with a body, the parser can expose headers before the body is complete. That lets server code inspect the method, target, and headers before deciding whether to keep reading. If the handler accepts the request, it reads bytes from the request stream. If the handler rejects early, the server still has to decide what to do with any remaining body bytes on the connection.

A request with a body has two visible moments -

text
metadata complete
body stream still flowing

That behavior lets a server reject a large upload after reading only the headers, or accept the request and stream the body through backpressure-aware code. It is still one HTTP message across both moments.

Request Target Forms

HTTP/1.1 has four request target forms. Most backend code sees one of them all day - origin-form.

Origin-form is the path and optional query string sent to an origin server -

http
GET /users?id=1 HTTP/1.1
Host: api.example.test

The request target is /users?id=1. The authority comes from Host. Node HTTP servers commonly expose that target as the URL-like value you later parse with URL or routing code.

Origin-form also uses / when the target URI has an empty path -

http
GET / HTTP/1.1
Host: api.example.test

That root slash is still the request target. Route decisions happen later. The slash is the path component the client placed into the request line.

Absolute-form carries a full URI in the request line -

http
GET http://api.example.test/users?id=1 HTTP/1.1
Host: api.example.test

Forward proxies use this form because the next recipient needs the full destination in the request line. Reverse proxy behavior and forwarding rules come later in this chapter. For now, read the bytes literally. The target includes the scheme, authority, path, and query.

Absolute-form can look strange in server logs because backend handlers usually expect a path. At the HTTP layer, both of these can be valid request targets -

text
/users?id=1
http://api.example.test/users?id=1

The receiver's role decides which one it expects. An origin server usually expects origin-form. A proxy-facing hop can receive absolute-form.

Authority-form is just the authority component -

http
CONNECT api.example.test:443 HTTP/1.1
Host: api.example.test:443

CONNECT uses authority-form because the host and optional port occupy the request-target position. Proxy tunnel behavior belongs to the proxy subchapter. The syntax here is enough - host plus optional port appears where the target normally appears.

Asterisk-form is the single * target -

http
OPTIONS * HTTP/1.1
Host: api.example.test

That form addresses the server as a whole for the OPTIONS method. Ordinary application routes rarely see it because route tables usually focus on paths.

These forms explain a common confusion in raw logs. A Node server behind normal infrastructure might receive origin-form. A proxy-facing component might receive absolute-form. A tunnel request uses authority-form. A server-wide OPTIONS request uses asterisk-form.

The first token still gives the method. The second token still gives the request target. The target form tells the receiver how to interpret that target.

The Host field is part of that interpretation in HTTP/1.1. A server can receive many hostnames on the same local address and port. The TCP connection gives local and remote socket addresses. The Host header gives the requested authority at the HTTP level.

Virtual hosting, routing, and proxy policy use that field later. At the wire-format layer, the role is immediate - HTTP/1.1 requests carry authority metadata in the header section.

In raw Node debugging, the target form can tell you which layer generated the request. A browser or normal HTTP client talking directly to an origin server sends origin-form. A client configured for a forward proxy can send absolute-form to that proxy. A tunnel setup sends authority-form with CONNECT. A server-wide OPTIONS probe sends asterisk-form.

When the target form surprises you, check how the request reached the process before changing application routes.

Target reconstruction uses more than the request line. With origin-form, the request line gives path and query. Host gives authority. The connection context can give the scheme, depending on whether the connection is plain HTTP or wrapped by TLS. The full target URI is reconstructed from protocol bytes plus connection context, then handed to higher layers for routing and policy.

Response Message Shape

An HTTP response starts with a status line -

http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 11

{"ok":true}

The status line has three pieces - HTTP version, status code, and reason phrase. Here, HTTP/1.1 is the version, 200 is the status code, and OK is the reason phrase.

The status code is the machine-readable result of the response. It is always three decimal digits. Clients treat the first digit as the class - 1xx informational, 2xx success, 3xx redirection, 4xx client error, and 5xx server error.

Chapter 12 covers API contract decisions around which status code to pick. At the wire level, the code sits in the status line and controls response behavior, including body rules for a few classes and codes.

Status classes are broad protocol categories -

text
1xx  informational
2xx  successful final response
3xx  redirection final response
4xx  client error final response
5xx  server error final response

The class is still useful when a client sees an unfamiliar exact code. A client that lacks special handling for 418, for example, can still place it in the 4xx class. Application policy sits above this layer.

The reason phrase is the human-readable text after the status code in HTTP/1.1. It can be empty or different from the usual phrase. Clients should use the numeric status code for behavior. The phrase still appears in raw responses and logs, but protocol behavior follows the code.

These two status lines carry the same machine-readable result -

http
HTTP/1.1 200 OK

HTTP/1.1 200 Fine

The code is 200 in both. The phrase changes. A parser can record the phrase, but client behavior should follow the code.

A response can carry header fields and a body -

http
HTTP/1.1 404 Not Found
Content-Type: text/plain
Content-Length: 7

missing

The status line itself does not give the body length. In this response, Content-Length supplies it. Some status codes require zero body bytes regardless of framing. Some responses use chunked transfer coding. Some responses are delimited by closing the connection. The parser combines status semantics and framing fields to decide where the body ends.

Responses also follow connection order. A client sends requests and receives responses. A server receives requests and sends responses. HTTP/1.1 uses order on the connection to associate a response with its request. There is no per-response request ID in the wire format.

If a client sends multiple requests on one connection, final responses are matched in the same order the requests were sent. Any 1xx responses belong to the request currently waiting for its final response.

That ordered association is why malformed framing can break more than one message. If a client reads too few body bytes, leftover bytes can be mistaken for the next response. If it reads too many, it consumes bytes that belong to the next message. Either way, responses stop lining up with requests.

For a Node client or server, the visible objects arrive later. The raw response is still a status line, header section, empty line, and body bytes. Once you can read those four pieces in a capture, Node's higher-level events become easier to place.

Response parsing also explains why headers can arrive before the body. The client can learn the status code and headers first, then stream the body over time. A large download, a chunked response, and an empty response all share the same initial shape. The body framing decision controls what happens after the headers.

Header Fields

A header field is one metadata line inside the header section -

http
Content-Type: application/json

Content-Type is the field name. application/json is the field value. The colon separates them on the wire. Optional whitespace can appear after the colon. Field names are case-insensitive, so content-type and Content-Type refer to the same field. Field values have their own syntax, depending on the field.

Many APIs normalize field names, but the raw wire data can preserve the sender's casing -

http
content-type: application/json
CONTENT-LENGTH: 14
Host: api.example.test

Those names still match their case-insensitive field definitions. Node later exposes normalized header objects and raw header arrays. The parser has to keep enough information for both views.

The header section is the group of header fields between the start line and the empty line -

http
GET /users HTTP/1.1
Host: api.example.test
Accept: application/json
User-Agent: curl/8.0

Three header fields appear in that section. The empty line ends it. After the empty line, a later body line remains body data even if it contains a colon.

Some fields appear once. Some can repeat. Some repeated fields can be combined with commas. Some have special handling. Parser and framework behavior around repeated fields comes later, especially because Node exposes both normalized headers and raw header order. At the wire level, repeated field lines are simply multiple field lines with the same field name.

Repeated fields are visible at the byte level -

http
Accept: application/json
Accept: text/plain
Host: api.example.test

The field definition decides whether those values combine and how. Accept values can be combined into a list. Other fields have rules that make blind joining unsafe. That is why raw header order remains useful during debugging.

Header field names are protocol tokens. They identify metadata slots with registered or application-defined meaning. Header field values are byte sequences constrained by HTTP field-value grammar, then interpreted according to the field definition.

Content-Length: 14 has numeric framing meaning. Content-Type: application/json identifies the media type of the message content. Accept: application/json tells the server what response media types the client can handle. Content negotiation policy belongs to API design, but the Accept field itself is request metadata.

Content-Type describes the content being sent in the current message -

http
Content-Type: application/json

It labels the content. A JSON parser still has to read body bytes and parse them after HTTP framing.

Accept describes response types the client can process -

http
Accept: application/json

The server may use it when choosing a response representation. Chapter 12 covers that negotiation policy. The wire field is a client preference line inside the request header section.

Host needs special attention in HTTP/1.1 -

http
GET /health HTTP/1.1
Host: api.example.test

The request line says /health. The Host field says api.example.test. A server can use both to identify the target URI. With a non-default port, the field can include it -

http
GET /health HTTP/1.1
Host: api.example.test:8080

That value belongs to HTTP, not TCP. The TCP connection might arrive at 10.0.2.15:3000 after a load balancer or local port mapping. The Host field still carries the authority the client placed in the request.

Later proxy and deployment chapters deal with trust around these fields. The wire rule is simpler - HTTP/1.1 requests need host authority metadata.

Header fields also participate in connection control. Connection: close changes what happens after the current response. Other connection options mark fields intended only for the current connection. Forwarding behavior belongs to proxies, but a receiver still has to parse Connection before carrying fields onward.

A practical rule follows from all of this - parse the header section as HTTP before parsing the body as application data. JSON parsing starts after framing. Compression starts after framing. Multipart parsing starts after framing. Until the empty line and body framing are known, the receiver is still doing protocol work.

Header size and syntax errors also belong here. A field line with an invalid name, an oversized header section, or malformed whitespace can fail before any application route runs. Later Node parser chapters cover exact error surfaces. For now, place the failure correctly - bad header bytes are protocol bytes.

Header parsing has several layers -

text
HTTP parser      field line endings
HTTP parser      field names and values
field definition value meaning
application      policy decision

For Content-Length, the field definition gives a body length. For Content-Type, it gives a media type label. For Accept, it gives a client preference list. For Host, it gives target authority. Treating all header values as generic strings loses useful meaning too early.

Body Length and Framing

The message body is the payload byte sequence after the header section, when the message rules say a body exists. It is the part application code usually wants, but HTTP/1.1 makes the receiver prove where that part ends.

The body is a byte sequence. It can be empty. It can contain text. It can contain JSON. It can contain compressed data. It can contain bytes that look exactly like \r\n\r\n. Once framing has placed bytes inside the body, the HTTP parser treats them as body bytes.

There are four common cases -

text
zero body bytes by rule
Content-Length gives exact byte count
Transfer-Encoding: chunked gives chunks
connection close ends the response body

The parser chooses among those cases using the start line, response status, and framing fields.

The decision path is ordered -

text
status and method body rules
Transfer-Encoding
Content-Length
request default: zero bytes
response default: read until close

That order is why conflicting framing has to be handled by the parser. The receiver needs one selected rule for the body end before it can hand body bytes to application code.

Here is the same idea as a small framing table -

text
GET carrying zero body fields  body ends at the empty line
POST with Content-Length: 14   body ends after 14 bytes
response with chunked          body ends at the zero chunk
response with close delimiter  body ends when the connection closes

That table catches a lot of bugs. A body parser that receives fewer bytes than Content-Length has an incomplete message. A response reader that expects a status line while still inside chunk data is out of sync. A server that leaves request body bytes unread has to consume, discard, or close before the next request can be parsed on that connection.

Some messages have zero body bytes. Informational responses have zero body bytes. 204 No Content and 304 Not Modified have zero body bytes. A response to HEAD carries headers as if a body could exist, but the actual response body length is zero. Requests carrying no body framing fields have zero body bytes. In those cases, the receiver moves to the next message after the header section.

A zero-body response can still carry useful headers -

http
HTTP/1.1 204 No Content
Date: Wed, 10 Jun 2026 12:00:00 GMT

The message completes at the empty line. A client waiting for body bytes here is already reading the protocol incorrectly.

Content-Length is the simplest explicit framing field -

http
POST /users HTTP/1.1
Host: api.example.test
Content-Length: 14

{"name":"Ada"}

The field value is a decimal byte count. The receiver reads exactly that many body bytes after the empty line.

If fewer bytes arrive before the connection closes, the message is incomplete. If more bytes arrive, the extra bytes come after the current message body. They might be the next request on the same connection. They might be invalid leftover data. The parser decides based on connection state.

The byte count is over transferred body bytes, not JavaScript string characters. UTF-8 can use more than one byte for one visible character. Binary content can contain any byte value, including bytes that display as line breaks. The body parser for JSON, text, form data, or another media type runs after HTTP has delivered the body stream.

Multi-byte text makes this visible -

js
Buffer.byteLength('{"snow":"\u2603"}');

The source stays ASCII, but JavaScript turns \u2603 into one Unicode code point before Buffer.byteLength() counts UTF-8 bytes. Content-Length uses the byte count.

Transfer-Encoding describes transfer codings applied to the message body for transport. In HTTP/1.1, chunked is the one you will see constantly because it lets the sender stream a body before knowing the total length.

Chunked is transfer framing. It does not describe what the content means. After chunked decoding, the application body might still be JSON, text, or binary data. Content-Type describes that decoded content. Transfer-Encoding describes how the body is carried across this HTTP/1.1 connection.

A chunked response begins like this -

http
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/plain

5
hello
0

The display hides the CRLF bytes around each chunk. The actual chunk body is shaped like this -

text
5\r\n
hello\r\n
0\r\n
\r\n

5 is hexadecimal for five body bytes. The receiver reads five bytes of chunk data, then the following CRLF. 0 is the terminating chunk. After that zero chunk, optional trailer fields can appear, then a final empty line ends the chunked body.

The chunk size is hexadecimal, so a means ten bytes -

text
a\r\n
0123456789\r\n
0\r\n
\r\n

The chunk data length is ten bytes. The chunk-size line and CRLF delimiters stay outside the decoded message body.

Trailer fields are header-field-shaped metadata sent after a chunked body. They exist for metadata only known after streaming the content. In Node terms, trailers are late HTTP metadata, separate from the initial header section. They are rare in application code, but the parser still needs to know where they can appear.

A chunked body with one trailer field looks like this -

text
5\r\n
hello\r\n
0\r\n
Digest: sha-256=...\r\n
\r\n

The trailer field arrives after the terminating chunk size. It is still protocol metadata. The application body remains hello.

Chunked transfer coding gives the connection a clear return point for the next message -

text
chunk size
chunk data
chunk size
chunk data
zero chunk
optional trailer fields
empty line
next response or close

That is why chunked bodies and connection reuse work well together. The sender can stream content without precomputing Content-Length. The receiver can still find the end of the body and return to start-line parsing for the next message.

Conflicting framing fields belong to the parser layer. A message with both Transfer-Encoding: chunked and Content-Length gives two body-length signals. HTTP/1.1 recipients treat transfer coding as the framing authority, and intermediaries have strict obligations around removing conflicting length metadata before forwarding.

Security chapters cover request smuggling. The operational rule for this chapter is narrower - a receiver has to reject, normalize, or otherwise handle ambiguous framing before application code sees a clean request.

Multiple Content-Length field lines create another parser decision. Matching values can be reduced to one value by some recipients. Conflicting values make the message ambiguous -

http
POST /a HTTP/1.1
Host: api.example.test
Content-Length: 5
Content-Length: 7

hello!!

The message claims two lengths. A receiver that chooses five sees hello as the body. A receiver that chooses seven sees hello!!. HTTP stacks treat that as protocol trouble because downstream recipients must agree on where the body ends. Application code should receive one clean interpretation or an error.

A mismatch with Content-Length creates parser pressure immediately -

http
POST /a HTTP/1.1
Host: api.example.test
Content-Length: 5

helloGET /b HTTP/1.1

The first five body bytes are hello. The next byte after those five is G. On a reusable connection, that byte can be interpreted as the beginning of another start line if the remaining bytes form a valid message. If the sender meant the whole visible tail as body data, the length is wrong. Application intent is invisible to the parser. It follows framing.

A too-large length stalls differently -

http
POST /a HTTP/1.1
Host: api.example.test
Content-Length: 10

hello

Five body bytes have arrived. Five more are still owed. The receiver waits until more bytes arrive, a timeout fires, or the connection closes. Your application handler may look idle because the parser is still waiting for the declared body to finish.

Chunked bodies can become incomplete too -

text
5\r\n
hello\r\n

The terminating zero chunk is still missing. The receiver has one complete chunk, but the message body is not complete. The parser waits for the next chunk-size line, a timeout, or connection close.

Connection-close framing handles responses whose length is given by the end of the connection. A server can send headers, body bytes, and then close the connection to mark the body end -

http
HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close

hello

Here, the response lacks Content-Length and chunked coding. The close marks the body end. The connection is consumed by this response. The transport ending is the delimiter, so the response sequence ends there.

Requests use a different default. A request carrying neither Content-Length nor transfer coding has zero body bytes. A server reads the next byte after the empty line as the next request start only when another request arrives on that connection. Close-delimited request bodies would break the request and response sequencing HTTP/1.1 relies on.

Body framing also decides when a message is complete. With valid Content-Length, completion occurs after exactly that count. With chunked, completion occurs after the terminating zero chunk and trailer section. With close-delimited responses, completion occurs when the connection closes after a valid header section. With zero-body messages, completion occurs at the empty line.

This logic sits under Node's HTTP behavior. req can be created after the start line and headers. The request body stream then produces bytes according to framing. message.complete later reflects whether the parser saw a complete HTTP message. Keep-alive reuse waits on full consumption or discard of the body because the parser must reach the next start line cleanly.

A handler that ignores a request body can still affect connection reuse, because unread body bytes remain on the connection before the next request can be parsed. The Node server lifecycle chapter covers the object details. The wire rule is already visible here - the connection advances only when the current message has a known end.

That is also why body limits sit where protocol handling meets application policy. A server may reject a request because the declared length is too large, because the chunk stream exceeds a configured cap, or because body bytes arrive too slowly. Those policies sit above the base wire syntax, but they run while the parser is framing the message. The route handler may receive an early rejection path instead of a full body.

For a streaming upload, body framing and backpressure work together. The HTTP parser identifies body bytes and feeds them into the request stream. The stream machinery from Chapter 3 decides how fast JavaScript consumes those bytes. The parser still owns message completion. Backpressure can slow delivery to your handler, but the HTTP body end still comes from framing.

Connection Semantics

HTTP/1.1 connection behavior sits above TCP connection state.

TCP can keep a socket open. HTTP decides whether another HTTP message can use it. Those are separate decisions. A TCP keep-alive probe belongs to the transport. HTTP keep-alive behavior belongs to message sequencing, framing, and connection metadata.

HTTP/1.1 defaults to carrying more than one request/response exchange on a connection when both sides keep it open and the current message is fully framed. The receiver has to know exactly where one message ends before it can parse the next start line.

Content-Length and chunked transfer coding support reuse. Connection-close framing can complete a response, but it consumes the connection after that response.

A reusable HTTP/1.1 connection follows this loop -

text
read request
send response
message complete
read next request

That loop stops when either side sends Connection: close, when the body uses connection close as its ending, when a parser error occurs, or when local timeout policy tears the socket down. Subchapter 5 covers timers, agents, and pools. The wire-level rule is already fixed - a connection that still contains unread body bytes is still inside the current message.

The Connection header affects the current connection only. Resource state, cache policy, and retry policy live elsewhere. This field tells the immediate recipient which connection options apply to this hop. That narrow scope becomes important once intermediaries enter the picture, because a field meant for one hop can be wrong or dangerous on the next hop.

The Connection header carries control options for the current connection -

http
GET /report HTTP/1.1
Host: api.example.test
Connection: close

close says the sender wants the connection closed after the current response. A server can also send it -

http
HTTP/1.1 200 OK
Content-Length: 5
Connection: close

hello

Here, the body length is five bytes, and the connection will close after the response. The length still counts because it tells the client where the body ends before the close happens. That can affect whether the client reports a complete response or a truncated one.

If the server sends only three body bytes and closes, the client has an incomplete response -

http
HTTP/1.1 200 OK
Content-Length: 5
Connection: close

hel

The close happened, but the declared body length was five. The body delivered three. A correct client reports incomplete transfer instead of treating the close as a successful body end.

Compare that with a close-delimited response -

http
HTTP/1.1 200 OK
Connection: close

hel

Here, the close supplies the body end. The body is three bytes because this response selected connection-close framing. Same TCP close signal, different HTTP framing state.

The Connection field can also name other connection-specific fields. Those fields are intended for the immediate connection hop. A forwarding component has to remove or replace them instead of passing them through blindly. Proxy behavior belongs to Subchapter 7, but the reason starts here - some header fields describe only the current connection, while others describe the end-to-end message.

Connection reuse depends on complete message consumption. Suppose a server receives this request -

http
POST /upload HTTP/1.1
Host: api.example.test
Content-Length: 1000000

If the handler sends an early final response after inspecting only headers, the request body bytes may still be arriving. The server then has a protocol choice. It can continue reading and discard the remaining body so the connection reaches the next request start. Or it can close the connection. Leaving unread body bytes in front of the parser would corrupt the next request.

Clients have the same issue for responses. If a client wants to reuse a connection, it has to consume or discard the whole response body according to framing. If it stops reading halfway through a chunked response, the next bytes on the socket are still chunk data, not a new status line. A pool can safely hand that socket to another request only after the parser reaches message completion.

HTTP/1.1 also associates responses with requests by order. A final response completes the oldest outstanding request on that connection, with any preceding informational responses attached to it. That keeps connection state small, but it also means correct parsing is essential. One bad body end can shift every response after it.

Close handling also carries protocol meaning. A clean close after a complete close-delimited response can complete that response. A close before a declared Content-Length count is reached means the message is incomplete. A reset during body transfer is a transport failure observed while the HTTP parser was waiting for bytes. The exact Node error surface comes later, but the parser state already tells you what kind of failure happened.

That shows up in logs. "Socket closed" is weak by itself. "Socket closed after complete chunked response" means the message completed and then the transport ended. "Socket closed with 30 bytes remaining from Content-Length" means incomplete HTTP. The same TCP event can happen at different HTTP states.

Later, agents and pools will put this rule under load. For now, the useful debugging version is enough - connection reuse is a parser outcome, not merely an open socket.

Informational Responses

An informational response is a 1xx response sent before the final response for the same request. It carries protocol progress rather than the final result.

http
HTTP/1.1 100 Continue

HTTP/1.1 201 Created
Content-Length: 0

The client receives two response messages for one request. The first is informational. The second is final. The final status code completes the request.

100 Continue is the common one. A client can send request headers with Expect: 100-continue when it is about to send a body and wants the server to make an early decision from the request line and headers -

http
PUT /large.bin HTTP/1.1
Host: uploads.example.test
Content-Length: 104857600
Expect: 100-continue

The server can send 100 Continue, which tells the client to send the body. Or the server can send a final error response based on the method, target, and headers. Node exposes this path through checkContinue and client-side events later. At the wire level, the client can observe protocol messages before the final status code.

A full exchange looks like this -

text
client sends request line and headers
server sends HTTP/1.1 100 Continue
client sends request body
server sends final response

The request has one final response. The 100 Continue message is an interim response on the same connection, associated with that request by order.

Informational responses have zero body bytes. They end at the empty line after their header section. After that, the connection remains positioned for the next response message belonging to the same request.

That sequence can surprise client code that assumes one status line per request. HTTP/1.1 permits one or more 1xx responses before a final response. The final response has a non-1xx status code. Response-body framing belongs to the final response, except for special upgrade and tunnel cases covered later.

The parser has to surface these events while preserving order. A 100 Continue is a zero-body response message with informational semantics, separate from the final response.

Other informational responses exist, including protocol switching paths. WebSocket and HTTP/2 upgrade behavior belong to later chapters. The local rule stays the same - 1xx responses can appear before the final response, and each 1xx message completes at its own empty line.

Reading Raw Messages Safely

A raw request is easier to reason about when you keep the end of the header section visible.

js
const raw = Buffer.from([
  'GET /users?id=1 HTTP/1.1',
  'Host: api.example.test',
  'Accept: application/json',
  '',
  '',
].join('\r\n'));

That buffer contains a request line, two header fields, the empty line, and zero body bytes. The final empty string creates the extra CRLF that ends the header section.

Inspect the actual bytes when something feels off -

js
console.log(raw.toString('latin1'));
console.log(raw.length);

latin1 keeps one JavaScript character per byte for this kind of inspection. It is a debugging display choice, not an instruction to parse HTTP as Latin-1 strings.

Hex output exposes the CRLF delimiters directly -

js
for (const byte of raw) {
  process.stdout.write(byte.toString(16).padStart(2, '0') + ' ');
}

Look for 0d 0a at each line ending and 0d 0a 0d 0a at the end of the header section. If the capture display hides line endings, hex usually settles the question fast. You can see whether the request line ended, whether the header section ended, and where body bytes begin.

For a body-bearing request, use the same byte discipline -

js
const body = Buffer.from('{"ok":true}');
console.log(body.length);

That length is the value a matching Content-Length field needs. When the source string contains escapes or non-ASCII code points, Buffer.byteLength() or an actual Buffer is a better source of truth than a visual character count.

For inspection, mark the parts before interpreting values -

text
request line  GET /users?id=1 HTTP/1.1
header        Host: api.example.test
header        Accept: application/json
empty line    end of headers
body          zero bytes

Framework routing comes later. JSON parsing comes later. The HTTP message is already complete because the request carries no body framing fields.

A raw response has the same outer shape with a different start line -

http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 11

{"ok":true}

Mark it the same way -

text
status line   HTTP/1.1 200 OK
header        Content-Type: application/json
header        Content-Length: 11
empty line    end of headers
body          11 bytes

Count the body bytes rather than the displayed field lines. The Content-Length value starts counting after the empty line. Header bytes stay outside it.

Chunked inspection needs one extra pass -

http
HTTP/1.1 200 OK
Transfer-Encoding: chunked

6
hello!
0

The header section ends at the first empty line. The body framing then switches to chunk parsing. 6 says six body bytes follow. hello! supplies those six bytes. 0 ends the chunked body. The final empty line ends the trailer section, which is empty here.

When framing disagrees with visible text, trust the framing rules before trusting the display. A body can contain \r\n\r\n as data when Content-Length says those bytes belong to the body. A chunk can contain bytes that look like header fields. A field value can describe JSON while the body is still incomplete. The parser decides structure from HTTP syntax and framing. Application parsers run after that.

Here is a body that contains a header-looking line -

http
POST /note HTTP/1.1
Host: api.example.test
Content-Length: 13

X-Test: hello

X-Test: hello is body data here. The header section ended at the empty line. The colon has application meaning only if the body parser gives it meaning.

Body bytes can also look like a new HTTP message -

http
POST /note HTTP/1.1
Host: api.example.test
Content-Length: 14

HTTP/1.1 500!!

The body starts with characters that resemble a status line. The current parser state says fixed-length request body, so those bytes stay inside the body until 14 bytes have been consumed. After that point, the parser returns to start-line mode.

A raw log that starts in the middle of a body can therefore look like protocol syntax even when the connection is still inside application content.

That detail is useful when you copy small fragments from packet captures or debug logs. A fragment beginning with GET or HTTP/1.1 proves very little by itself. The previous parser state decides whether those bytes are protocol control bytes or body bytes. Find the start line, end of headers, and selected framing rule before assigning meaning to a fragment.

A compact debug workflow works well -

text
1. Find the first CRLF - the start line ends there.
2. Find CRLFCRLF - the header section ends there.
3. Parse framing fields and status/method rules.
4. Count body bytes or chunks.
5. Only then parse body content.

That workflow is for reading captures and tests. Production HTTP parsing belongs to Node's parser, because valid HTTP includes edge cases around whitespace, repeated fields, oversized fields, invalid bytes, partial reads, and connection state.

The main skill is knowing where the failure belongs. A malformed request line fails before routing. A missing Host field fails before application resource handling. A bad Content-Length fails while framing the body. A JSON parse error happens after HTTP delivered body bytes. A connection that closes before the declared body length completes is an incomplete HTTP message, even if the partial body looked meaningful.

This small placement table helps during incident notes -

text
bad start line       parser rejects request syntax
bad header field     parser rejects header section
bad framing          parser lacks complete body
bad JSON             body parser rejects content
handler error        application code rejects request

Those failures happen at different layers. They produce different logs, different status codes, and different retry behavior later.

That is the handoff to the rest of this chapter. Subchapter 2 turns these messages into http.Server, IncomingMessage, and ServerResponse. Subchapter 3 opens the parser path. The bytes stay the same. Node just starts naming the pieces for JavaScript.