Netty Pipeline & Protocol Handling

June 26, 2026 · View on GitHub

Server Bootstrap

The server is started from Main.java (CLI) or ClientAndServer (embedded). Both create a MockServer instance which extends LifeCycle.

sequenceDiagram
    participant CLI as Main.java / ClientAndServer
    participant MS as MockServer
    participant LC as LifeCycle
    participant NT as NettyTransport
    participant NIO as Netty NIO / Epoll

    CLI->>MS: new MockServer(ports)
    MS->>LC: super(configuration)
    LC->>NT: newEventLoopGroup(useNativeTransport)
    NT-->>LC: EpollEventLoopGroup (Linux) or NioEventLoopGroup (other)
    MS->>MS: createServerBootstrap()
    MS->>NT: serverSocketChannelClass(useNativeTransport)
    NT-->>MS: EpollServerSocketChannel or NioServerSocketChannel
    MS->>NIO: ServerBootstrap.group(bossGroup, workerGroup)
    MS->>NIO: .channel(selected channel class)
    MS->>NIO: .childHandler(MockServerUnificationInitializer)
    MS->>LC: bindServerPorts(ports)
    LC->>NIO: serverBootstrap.bind(port) [per port, on dedicated thread]
    NIO-->>LC: ChannelFuture completes
    LC->>MS: startedServer(boundPorts)

Transport Selection

NettyTransport (mockserver-core, org.mockserver.socket) selects the highest-performance transport available at startup. On Linux with the native epoll library present and useNativeTransport=true (the default), it creates EpollEventLoopGroup and uses EpollServerSocketChannel / EpollSocketChannel. On all other platforms (macOS, Windows) or when the opt-out flag is set, it falls back to NIO transparently.

The transport selection is consistent across the entire data path: server bootstrap (boss + worker groups), outbound HTTP client (NettyHttpClient), and relay connect handler (RelayConnectHandler). This ensures the EventLoopGroup type always matches the channel type (epoll group with epoll channels, NIO group with NIO channels).

Epoll transport is required for transparent-proxy SO_ORIGINAL_DST resolution, which needs EpollSocketChannel children to extract the raw file descriptor.

Intentionally left on NIO: Http3Server (QUIC/datagram, separate experimental transport with its own NioEventLoopGroup), McpToolRegistry's internal client, and EchoServer (test infrastructure).

PropertyDefaultEnv varSystem property
useNativeTransporttrueMOCKSERVER_USE_NATIVE_TRANSPORT-Dmockserver.useNativeTransport

CI Test Coverage

On Linux CI the full existing integration-test suite exercises the epoll transport automatically because:

  1. useNativeTransport defaults to true
  2. The netty-transport-native-epoll JARs (linux-x86_64, linux-aarch_64) are declared as runtime-scoped dependencies in mockserver-netty/pom.xml, which Maven includes on the test classpath
  3. On Linux the native .so loads successfully, so Epoll.isAvailable() returns true

To force NIO on Linux for comparison testing, set useNativeTransport=false via system property (-Dmockserver.useNativeTransport=false) or environment variable (MOCKSERVER_USE_NATIVE_TRANSPORT=false).

Dedicated activation tests in EpollTransportIntegrationTest (mockserver-netty) verify the channel and event-loop-group types at runtime. These tests are gated by Assume.assumeTrue(Epoll.isAvailable()) and skip cleanly on macOS/Windows.

Key Bootstrap Configuration

SettingValuePurpose
Boss groupEpollEventLoopGroup(5) or NioEventLoopGroup(5)Accept connections
Worker groupEpollEventLoopGroup(configurable) or NioEventLoopGroup(configurable)Handle I/O
ChannelEpollServerSocketChannel or NioServerSocketChannelServer socket (transport-matched)
SO_BACKLOG1024Connection queue depth
AUTO_READtrueAutomatic read on new channels
ALLOCATORPooledByteBufAllocator.DEFAULTMemory-efficient buffer allocation
WRITE_BUFFER_WATER_MARK8KB - 32KBBackpressure control

Channel Attributes

AttributeTypePurpose
REMOTE_SOCKETInetSocketAddressRemote proxy target (port-forwarding mode)
PROXYINGBooleanWhether channel is in proxy mode
TLS_ENABLED_UPSTREAMBooleanTLS active on client side
TLS_ENABLED_DOWNSTREAMBooleanTLS needed for upstream connections
HTTP_ENABLEDBooleanHTTP pipeline configured
HTTP2_ENABLEDBooleanHTTP/2 pipeline configured
TRANSPARENT_ORIGINAL_DST_RESOLVEDBooleanWhether original-dst was resolved (conntrack/PROXY protocol)
NETTY_SSL_CONTEXT_FACTORYNettySslContextFactorySSL context for this channel

Channel Initializer

MockServerUnificationInitializer is a @Sharable ChannelHandlerAdapter that replaces itself with a PortUnificationHandler on handlerAdded(). This thin adapter ensures each new channel gets its own PortUnificationHandler instance (since the decoder maintains per-channel state).

When transparentProxyEnabled is true, the initializer adds two handlers before the port unification handler:

  1. ProxyProtocolOriginalDestinationHandler ("proxy-protocol") — inspects the first inbound bytes for a PROXY protocol header, dispatching on the first byte: 0x0D → v2 (binary), 'P' → v1 (text). If a recognised header is found, sets REMOTE_SOCKET + PROXYING + TRANSPARENT_ORIGINAL_DST_RESOLVED (v2: for the PROXY command on INET/INET6; LOCAL/UNIX defer to downstream resolution), consumes the header bytes, and removes itself. If not found, removes itself and passes bytes through unchanged.
  2. TransparentProxyHandler ("transparent-proxy") — fires at channelActive and runs the pluggable CompositeOriginalDestinationResolver chain (default: TPROXY → eBPF → SO_ORIGINAL_DST → conntrack → dns-intent). Skips resolution if TRANSPARENT_ORIGINAL_DST_RESOLVED is already set (e.g., by the PROXY protocol handler).

Original Destination Resolver Chain

CompositeOriginalDestinationResolver.defaultChain(Configuration) tries strategies in order (first non-null wins):

OrderStrategyClassNotes
1TPROXY (IP_TRANSPARENT)TproxyOriginalDestinationResolverReturns channel.localAddress() when transparentProxyTproxy=true; null otherwise
2eBPF socket metadataEbpfOriginalDestinationResolverO(1) BPF hash-map lookup; requires Linux + CAP_BPF + external cgroup BPF program; enabled via transparentProxyEbpf=true
3SO_ORIGINAL_DST getsockoptSoOriginalDstResolverO(1) JNA getsockopt; requires Linux + Netty epoll transport
4Linux conntrack tableConntrackOriginalDestinationResolverO(n) conntrack table scan; fallback when SO_ORIGINAL_DST is unavailable
5DNS-intent (recover hostname MockServer's DNS answered)DnsIntentOriginalDestinationResolverConsults DnsIntentRegistry; last resort when all others return null

The DNS-intent resolver consults DnsIntentRegistry (mockserver-core, org.mockserver.mock.dns), which records the answeredIP → hostname mappings MockServer's own DNS server hands out (A/AAAA answers). When a connection arrives at such an IP and all earlier strategies return null, the resolver returns an unresolved InetSocketAddress carrying the recovered hostname, so downstream forwarding/matching works by name (loop-prevention guards against a DNS-to-self loop). The registry is cleared by HttpState.reset().

Note: PROXY protocol is handled separately in the pipeline (it reads bytes, not channel metadata).

Port Unification Handler

PortUnificationHandler extends Netty's ReplayingDecoder<Void> and is the heart of protocol detection. It inspects the first bytes of every connection and routes to the appropriate protocol pipeline.

Protocol Detection Order

flowchart TD
    START(["New Connection
First bytes arrive"]) --> SOCKS4{"SOCKS4?
byte 0 == 0x04"}
    SOCKS4 -->|Yes| EN_S4["enableSocks4
Add SOCKS4 decoders"]
    SOCKS4 -->|No| SOCKS5{"SOCKS5?
byte 0 == 0x05"}
    SOCKS5 -->|Yes| EN_S5["enableSocks5
Add SOCKS5 decoders"]
    SOCKS5 -->|No| TLS{"TLS?
SslHandler.isEncrypted"}
    TLS -->|Yes| EN_TLS["enableTls
Add SniHandler"]
    TLS -->|No| H2{"HTTP/2?
ALPN negotiated h2"}
    H2 -->|Yes| SW_H2["switchToHttp2
Add HTTP/2 pipeline"]
    H2 -->|No| HTTP{"HTTP?
GET/POST/PUT/..."}
    HTTP -->|Yes| SW_HTTP["switchToHttp
Add HTTP/1.1 pipeline"]
    HTTP -->|No| PROXY{"PROXIED_
prefix?"}
    PROXY -->|Yes| SW_PROXY["switchToProxyConnected
Internal relay setup"]
    PROXY -->|No| ASSUME{"Config:
assumeAllRequestsAreHttp?"}
    ASSUME -->|Yes| SW_HTTP
    ASSUME -->|No| SW_BIN["switchToBinaryRequestProxying
Raw binary handler"]

    EN_TLS -->|Re-fire decrypted bytes| START
    EN_S4 -->|Re-fire bytes| START
    EN_S5 -->|Re-fire bytes| START

    style EN_TLS fill:#f9f,stroke:#333
    style EN_S4 fill:#bbf,stroke:#333
    style EN_S5 fill:#bbf,stroke:#333

Recursive detection: When TLS or SOCKS is detected, the handler adds protocol-specific decoders, re-fires the bytes through the pipeline, and runs detection again on the decoded data. This enables arbitrary nesting (e.g., SOCKS5 → TLS → HTTP/2).

Connection Delay

A configurable connection delay can be applied before protocol detection begins. When connectionDelayMillis is set to a non-zero value, PortUnificationHandler.channelActive() suppresses auto-read on the new channel and schedules the first read to resume after the configured duration, so the first inbound bytes (and protocol detection) are deferred without blocking the event loop. This simulates slow connection establishment for testing timeout handling in clients.

Configuration: ConfigurationProperties.connectionDelayMillis(long millis), system property mockserver.connectionDelayMillis, environment variable MOCKSERVER_CONNECTION_DELAY_MILLIS. Default: 0 (no delay).

Non-blocking: The delay defers the first read via the event loop's scheduler instead of sleeping, so it does not stall other channels sharing the same worker thread. The delay is applied once per channel at channelActive.

TCP Chaos Handler

When TCP-layer chaos is active (at least one host registered in TcpChaosRegistry), a TcpChaosHandler is inserted at the front of the pipeline before HTTP codecs. This handler operates on raw ByteBuf data and can inject transport-layer faults that mirror Toxiproxy's named toxics:

Fault TypeFieldBehaviour
latencylatencyMsDelays all inbound data by the configured milliseconds
downdownSilently drops all inbound data (service appears down)
bandwidthbandwidthBytesPerSecThrottles inbound data to the configured bytes/sec
slow_closeslowCloseDelays the TCP FIN by 2 seconds on close
timeouttimeoutNever sends FIN; connection hangs on close
reset_peerresetPeerSends TCP RST and closes immediately
slicerslicerChunkSizeFragments inbound data into chunks of the configured size
limit_datalimitDataBytesCloses the connection after the configured bytes received

The handler is not sharable (each channel gets its own instance) because it maintains per-connection state (bytesConsumed for limitData).

Profiles are managed via the REST API:

  • PUT /mockserver/tcpChaos -- register, remove, or clear TCP chaos profiles
  • GET /mockserver/tcpChaos -- list all active TCP chaos profiles
  • PATCH /mockserver/tcpChaos -- merge-patch an existing profile

Profiles support optional TTL-based auto-expiry (dead-man's switch), identical to the ServiceChaosRegistry pattern.

Protocol-Specific Pipelines

HTTP/1.1 Pipeline

graph LR
    TCH["TcpChaosHandler
(conditional)"] --> A[HttpServerCodec]
    A --> B[PreserveHeadersNettyRemoves]
    B --> C[HttpContentDecompressor]
    C --> D[HttpContentLengthRemover]
    D --> EMH[EarlyMatchingHandler]
    EMH --> E[HttpObjectAggregator]
    E --> F[CallbackWebSocketServerHandler]
    F --> G[DashboardWebSocketHandler]
    G --> MCP["McpStreamableHttpHandler
(conditional)"]
    MCP --> H[MockServerHttpServerCodec]
    H --> I[HttpRequestHandler]
HandlerClassPurpose
TcpChaosHandlero.m.netty.unification(Conditional) Injects TCP-layer faults (latency, down, bandwidth, slicer, etc.) on raw bytes before HTTP decoding. Only added when TcpChaosRegistry has active entries
HttpServerCodecNetty built-inHTTP/1.1 request decoding / response encoding
PreserveHeadersNettyRemoveso.m.codecPreserves Content-Encoding/Transfer-Encoding headers that the downstream HttpContentDecompressor/HttpObjectAggregator strip (reset per request so they cannot leak across a pooled connection — issue #2322). Also captures the original (still compressed) request body bytes before decompression onto a channel attribute, so the decompressed body and the original on-the-wire bytes are both available (issue #2326)
HttpContentDecompressorNetty built-inDecompresses gzipped request bodies. The original compressed bytes are still preserved by PreserveHeadersNettyRemoves above and exposed via HttpRequest#getBodyAsOriginalRawBytes()
HttpContentLengthRemovero.m.netty.unificationStrips empty Content-Length headers
EarlyMatchingHandlero.m.netty.unificationOn the first HttpRequest (headers only), checks for an expectation with respondBeforeBody=true whose matcher has no body component. If found, dispatches the response (and any close) and discards remaining HttpContent, so the response can be sent before the body is read. Reproduces scenarios like okhttp/okhttp#1001 (issue #1831). Skipped for CONNECT and HTTP/2
HttpObjectAggregatorNetty built-inAggregates HTTP chunks into FullHttpRequest
CallbackWebSocketServerHandlero.m.netty.websocketregistryIntercepts /_mockserver_callback_websocket
DashboardWebSocketHandlero.m.dashboardIntercepts /_mockserver_ui_websocket
McpStreamableHttpHandlero.m.netty.mcpIntercepts /mockserver/mcp for MCP (Model Context Protocol) Streamable HTTP transport. Only added when ConfigurationProperties.mcpEnabled() is true. POST requests are offloaded to a dedicated executor (McpSessionManager.getExecutor()) to avoid blocking the Netty event loop during blocking tool calls (e.g., Future.get())
MockServerHttpServerCodeco.m.codecConverts Netty HTTP ↔ MockServer model
HttpRequestHandlero.m.nettyMain request processing

HTTP/2 Pipeline

graph LR
    SSL[SslHandler] --> TCH["TcpChaosHandler
(conditional)"]
    TCH --> H2C["HttpToHttp2ConnectionHandler
with InboundHttp2ToHttpAdapter"]
    H2C --> F[CallbackWebSocketServerHandler]
    F --> G[DashboardWebSocketHandler]
    G --> MCP["McpStreamableHttpHandler
(conditional)"]
    MCP --> H[MockServerHttpServerCodec]
    H --> I[HttpRequestHandler]

HTTP/2 frames are converted to HTTP/1.1 objects via InboundHttp2ToHttpAdapter, allowing the same HttpRequestHandler to process both protocols uniformly. When MCP is enabled (ConfigurationProperties.mcpEnabled()), the McpStreamableHttpHandler is also inserted in the HTTP/2 pipeline.

gRPC Pipeline (over HTTP/2)

When gRPC is enabled and the GrpcProtoDescriptorStore has loaded services, two additional handlers are inserted into both the h2c and TLS-negotiated HTTP/2 pipelines:

graph LR
    H2C["HttpToHttp2ConnectionHandler\nwith InboundHttp2ToHttpAdapter"] --> CB[CallbackWebSocketServerHandler]
    CB --> DASH[DashboardWebSocketHandler]
    DASH --> MCP["McpStreamableHttpHandler\n(conditional)"]
    MCP --> CODEC[MockServerHttpServerCodec]
    CODEC --> GRPC_RESP["GrpcToHttpResponseHandler\n(conditional)"]
    GRPC_RESP --> GRPC_REQ["GrpcToHttpRequestHandler\n(conditional)"]
    GRPC_REQ --> HANDLER[HttpRequestHandler]
HandlerClassPurpose
GrpcToHttpResponseHandlero.m.netty.grpcOutbound encoder — intercepts responses with x-grpc-service header, encodes JSON body back to gRPC-framed protobuf, appends grpc-status trailers; also converts gRPC-Web responses (trailers-in-body) when x-grpc-web-content-type header is present
GrpcToHttpRequestHandlero.m.netty.grpcInbound handler — intercepts application/grpc requests, decodes protobuf body to JSON using descriptors, rewrites as POST /<service>/<method> with x-grpc-* headers; also translates application/grpc-web* requests to standard gRPC before processing

The handlers are placed after MockServerHttpServerCodec so they operate on MockServer model objects (HttpRequest/HttpResponse), not raw Netty HTTP objects.

h2c (HTTP/2 cleartext) is detected by isH2cPreface() in PortUnificationHandler, which checks for the HTTP/2 connection preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n). Both switchToH2c() and switchToHttp2() conditionally wire gRPC handlers when descriptors are loaded. The switchToHttp() method also adds gRPC handlers to the HTTP/1.1 pipeline to support gRPC-Web over HTTP/1.1.

Multiplex Pipeline (default OFF)

When grpcBidiStreamingEnabled is true and gRPC descriptors are loaded, switchToHttp2() / switchToH2c() use an alternate HTTP/2 pipeline based on Http2FrameCodec + Http2MultiplexHandler instead of the connection-level HttpToHttp2ConnectionHandler + InboundHttp2ToHttpAdapter. Each HTTP/2 stream gets its own child channel initialized by GrpcMultiplexChildInitializer:

graph LR
    FC[Http2FrameCodec] --> MUX[Http2MultiplexHandler]
    MUX -->|per-stream child| SF["Http2StreamFrameToHttpObjectCodec\n(server=true)"]
    SF --> AGG[HttpObjectAggregator]
    AGG --> CB[CallbackWebSocketServerHandler]
    CB --> DASH[DashboardWebSocketHandler]
    DASH --> MCP["McpStreamableHttpHandler\n(conditional)"]
    MCP --> CODEC[MockServerHttpServerCodec]
    CODEC --> GRPC_RESP[GrpcToHttpResponseHandler]
    GRPC_RESP --> GRPC_REQ[GrpcToHttpRequestHandler]
    GRPC_REQ --> HANDLER[HttpRequestHandler]

Http2StreamFrameToHttpObjectCodec + HttpObjectAggregator re-aggregate inbound stream frames into FullHttpRequest objects, so the downstream handler chain sees the same objects as the connection-level adapter produces. This means inbound behaviour is byte-for-byte equivalent to the default pipeline for unary RPCs. The flag defaults to false; when off, the existing connection-level adapter path is used unchanged.

Server-streaming: GrpcStreamResponseActionHandler writes raw Netty HTTP objects (DefaultHttpResponse, per-message DefaultHttpContent, DefaultLastHttpContent with grpc-status/grpc-message trailers) directly to the ChannelHandlerContext. On the multiplex path, Http2StreamFrameToHttpObjectCodec is bidirectional and converts these outbound objects to HTTP/2 stream frames: initial HEADERS (with Transfer-Encoding: chunked automatically stripped by HttpConversionUtil), per-message DATA frames (byte-for-byte identical gRPC framing), and a trailing HEADERS frame with grpc-status/grpc-message and endStream=true. The MockServerHttpServerCodec encoder and GrpcToHttpResponseHandler do not intercept raw Netty objects (they only match org.mockserver.model.HttpResponse), so the objects pass through cleanly. No production code changes were needed -- the codec handles everything correctly.

PropertyDefaultEnv varSystem property
grpcBidiStreamingEnabledfalseMOCKSERVER_GRPC_BIDI_STREAMING_ENABLEDmockserver.grpcBidiStreamingEnabled

Client-streaming (collect-then-respond): For client-streaming RPCs, a client sends HEADERS followed by N DATA frames (each containing a gRPC length-prefixed message) then END_STREAM. On the multiplex path, Http2StreamFrameToHttpObjectCodec + HttpObjectAggregator re-aggregate all DATA frame bytes into a single FullHttpRequest body (byte-for-byte concatenation). GrpcToHttpRequestHandler.convertGrpcRequest() then decodes the concatenated body via GrpcFrameCodec.decode() into N messages, producing a JSON array body with the x-grpc-client-streaming: true header. This is identical to how the connection-level adapter handles client-streaming. Single-message requests (unary) decode as a single JSON object with no client-streaming header, preserving the distinction. No production code changes were needed -- the existing re-aggregation + decode pipeline handles this correctly.

Phase 3 will add true interleaved/reactive bidirectional streaming by removing the inbound re-aggregation and handling individual DATA frames with per-inbound-message reactive responses.

TLS Pipeline

graph LR
    SNI[SniHandler] -->|replaces self with| SSL[SslHandler]
    SSL -->|decrypted bytes re-enter| PU["PortUnificationHandler
detects inner protocol"]

SniHandler (in mockserver-core) extends Netty's AbstractSniHandler. It extracts the hostname from the TLS ClientHello SNI extension, dynamically generates a certificate with that hostname as a Subject Alternative Name, and negotiates ALPN (HTTP/1.1 or HTTP/2).

SOCKS4 Pipeline

graph LR
    S4D[Socks4ServerDecoder] --> S4E[Socks4ServerEncoder] --> S4H[Socks4ProxyHandler]

SOCKS5 Pipeline

graph LR
    S5D[Socks5InitialRequestDecoder] --> S5E[Socks5ServerEncoder] --> S5H[Socks5ProxyHandler]

SOCKS5 is multi-phase: initial handshake → optional password auth → CONNECT command.

Streaming Relay

When streamingResponsesEnabled is true (default), the HttpObjectAggregator in the forward-path client pipeline (inside NettyHttpClient) and in the relay pipelines inside RelayConnectHandler is replaced by StreamingAwareHttpObjectAggregator.

StreamingAwareHttpObjectAggregator is a subclass of HttpObjectAggregator (mockserver-core org.mockserver.codec). It inspects the first response head and switches to streaming when either signal is present:

  • Response says soContent-Type: text/event-stream.
  • The client asked for a streamNettyHttpClient sets an EXPECT_STREAMING_RESPONSE channel attribute when the forwarded request declares streaming intent (an Accept: text/event-stream header, or a JSON body with "stream": true). This covers streaming backends that omit the content type — notably the OpenAI Codex backend used by the opencode CLI (chatgpt.com/backend-api/codex/responses), whose SSE response carries no content type at all. Without it MockServer would aggregate the whole (10–30s) response before sending any headers, and the client would time out waiting for response headers.

Then:

  • Non-streaming (neither signal): delegates to super — behaviour is byte-for-byte identical to before. Ordinary chunked responses without an SSE content type or a streaming request are always aggregated normally. DISABLE_RESPONSE_STREAMING (set for FORWARD_REPLACE response overrides) wins over both signals.
  • Streaming: removes itself from the pipeline and installs StreamingResponseRelayHandler in its place, positioned before MockServerHttpClientCodec. It also removes the per-request ReadTimeoutHandler (sized from maxSocketTimeout, ~20s, armed on non-pooled channels) so the longer stream-appropriate idle bound (streamIdleTimeoutSeconds, default 60s) governs — a streaming LLM response can legitimately pause longer than 20s between chunks. The relay handler then processes unaggregated HttpObject events.

Boundaries. The request-intent path covers the forward-action client pipeline (HttpActionHandlerNettyHttpClient), which is the path TLS-intercepted proxy traffic from coding CLIs takes. The transparent CONNECT-relay pipeline (RelayConnectHandler, relayOnly) and the HTTP/2 upstream pipeline (which aggregates via InboundHttp2ToHttpAdapter) still rely on the response content type; a streaming response with no content type over those paths is aggregated. In practice coding-CLI proxy traffic is HTTP/1.1 over the forward path, so it is covered.

StreamingResponseRelayHandler

StreamingResponseRelayHandler (mockserver-core org.mockserver.httpclient) is a ChannelInboundHandler that consumes the raw HttpObject stream from the upstream server:

EventAction
HttpResponse (head)Builds a head-only org.mockserver.model.HttpResponse with a StreamingBody sink. Completes RESPONSE_FUTURE immediately.
HttpContentForwards the chunk to the downstream (client) channel. Appends to StreamingBody capture buffer (bounded to maxStreamingCaptureBytes).
LastHttpContentCloses the sink. Signals HttpActionHandler to write the FORWARDED_REQUEST log entry using the captured bytes.
channelInactive (mid-stream)Calls onError on the sink. Emits a FORWARDED_REQUEST log entry flagged as truncated/aborted.

An IdleStateHandler(0, 0, streamIdleTimeoutSeconds) is added to the streaming channel so stalled upstream connections are detected without the fixed global socket timeout cutting live streams.

StreamingBody

StreamingBody (mockserver-core org.mockserver.model) is a chunk sink used to bridge the Netty handler with HttpResponse. It holds:

  • A subscribe(onChunk, onComplete, onError) API consumed by the server-side NettyResponseWriter to write chunks to the downstream client.
  • A bounded byte capture buffer (capturedBytes()) with a truncated flag.

The server-side NettyResponseWriter checks response.getStreamingBody() != null and, when true, writes a DefaultHttpResponse head followed by DefaultHttpContent frames per chunk and LastHttpContent.EMPTY_LAST_CONTENT at stream end — mirroring the existing HttpSseResponseActionHandler pattern.

Response Trailers (Trailing Headers)

A HttpResponse can carry general HTTP trailers (trailing headers) via withTrailers(...) / withTrailer(name, values...), serialised in JSON as a trailers object that mirrors headers. When present they are emitted as protocol-appropriate trailing headers; when absent (null/empty — the default) the response is byte-for-byte identical to before.

flowchart TD
    R["HttpResponse with trailers"] --> ENC["MockServerHttpToNettyHttpResponseEncoder\nMockServerHttpResponseToFullHttpResponse"]
    ENC -->|"DefaultHttpResponse (chunked) + DefaultHttpContent +\nDefaultLastHttpContent.trailingHeaders()"| H1["HTTP/1.1: chunked body + Trailer header +\ntrailing header block"]
    ENC -->|same LastHttpContent trailing headers| H2["HTTP/2: HttpToHttp2ConnectionHandler /\nHttp2StreamFrameToHttpObjectCodec ⇒ trailing HEADERS frame"]
    W3["Http3ResponseWriter"] -->|"Http3RequestBridge.toHttp3TrailersFrame()"| H3["HTTP/3: trailing HEADERS frame after DATA"]
ProtocolWhereHow trailers are emitted
HTTP/1.1MockServerHttpResponseToFullHttpResponse.mapResponseWithTrailers() (mockserver-core)Emits a DefaultHttpResponse head with chunked transfer-encoding and an automatic Trailer header listing the field names (RFC 9110 §6.5.1), the body as DefaultHttpContent, and a DefaultLastHttpContent whose trailingHeaders() carry the trailers. A body-less status (204/304/HEAD) yields an empty LastHttpContent that still carries the trailers. Trailers force chunked encoding: RFC 7230 §3.3.1 makes a fixed Content-Length and chunked transfer-encoding mutually exclusive, and Netty's HttpObjectEncoder only writes the trailing-header block while in its chunked state — so any explicit Content-Length (and contentLengthHeaderOverride) is dropped from a trailer-carrying HTTP/1.1 response. Streaming-body responses (NettyResponseWriter.writeStreamingResponse) are already chunked and emit the same trailing-header block on a DefaultLastHttpContent at stream completion.
HTTP/2Netty HttpToHttp2ConnectionHandler (default pipeline) / Http2StreamFrameToHttpObjectCodec (gRPC-multiplex child pipeline)Both adapters strip transfer-encoding and convert the same LastHttpContent.trailingHeaders() into a trailing HEADERS frame with endStream=true. No MockServer-specific wiring is needed beyond the HTTP/1.1 mapping.
HTTP/3Http3ResponseWriter + Http3RequestBridge.toHttp3TrailersFrame() (mockserver-netty)After the DATA frame(s), a trailing Http3HeadersFrame is written before the QUIC stream output is shut down — for both static and streaming responses. Field names are lower-cased per HTTP/2/3 conventions.
Servlet (WAR)MockServerHttpResponseToHttpServletResponseEncoder.setTrailers() (mockserver-core)Sets HttpServletResponse.setTrailerFields(...); the container handles framing. The Servlet API models one string value per name, so multi-valued trailers are joined with ", " (HTTP list semantics) and duplicate names collapse to the last write — a WAR-path-only limitation.

Precedence vs gRPC trailers

gRPC responses carry their own status trailers (grpc-status / grpc-message), built by the gRPC layer independently of the general-trailer field:

  • On the gRPC HTTP/2 path (GrpcToHttpResponseHandler) the gRPC status is set as a response header on a cloned HttpResponse; the gRPC writers (Http3GrpcResponseWriter, GrpcStreamResponseActionHandler) build the trailing HEADERS frame directly from grpc-status/grpc-message.
  • The gRPC writers do not read the general trailers field, so on a gRPC response general trailers are simply not emitted — there is no name collision to resolve, because the general-trailer block is never produced on the gRPC path.

ByteBuf safety: the trailer path keeps the body buffer's refcount at exactly one across all branches — it is handed to a DefaultHttpContent only when non-empty and released in a finally otherwise (and on any exception between allocation and transfer), and a body-less response attaches an empty Unpooled.EMPTY_BUFFER LastHttpContent. The existing chunk-delay and HTTP/3 writer paths retain/release as before.

Connection-Lifecycle Response-Path Faults

These faults fire at response/dispatch time (not connect time) and are distinct from the connect-time TcpChaosHandler faults. They are implemented in NettyResponseWriter and Http2GoAwayEmitter, and gated by HttpRequestHandler's L6 cordon check.

Hot-path guarantee

NettyResponseWriter.resolveLifecycleProfile(HttpRequest) resolves the host-scoped TcpChaosProfile keyed on the request Host header. It returns null immediately when:

  1. ConfigurationProperties.connectionLifecycleChaosEnabled() is false, OR
  2. TcpChaosRegistry.getInstance().activeCount() == 0 (a single volatile read)

When either condition holds the normal write-and-close path is taken byte-for-byte unchanged. There is no allocation and no additional branching on the hot path in the common (no-lifecycle-chaos) case.

L1 — Mid-response RST (resetMidResponse)

NettyResponseWriter.writeHeadThenReset() writes the response head via ctx.writeAndFlush(response), then on the write-complete future sets SO_LINGER 0 and calls channel.close() — the same proven RST mechanism as TcpChaosHandler (zero linger makes the close emit a TCP RST rather than a FIN). The client sees "connection reset" while reading the body — the "server crashed mid-reply" fault.

When connectionLifecycleAutoHaltCountsRst is true (default), the RST records Metrics.incrementHttpChaosInjected("drop") so a RST storm trips the auto-halt circuit-breaker.

Streaming carve-out (v1): these L1/L2/L3 faults are applied only in the non-streaming writeAndCloseSocket() path. The streaming response path (writeStreamingResponse, SSE / chunked streaming) ignores lifecycle faults in v1 and completes normally even when a host profile is registered.

Host-scoping is not control-plane-exempt: like TcpChaosHandler, these faults are keyed on the request Host header and are not control-plane-exempt — a profile registered against the MockServer host itself can RST a control-plane response on that host. Register lifecycle profiles against the mocked-upstream host. (The L6 preemption cordon below is control-plane-exempt.)

L2 — Host-scoped slow close (slowCloseDelay)

NettyResponseWriter.addCloseSocketListener() applies the close delay in priority order:

  1. ConnectionOptions.closeSocketDelay (per-expectation)
  2. TcpChaosProfile.slowCloseDelay (host-scoped, L2 — only reached when no per-expectation delay is set)
  3. Immediate close (default)

This means a single PUT /mockserver/tcpChaos registration can make every response to a given host linger on close without touching individual expectations.

L3 — HTTP/2 GOAWAY on the response path (http2GoAway)

Http2GoAwayEmitter.emit(ctx, lastStreamId, errorCode) emits a connection-level GOAWAY frame so the client stops opening new streams. It is called in NettyResponseWriter.writeAndCloseSocket() before the response head is written, so the client receives the GOAWAY and can avoid opening further streams while the current stream still completes normally.

Implementation: Http2GoAwayEmitter resolves the Http2ConnectionHandler via ctx.pipeline().context(Http2ConnectionHandler.class) — the same lookup pattern as HttpErrorActionHandler.resetHttp2Stream. A negative lastStreamId argument is converted to Integer.MAX_VALUE and clamped down by the connection handler to the actual last-processed stream.

v1 scope: connection-level HTTP/2 pipeline only. The multiplex pipeline (per-stream child channels used for gRPC bidi streaming) is deferred — see chaos.md.

HTTP/1.1 degradation: when no Http2ConnectionHandler is found on the pipeline (HTTP/1.1 connection), Http2GoAwayEmitter.emit() returns false and callers degrade to Connection: close + 503. GOAWAY is benign (graceful drain signal) and is NOT counted toward the auto-halt window.

L6 — Preemption cordon check in HttpRequestHandler

HttpRequestHandler.channelRead0() checks the preemption cordon early in request processing, before any expectation matching:

flowchart TD
    REQ["Incoming request"] --> FEAT{"connectionLifecycleChaosEnabled?"}
    FEAT -->|No| NORMAL["Normal processing"]
    FEAT -->|Yes| CP{"path starts with /mockserver/?"}
    CP -->|Yes control-plane| NORMAL
    CP -->|No| CORD{"PreemptionSimulator.isCordoned()?"}
    CORD -->|No| NORMAL
    CORD -->|Yes| GA{"emitsGoAway() and HTTP/2?\n(Http2GoAwayEmitter.emit returns true)"}
    GA -->|Yes| EMIT["emit connection-level GOAWAY"]
    GA -->|No| REJ
    EMIT --> REJ{"rejectsNewExchanges()?"}
    REJ -->|Yes| REJECT["503 + Retry-After + Connection: close\ncompleteInFlight() + CLOSE listener"]
    REJ -->|No| NORMAL2["serve request normally\n(goaway-only: GOAWAY already sent / HTTP/1.1 no-op)"]

The control plane (/mockserver/...) is always exempt so the operator can observe state and issue DELETE /mockserver/preemption to uncordon. The GOAWAY is emitted lazily on a cordoned HTTP/2 connection's next request — Http2GoAwayEmitter.emit() is the HTTP/2 detection (it returns false, a no-op, on HTTP/1.1). The in-flight token is completed on every branch so the drain counter cannot leak, and GET /mockserver/preemption reports the live in-flight count from LifeCycle.getRequestsInFlight(). The isCordoned() probe is a single volatile read when no simulation is active, so this branch adds nothing measurable to the hot path in the common case. GOAWAY is HTTP/2-only; HTTP/1.1 has no GOAWAY and falls back to the 503 path (or is served normally in goaway-only mode).

Stream-Level Error Injection (HttpError streamError)

An HttpError action can reset the individual request stream instead of returning a response, for resilience testing of clients that must handle mid-stream resets. It is configured with HttpError.withStreamError(long errorCode) (or the StreamErrorCode enum / withStreamErrorCodeName convenience), serialised as a streamError integer. When streamError is null (the default) the existing dropConnection / responseBytes behaviour is unchanged.

flowchart TD
    A["HttpError with streamError"] --> D{"Action dispatch\nHttpActionHandler.dispatchErrorAction"}
    D -->|"responseWriter is StreamErrorWriter\n(HTTP/3)"| H3["Http3ResponseWriter.writeStreamError()\nQuicStreamChannel.shutdownOutput(code)\n→ QUIC RESET_STREAM"]
    D -->|"otherwise"| HEH["HttpErrorActionHandler.handle()"]
    HEH -->|"channel is Http2StreamChannel\n(multiplex child)"| MUX["write DefaultHttp2ResetFrame(code)\n→ RST_STREAM"]
    HEH -->|"Http2ConnectionHandler present\n+ request.streamId (default h2 path)"| CONN["Http2ConnectionHandler.resetStream(streamId, code)\n→ RST_STREAM"]
    HEH -->|"HTTP/1.1 (no stream)"| DROP["ctx.disconnect + close\n(connection drop fallback)"]
TransportWhereHow the stream is reset
HTTP/2 (default connection-level pipeline)HttpErrorActionHandler.resetHttp2Stream() (mockserver-core)Resolves the Http2ConnectionHandler (HttpToHttp2ConnectionHandler) via ctx.pipeline().context(...) and calls resetStream(ctx, streamId, errorCode, promise). The stream id is carried on the HttpRequest (set from the x-http2-stream-id extension header that InboundHttp2ToHttpAdapter adds — now captured for both TLS h2 and cleartext h2c).
HTTP/2 (gRPC multiplex pipeline)HttpErrorActionHandler.resetHttp2Stream()When the request is processed on a per-stream Http2StreamChannel child channel, writes a DefaultHttp2ResetFrame(errorCode) on that child channel; the parent Http2MultiplexHandler/Http2FrameCodec emits the RST_STREAM.
HTTP/3Http3ResponseWriter.writeStreamError() (mockserver-netty), reached via the StreamErrorWriter seamCalls QuicStreamChannel.shutdownOutput(errorCode), sending a QUIC RESET_STREAM for just this stream. The QUIC types live only in the netty module, so dispatch delegates through the transport-neutral StreamErrorWriter seam in core (mirroring the GrpcStreamResponseWriter pattern).
HTTP/1.1HttpErrorActionHandler.handle()No stream concept — falls back to dropping the whole connection (ctx.disconnect() + ctx.close()), the same as the existing dropConnection behaviour. Documented caveat: a streamError on HTTP/1.1 closes the connection rather than resetting a single stream.

HttpActionHandler.dispatchErrorAction() is the single funnel for the ERROR action (both the early-match and main paths). It first checks whether the active ResponseWriter implements StreamErrorWriter (the HTTP/3 case) and delegates; otherwise it hands the HttpError plus the HttpRequest (for the stream id) to HttpErrorActionHandler. ByteBuf safety: the HTTP/2 reset uses resetStream(...)/a single DefaultHttp2ResetFrame (no body buffer), and the HTTP/3 reset allocates no buffer, so there is nothing to leak. Resetting one stream leaves the rest of the multiplexed connection intact.

Relay Connect Pattern

When HTTP CONNECT or SOCKS tunneling is established, MockServer uses a self-loopback relay rather than connecting directly to the target:

sequenceDiagram
    participant C as Client
    participant H as HttpConnectHandler / SocksConnectHandler
    participant MS as MockServer (loopback)
    participant PU as PortUnificationHandler

    C->>H: CONNECT target.com:443
    H->>MS: New Bootstrap connection to localhost:1080
    H->>MS: Send "PROXIED_SECURE_target.com:443"
    MS->>PU: PortUnificationHandler.decode()
    PU->>PU: Detect PROXIED_ prefix
    PU->>PU: Set remote address = target.com:443
    PU->>PU: Set TLS flags
    MS->>H: "PROXIED_RESPONSE_..."
    H->>C: 200 Connection Established
    H->>H: Strip HTTP codecs from both pipelines
    H->>H: Install relay handlers

    Note over C,MS: Bidirectional relay established
    C->>H: Encrypted traffic
    H->>MS: UpstreamProxyRelayHandler → MockServer pipeline
    MS->>MS: Decrypt, match expectations, forward
    MS->>H: DownstreamProxyRelayHandler → response
    H->>C: Response to client

This pattern allows MockServer to:

  • Intercept and log tunneled HTTPS traffic
  • Match expectations against tunneled requests
  • Generate dynamic TLS certificates for the target hostname

IPv6 Support

The relay connect protocol and CONNECT handler support IPv6 addresses in bracket notation (e.g., [::1]:443, [2001:db8::1]:8443). Host:port parsing uses HttpRequest.splitHostPort() which correctly handles both IPv4 and IPv6 formats.

The local address detection (calculateLocalAddresses()) explicitly includes 127.0.0.1 and localhost, plus the bound interface address (via InetAddress.getHostAddress()), which may include IPv6 addresses depending on the network configuration. This ensures requests sent to MockServer's bound address are correctly identified as control-plane requests rather than proxy targets.

Relay Handler Hierarchy

classDiagram
    class RelayConnectHandler~T~ {
        <<abstract>>
        +channelRead0(ctx, msg)
        #removeCodecSupport(ctx)*
        #successResponse(request)*
        #failureResponse(request)*
    }

    class HttpConnectHandler {
        +removeCodecSupport()
        +successResponse(): HTTP 200
        +failureResponse(): HTTP 502
    }

    class SocksConnectHandler~T~ {
        <<abstract>>
        +removeCodecSupport()
    }

    class Socks4ConnectHandler {
        +successResponse(): SOCKS4 SUCCESS
        +failureResponse(): SOCKS4 REJECTED
    }

    class Socks5ConnectHandler {
        +successResponse(): SOCKS5 SUCCESS
        +failureResponse(): SOCKS5 FAILURE
    }

    RelayConnectHandler <|-- HttpConnectHandler
    RelayConnectHandler <|-- SocksConnectHandler
    SocksConnectHandler <|-- Socks4ConnectHandler
    SocksConnectHandler <|-- Socks5ConnectHandler

Relay Data Flow

Once the relay is established, two handler pairs shuttle data:

graph LR
    subgraph "Client-facing pipeline"
        UPR["UpstreamProxyRelayHandler
reads requests from client"]
    end

    subgraph "MockServer-facing pipeline"
        DPR["DownstreamProxyRelayHandler
reads responses from MockServer"]
    end

    UPR -->|writes requests to| MS["MockServer
Internal Channel"]
    MS -->|responses flow to| DPR
    DPR -->|writes responses to| CLIENT[Client Channel]

Relay Protocol Selection (HTTP/1.1 vs HTTP/2)

RelayConnectHandler.configurePipelines() builds both relay pipelines to match the protocol negotiated with the proxy client (http2EnabledDownstream, derived from the proxy-client TLS ALPN result):

  • the client-facing pipeline uses an HttpToHttp2ConnectionHandler when the client negotiated HTTP/2, otherwise an HttpServerCodec;
  • the internal loopback pipeline mirrors that choice — a client-mode HttpToHttp2ConnectionHandler for HTTP/2, otherwise an HttpClientCodec — and its client TLS context advertises h2 via ALPN only when the loopback codec is HTTP/2.

Keeping the loopback's TLS layer and codec in agreement makes the relay a transparent passthrough. Before this was fixed, the loopback hard-wired an HTTP/1.1 codec while its TLS could negotiate h2, so HTTP/2 requests through the CONNECT proxy were never decoded and hung (#2260).

When the http2Enabled configuration property is false, NettySslContextFactory never advertises h2 via ALPN and PortUnificationHandler ignores the h2c cleartext preface, so every connection — direct or relayed — falls back to HTTP/1.1.

DNS UDP Server

When dnsEnabled=true, MockServer.bindDnsPort() creates a separate Netty Bootstrap with NioDatagramChannel for UDP DNS:

graph LR
    UDP["NioDatagramChannel\n(UDP)"] --> DEC["DatagramDnsQueryDecoder"]
    DEC --> ENC["DatagramDnsResponseEncoder"]
    ENC --> HANDLER["DnsRequestHandler"]
HandlerClassPurpose
DatagramDnsQueryDecoderNetty built-in (netty-codec-dns)Decodes UDP datagrams into DatagramDnsQuery
DatagramDnsResponseEncoderNetty built-in (netty-codec-dns)Encodes DatagramDnsResponse to UDP datagrams
DnsRequestHandlero.m.netty.dnsMatches DNS queries against expectations via HttpState, returns DnsResponse records

The DNS channel uses the same workerGroup as the TCP server. It is managed separately from TCP serverChannelFutures — closed explicitly in MockServer.stopAsync().

Binary Protocol Handling

When no known protocol is detected, BinaryRequestProxyingHandler handles the raw bytes. The handler first checks for matching expectations via HttpState.firstMatchingExpectation(BinaryRequestDefinition). If a match with a BinaryResponse action is found, the response bytes are written directly to the channel. Otherwise, in proxy mode (when a remote address is configured on the channel), raw bytes are forwarded via NettyHttpClient.sendRequest(BinaryMessage, ...):

  • Waiting mode: Blocks until upstream response arrives, writes it back
  • Non-waiting mode: Fire-and-forget with optional BinaryProxyListener callback. BinaryProxyListener (o.m.model.BinaryProxyListener) is a functional interface with onProxy(BinaryMessage binaryRequest, CompletableFuture<BinaryMessage> binaryResponse, SocketAddress serverAddress, SocketAddress clientAddress) invoked when binary data is proxied

SOCKS Protocol Detection

SocksDetector provides static detection methods:

SOCKS4 detection (isSocks4):

  • Byte 0 = 0x04 (version)
  • Byte 1 = valid command (CONNECT or BIND)
  • Validates null-terminated username (max 256 chars)
  • Optionally validates SOCKS4a hostname

SOCKS5 detection (isSocks5):

  • Byte 0 = 0x05 (version)
  • Byte 1 = auth method count
  • Each auth method is NO_AUTH, PASSWORD, or GSSAPI

SOCKS5 handshake lifecycle:

stateDiagram-v2
    [*] --> InitialRequest: Client sends version + auth methods
    InitialRequest --> PasswordAuth: Server selects PASSWORD
    InitialRequest --> CommandRequest: Server selects NO_AUTH
    PasswordAuth --> CommandRequest: Credentials valid
    PasswordAuth --> [*]: Credentials invalid (close)
    CommandRequest --> RelayEstablished: CONNECT command
    CommandRequest --> [*]: Unsupported command (close)
    RelayEstablished --> [*]: Connection closed

SSL and Decoder Fault Logging

Netty's exceptionCaught fires for both benign connection closes and genuine faults. MockServer distinguishes these two categories using ExceptionHandling.isSslOrDecoderFault(Throwable):

Exception typeClassificationLogged at
SSLException (as cause)SSL/decoder faultWARN
DecoderExceptionSSL/decoder faultWARN
NotSslRecordExceptionSSL/decoder faultWARN
Connection reset / broken pipe (regex + stack match)Benign closesilent
Other unexpected exceptionsUnexpectedERROR

The isSslOrDecoderFault predicate is wired into the exceptionCaught handler of every handler that could receive these exceptions:

  • PortUnificationHandler (protocol detection)
  • HttpRequestHandler (main request dispatcher)
  • BinaryRequestProxyingHandler (raw binary proxy)
  • SocksProxyHandler (SOCKS4/5)
  • UpstreamProxyRelayHandler / DownstreamProxyRelayHandler / RelayConnectHandler (relay handlers)
  • CallbackWebSocketServerHandler (WebSocket callback channel)
  • McpStreamableHttpHandler (MCP streaming)
  • DashboardWebSocketHandler (dashboard WebSocket)

This means genuine SSL negotiation failures (e.g., client sends plain HTTP to a TLS port, or a non-TLS client probes a TLS port) surface at WARN and are visible in logs, while normal connection teardowns remain silent. ExceptionHandling.isSslOrDecoderFault mirrors the predicate already in connectionClosedException but as a positive match so callers can route specifically to WARN rather than silently drop.

Class Reference

ClassFileRole
Mainmockserver-netty/.../cli/Main.javaCLI entry point, argument parsing
LifeCyclemockserver-netty/.../lifecycle/LifeCycle.javaAbstract server lifecycle (event loops, port binding, shutdown)
MockServermockserver-netty/.../netty/MockServer.javaConcrete server, configures ServerBootstrap
MockServerUnificationInitializermockserver-netty/.../netty/MockServerUnificationInitializer.javaReplaces self with PortUnificationHandler
PortUnificationHandlermockserver-netty/.../netty/unification/PortUnificationHandler.javaProtocol detection and pipeline assembly
HttpRequestHandlermockserver-netty/.../netty/HttpRequestHandler.javaMain request dispatcher
NettyResponseWritermockserver-netty/.../netty/responsewriter/NettyResponseWriter.javaWrites responses to Netty channels
HttpErrorActionHandlermockserver-core/.../mock/action/http/HttpErrorActionHandler.javaApplies an HttpError action: raw response bytes, HTTP/2 stream reset (RST_STREAM), and/or connection drop (also the HTTP/1.1 stream-error fallback)
StreamErrorWritermockserver-core/.../responsewriter/StreamErrorWriter.javaTransport-neutral seam for resetting the request stream; implemented by Http3ResponseWriter for the QUIC RESET_STREAM
HttpConnectHandlermockserver-netty/.../netty/proxy/connect/HttpConnectHandler.javaHTTP CONNECT tunnel handler
RelayConnectHandlermockserver-netty/.../netty/proxy/relay/RelayConnectHandler.javaAbstract relay establishment
UpstreamProxyRelayHandlermockserver-netty/.../netty/proxy/relay/UpstreamProxyRelayHandler.javaClient → MockServer relay
DownstreamProxyRelayHandlermockserver-netty/.../netty/proxy/relay/DownstreamProxyRelayHandler.javaMockServer → client relay
BinaryRequestProxyingHandlermockserver-netty/.../netty/proxy/BinaryRequestProxyingHandler.javaRaw binary proxying
SocksDetectormockserver-netty/.../netty/proxy/socks/SocksDetector.javaSOCKS4/5 protocol detection
SocksProxyHandlermockserver-netty/.../netty/proxy/socks/SocksProxyHandler.javaAbstract SOCKS handler base
Socks4ProxyHandlermockserver-netty/.../netty/proxy/socks/Socks4ProxyHandler.javaSOCKS4 CONNECT handling
Socks5ProxyHandlermockserver-netty/.../netty/proxy/socks/Socks5ProxyHandler.javaSOCKS5 multi-phase handshake
SocksConnectHandlermockserver-netty/.../netty/proxy/socks/SocksConnectHandler.javaAbstract SOCKS relay base
SniHandlermockserver-core/.../socket/tls/SniHandler.javaTLS SNI extraction, dynamic cert generation
HttpContentLengthRemovermockserver-netty/.../netty/unification/HttpContentLengthRemover.javaStrips empty Content-Length
MockServerHttpServerCodecmockserver-core/.../codec/MockServerHttpServerCodec.javaNetty HTTP ↔ MockServer model codec
GrpcToHttpRequestHandlermockserver-netty/.../netty/grpc/GrpcToHttpRequestHandler.javagRPC request decode (protobuf→JSON); gRPC-Web translation
GrpcToHttpResponseHandlermockserver-netty/.../netty/grpc/GrpcToHttpResponseHandler.javagRPC response encode (JSON→protobuf); gRPC-Web re-framing
GrpcWebTranslatormockserver-core/.../grpc/GrpcWebTranslator.javagRPC-Web framing utilities (trailer frame, base64, content-type detection)
DnsRequestHandlermockserver-netty/.../netty/dns/DnsRequestHandler.javaDNS query matching and response
StreamingAwareHttpObjectAggregatormockserver-core/.../codec/StreamingAwareHttpObjectAggregator.javaReplaces HttpObjectAggregator in forward-path client pipelines; detects streaming responses and switches to StreamingResponseRelayHandler
StreamingResponseRelayHandlermockserver-core/.../httpclient/StreamingResponseRelayHandler.javaConsumes unaggregated HttpObject events; relays chunks immediately; captures bounded body; signals HttpActionHandler on completion
StreamingBodymockserver-core/.../model/StreamingBody.javaChunk sink bridging relay handler to server-side response writer; holds bounded capture buffer
AltSvcHeaderHandlermockserver-netty/.../netty/unification/AltSvcHeaderHandler.javaOutbound handler that adds Alt-Svc header to TCP responses when HTTP/3 is enabled; does not clobber user-set values