Skip to content

Fix/relative url proxy handler#1220

Merged
tommysitu merged 1 commit intoSpectoLabs:masterfrom
scovl:fix/relative-url-proxy-handler
Apr 22, 2026
Merged

Fix/relative url proxy handler#1220
tommysitu merged 1 commit intoSpectoLabs:masterfrom
scovl:fix/relative-url-proxy-handler

Conversation

@scovl
Copy link
Copy Markdown
Contributor

@scovl scovl commented Apr 10, 2026

Problem

When an HTTP/1.1 client sends a request with a relative URL (path only, no scheme/host) and a Host header a valid pattern per RFC 7230 goproxy routes it to NonproxyHandler. The default implementation returns 500 "This is a proxy server", making Hoverfly appear broken for clients that rely on this behaviour (e.g. some HTTP libraries and CLI tools).

Reproduces with:

curl --proxy http://localhost:8500 http://example.com

Fix

Override NonproxyHandler in NewProxy to reconstruct the absolute URL from the Host header and re-dispatch the request through the normal proxy pipeline:

  • Returns 400 Bad Request if the Host header is absent (HTTP/1.0 style request with no host information cannot be forwarded).
  • Returns 500 (original behaviour) if the Host port matches the proxy's own port, preventing infinite recursion when a client connects directly to Hoverfly instead of using it as a proxy.
  • Otherwise sets r.URL.Scheme = "http" and r.URL.Host = r.Host, then calls proxy.ServeHTTP the same pattern already used by NewWebserverProxy.

Testing

Three regression tests added to core/proxy_test.go using raw TCP (required because http.DefaultClient always produces absolute URLs and would bypass NonproxyHandler):

  • Test_NewProxy_ShouldReturn400ForRelativeURLWithNoHostHeader
  • Test_NewProxy_ShouldHandleRelativeURLWithHostHeader
  • Test_NewProxy_ShouldReturn500WhenRelativeURLHostIsProxyItself

Closes #774

Resolves SpectoLabs#774. When goproxy routes a relative-URL HTTP/1.1 request to
NonproxyHandler, reconstruct the absolute URL from the Host header and
re-dispatch to the proxy. Return 400 Bad Request if Host is absent,
500 if Host points to the proxy itself (self-address guard to prevent
infinite recursion). Add regression tests covering all three paths.
Extract duplicated string literals into constants.
@tommysitu
Copy link
Copy Markdown
Member

hi @scovl thanks for your contributions! Just let you know that I will be offline for the next few days in case you're wondering why no one picks up the review.

no sure if @marcoioco is around, otherwise i will check these out as soon as I can!

@scovl
Copy link
Copy Markdown
Contributor Author

scovl commented Apr 10, 2026

hi @scovl thanks for your contributions! Just let you know that I will be offline for the next few days in case you're wondering why no one picks up the review.

no sure if @marcoioco is around, otherwise i will check these out as soon as I can!

Np, dude!
Ty!

@marcoioco
Copy link
Copy Markdown
Contributor

@scovl thanks. Will review this in a few days.

Comment thread core/proxy.go
// Guard: if the Host header targets the proxy's own port the client is connecting directly to
// Hoverfly (not using it as a proxy). In that case keep the original 500 "is a proxy server"
// response — forwarding to ourselves would cause infinite recursion or a panic.
proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it override the default goproxy implementation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it overrides the default goproxy NonproxyHandler, which returns HTTP 500 for all non-proxy requests. The new handler reconstructs the absolute URL from the Host header and re-dispatches the request through proxy.ServeHTTP, enabling HTTP/1.1 clients that send relative URLs to be handled correctly.

The original 500 behavior is preserved only when the Host targets the proxy's own port, to prevent infinite recursion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I think this should fix the issue while improving the existing non-proxy request check.

Copy link
Copy Markdown
Member

@tommysitu tommysitu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix — the approach is sound and the placement right after goproxy.NewProxyHttpServer() is correct, since it overrides the default NonproxyHandler before any other handlers are wired.

RFC compliance. HTTP/1.1 mandates a Host header (RFC 7230 §5.4), so the 400 for missing Host is correct. HTTP/1.0 clients without Host genuinely can't be reconstructed — 400 is the only sane response there too.

HTTPS scope. Hard-coding scheme = "http" is fine — raw TCP clients like telnet can't do TLS anyway, and HTTPS on the proxy port requires CONNECT.

Good test coverage too. Well done!

@tommysitu tommysitu merged commit be4c879 into SpectoLabs:master Apr 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Why would this be a bad request?

3 participants