HTTP/2 frames don’t actually contain the HTTP request or response data you’re used to; they’re more like building blocks that the protocol uses to assemble those requests and responses.
Let’s watch a request unfold. Imagine we’re fetching a webpage that includes an image.
# This is a simplified representation of network traffic capture (e.g., using Wireshark)
# Actual packets would be much more detailed.
# Client initiates a connection to the server.
# ... TLS handshake ...
# Client sends HEADERS frame for the initial HTML request:
# STREAM ID: 1
# TYPE: HEADERS (0x01)
# FLAGS: END_HEADERS (0x04)
# PAYLOAD: HPACK-encoded request headers (e.g., :method: GET, :path: /, :authority: example.com, etc.)
# Server acknowledges connection parameters with SETTINGS frame:
# STREAM ID: 0 (connection-level)
# TYPE: SETTINGS (0x04)
# FLAGS: ACK (0x01)
# PAYLOAD: Empty, indicating acknowledgment of client's SETTINGS.
# Server sends HEADERS frame for the HTML response:
# STREAM ID: 1
# TYPE: HEADERS (0x01)
# FLAGS: END_HEADERS (0x04)
# PAYLOAD: HPACK-encoded response headers (e.g., :status: 200, content-type: text/html, etc.)
# Server sends DATA frame with the HTML content:
# STREAM ID: 1
# TYPE: DATA (0x00)
# FLAGS: END_STREAM (0x01)
# PAYLOAD: Actual HTML document bytes.
# While processing the HTML, the client sees a reference to an image (e.g., <img src="/logo.png">).
# The client decides to proactively ask for the image using PUSH_PROMISE.
# Client sends PUSH_PROMISE frame to tell the server it *will* request the image:
# STREAM ID: 1 (associated with the original HTML request)
# TYPE: PUSH_PROMISE (0x05)
# FLAGS: END_HEADERS (0x04)
# PAYLOAD: HPACK-encoded request headers for the image (e.g., :method: GET, :path: /logo.png, :authority: example.com)
# (Note: The PUSH_PROMISE frame itself contains the headers for the *promised* resource, not the original request.)
# Server receives PUSH_PROMISE. It knows it can serve /logo.png.
# Server sends HEADERS frame for the promised image resource:
# STREAM ID: 3 (a new stream ID, chosen by the server)
# TYPE: HEADERS (0x01)
# FLAGS: END_HEADERS (0x04)
# PAYLOAD: HPACK-encoded response headers for the image (e.g., :status: 200, content-type: image/png)
# Server sends DATA frame with the image data:
# STREAM ID: 3
# TYPE: DATA (0x00)
# FLAGS: END_STREAM (0x01)
# PAYLOAD: Actual image bytes.
# Client receives the image data (stream 3) *before* it even explicitly asked for it.
# Client can now render the image.
HTTP/2 breaks down an HTTP message (request or response) into smaller, ordered chunks called frames. Each frame has a type, a stream identifier, flags, and a payload. The stream ID is crucial; it multiplexes multiple requests/responses over a single TCP connection. Frames with the same stream ID belong to the same logical HTTP message.
The SETTINGS frame is how endpoints negotiate parameters for the HTTP/2 connection. This includes things like SETTINGS_MAX_CONCURRENT_STREAMS (how many requests the peer can handle simultaneously) and SETTINGS_INITIAL_WINDOW_SIZE (for flow control). A SETTINGS frame with the ACK flag set is just an acknowledgment that the parameters have been received and processed.
HEADERS frames carry the HTTP headers. Because HTTP/2 uses HPACK compression, these frames are much smaller than plain HTTP/1.1 headers. The END_HEADERS flag indicates that this frame (or the last frame in a sequence for this stream) contains all the headers for that message.
DATA frames carry the actual message payload – the HTML, JSON, image bytes, etc. Multiple DATA frames can be sent for a single message, and the END_STREAM flag on the last DATA frame (or the last HEADERS frame if there’s no body) signals the end of that message.
PUSH_PROMISE is the magic behind server push. When a client requests a resource (e.g., /index.html), the server might know that the HTML will require other resources (like /style.css or /logo.png). Instead of waiting for the client to parse the HTML and then request those resources, the server can promise them. It sends a PUSH_PROMISE frame before sending the actual response headers for the promised resource. This frame contains the request headers for the resource the server intends to push. The client then knows to expect this resource and can even send a RST_STREAM frame if it doesn’t want it. The server then sends the HEADERS and DATA frames for the promised resource on a new stream ID.
The most counterintuitive aspect of PUSH_PROMISE is that it’s initiated by the client’s request, but it’s the server that decides what to push and sends the actual resource data. The client receives the pushed resource on a new stream ID that it didn’t explicitly initiate, and it’s up to the client’s cache and application logic to decide if it actually needs the pushed resource. If it doesn’t, it can send a RST_STREAM frame to cancel the pushed stream.
The next thing you’ll likely encounter is managing flow control with WINDOW_UPDATE frames.