Solving Cannot handshake client first record does not look like a TLS handshake in Hoverfly

If you’ve ever tried to integrate Spring’s WebClient with Hoverfly for mocking or capturing HTTP requests, you might have run into a baffling problem: seemingly correct HTTP calls (http://...) resulting in cryptic TLS handshake errors in your proxy logs, and no traffic showing up in Hoverfly’s journal. What gives?

This isn’t a bug; it’s a fascinating case of protocol expectation mismatch between how WebClient (powered by Reactor Netty) talks to a proxy, and how Hoverfly (written in Go) initially expects that conversation to begin.

Hoverfly’s Expectation (Go-based Philosophy):

Hoverfly, being a sophisticated proxy written in Go, operates on clear, traditional HTTP proxy protocols:

  • For plain HTTP requests (e.g., to http://example.com), Hoverfly expects the client to send a direct, plain HTTP request line to the proxy (e.g., GET http://example.com/path HTTP/1.1).
  • For HTTPS requests (e.g., to https://example.com), Hoverfly expects the client to first send an HTTP CONNECT request (e.g., CONNECT example.com:443 HTTP/1.1). This signals the client’s intent to establish a secure tunnel through the proxy, after which a TLS handshake will occur.

This is how Hoverfly differentiates between simply forwarding an HTTP request and setting up a secure tunnel for HTTPS.

Spring WebClient’s Reality (Netty’s Behavior):

On the other hand, Spring’s WebClient, which leverages Reactor Netty as its underlying HTTP client, has a slightly different default behavior for proxying. Even for plain HTTP requests, Netty often sends an HTTP CONNECT request to the proxy first.

This is the core of the problem!

The Misunderstanding: HTTP Request, HTTPS Assumption

When your WebClient tries to make an HTTP call (say, to http://localhost:8090 or http://httpbin.org/anything) through Hoverfly:

  1. WebClient (via Netty) sends a CONNECT request to Hoverfly.
  2. Hoverfly receives this CONNECT request. Based on its default expectations, it interprets this CONNECT as a signal that the client intends to establish an HTTPS tunnel.
  3. Hoverfly then expects the next bytes it receives to be the start of a TLS handshake.
  4. However, your WebClient (for an HTTP target) immediately starts sending plain HTTP request bytes after the CONNECT.

This mismatch leads to the confusing error you see in Hoverfly’s logs:

WARN: Cannot handshake client httpbin.org:80 tls: first record does not look like a TLS handshake

The Elegant Solution: --plain-http-tunneling

Recognizing this common client behavior (where some clients send CONNECT for HTTP targets), the Hoverfly team thoughtfully added a flag to accommodate it.

The --plain-http-tunneling flag tells Hoverfly to be more lenient. When this flag is enabled, Hoverfly will:

  • Accept an incoming CONNECT request.
  • If the subsequent bytes are a TLS handshake, it proceeds as a normal HTTPS tunnel (MITM).
  • If the subsequent bytes are a plain HTTP request (after the CONNECT), it will also accept them and proxy them as plain HTTP.

Reference: https://github.com/SpectoLabs/hoverfly/issues/650

docker run -d \
  -p 30050:8500 \
  -p 30088:8888 \
  --name hoverfly \
  --rm \
  spectolabs/hoverfly:latest \
  --proxy-port 8500 \
  --admin-port 8888 \
  --mode capture \
  --log-level debug \
  --plain-http-tunneling # <--- The game-changer flag

Related Post