Interface FrameInterceptor

Functional Interface:
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.

@FunctionalInterface public interface FrameInterceptor
Inspects, edits, drops and adds HTTP/2 and HTTP/3 frames on live connections, and HTTP/3 frames in an Http3FramePipeline. Configure one per proxy with HttpProxyServerBootstrap.withFrameInterceptor(FrameInterceptor). It is not an HttpFilters hook: SETTINGS, PING and GOAWAY belong to a connection, not to an exchange.

 bootstrap.withFrameInterceptor((frame, direction, ctx) -> switch (frame) {
     case Http2Frame.Headers h when direction == FrameDirection.TO_CLIENT -> h.withHeader("x-seen-by", "proxy");
     case Http2Frame.Priority p -> null;                 // drop
     default -> frame;                                   // pass through
 });
 

What it sees

The proxy terminates HTTP/2 on each side, so it sees each hop's frames: those it receives from the client (FrameDirection.FROM_CLIENT) after decoding them and before acting on them, those it sends to the client (FrameDirection.TO_CLIENT) before encoding them, and the same for every connection to a server (FrameDirection.FROM_SERVER, FrameDirection.TO_SERVER). That covers HTTP/2 clients on intercepted TLS, h2c and the TLS listener, HTTP/2 connections to servers with all their multiplexed streams, full-duplex gRPC, and CONNECT tunnels and WebSockets over HTTP/2, whose bytes are DATA frames; and HTTP/3 where the proxy speaks it: clients of a reverse proxy's HTTP/3 listener and connections to HTTP/3 servers (see Http3FramePipeline for HTTP/3's differences). Header blocks arrive decoded, CONTINUATION frames folded in and padding removed; frames of extension types, which the proxy otherwise discards unread, are shown as Http2Frame.Unknown. DATA the proxy sends is shown in pieces of at most 16 KiB, before flow control. PUSH_PROMISE is never shown: the proxy disables push, and one is a connection error.

The interceptor is called from many threads at once (each connection's reading thread, each stream's thread) and must be thread-safe. It may block, but a blocked call stalls what waits on it: a connection's reading of frames, or a stream's writing. The HEADERS that open a stream to a server are intercepted while the connection's writing is held, so that stream ids reach the server in order.

What it may change

The return value replaces the frame: frame itself to pass it on, an edited copy, or null to drop it. FrameContext.send(org.microproxy.frames.HttpFrame) adds frames after it. Whatever the interceptor does, the proxy stays protocol-correct, so it accepts only these results:

Allowed results per frame type
FrameMay be editedMay be droppedMay be followed by
DATAthe payload, to any sizeyes DATA; HEADERS (trailers), as the last frame, when the frame ends the stream; extension frames
HEADERSthe fieldsnoHEADERS; extension frames
PRIORITYeverything but the streamyesextension frames
RST_STREAMthe error codenoextension frames
SETTINGSreceived: values, only to be stricter (below); sent: only extension settingsnoextension frames
GOAWAYthe error code and debug datanoextension frames
extension frameseverythingyesextension frames
SETTINGS ACK, PING, WINDOW_UPDATEnothingnonothing
  • Streams. Stream ids are the proxy's: every frame returned or added is on the intercepted frame's stream, except extension frames, which may also be on stream 0.
  • END_STREAM is the proxy's too: what the interceptor sets is ignored. When the intercepted frame ended its stream, the last DATA or HEADERS frame of the result does; when a DATA frame that ended its stream is dropped, an empty DATA frame with END_STREAM takes its place. Nothing may follow it on the stream.
  • Header fields must pass Field.validate(java.util.List<org.microproxy.frames.Field>). Whether they make a valid request or response is checked as usual: a malformed request or response the proxy receives resets its stream, and a peer resets a stream the proxy sends it a malformed message on. When an edit changes the length of a body whose HEADERS declared content-length, remove or fix that field in the HEADERS too (the interceptor sees them first): the proxy checks the body it receives against it.
  • SETTINGS received may be edited only to make the proxy stricter towards the peer: a smaller header table, initial window, frame size (not below 16384), concurrent stream limit or header list size, and not turning on ENABLE_PUSH or ENABLE_CONNECT_PROTOCOL. The proxy then uses the edited values. Its own SETTINGS keep the values it relies on.
  • Sizes. Extension frame payloads and GOAWAY debug data are at most 16376 bytes.
  • HPACK is never at risk: header blocks are decoded once as they arrive and encoded once as they are sent, in order.
  • Flow control. Received DATA is counted against the proxy's windows, and credited back, at its size on the wire, whatever the interceptor makes of it. DATA the proxy sends waits for window at its size after interception.
  • Rate limits count frames as they arrive, before interception.
  • Added frames coming in are processed as if the peer had sent them; extension frames coming in are ignored by the proxy after the interceptor has seen them. Frames are never relayed from one side to the other: add them on the side they should reach.

A result that breaks these rules is rejected as a whole: it is logged as a warning, and the intercepted frame goes on unchanged, without the added frames. So does a frame whose interceptor throws. Without an interceptor, frames cost nothing extra.

HTTP/3 follows the same rules, live and in an Http3FramePipeline, which describes the differences.