Every few months I find myself stuck on the same kind of question: what exactly is hitting my service? A reverse proxy is mangling a header, a Kubernetes ingress is rewriting a path, a webhook provider is sending a body in some shape I didn’t expect. The fastest way to answer is to point the traffic at something that will tell me, in painful detail, what arrived.

That’s why I wrote http-echo: a tiny Go server that responds to any HTTP request by dumping a structured, human-readable report of everything it saw.

Why another echo server

There are plenty of “echo” tools out there. The reason I built my own is that most of them either return the request as a blob of JSON (fine for machines, awful when you’re SSH’d into a debug pod), or they only show a subset of the request (no client IP, no Go runtime info, no container hostname). I wanted something I could kubectl exec into a debug pod, hit with curl, and read with my eyes.

So http-echo produces a report broken into clearly labelled sections: request summary, URL info, headers, body (with JSON pretty-printed automatically), parsed form data, server info, and timing. No flags, no config, no JSON soup unless you want it.

Getting it running

The fastest way is the published image:

docker run -p 8080:8080 ghcr.io/sgaunet/http-echo:latest

Or via Docker Compose if you’re wiring it into an existing stack:

docker-compose up -d
curl http://localhost:8080/hello?param=value

Build from source if you’d rather:

go build .
./http-echo

The binary is around 8 MB, statically compiled, no dependencies beyond the standard library. The container image is built FROM scratch and ships for amd64, arm64, armv6 and armv7.

What the output looks like

Hit it with a plain GET:

curl "http://localhost:8080/hello?param1=value1&param2=value2"

And you get something like this:

=== REQUEST SUMMARY ===
Timestamp: 2025-01-15T10:30:45Z
Method: GET | Protocol: HTTP/1.1 | Host: localhost:8080
Full URL: /hello?param1=value1&param2=value2
Remote Address: [::1]:53818
User Agent: curl/8.7.1

=== URL INFORMATION ===
Path: /hello?param1=value1&param2=value2
Query Parameters:
  param1 = value1
  param2 = value2

=== REQUEST HEADERS ===
* Accept         : */*

=== SERVER INFORMATION ===
Server Hostname: container-abc123
Go Version: go1.24
Server OS: linux/amd64

=== REQUEST COMPLETED ===
Processing Time: 76.208µs

Send a JSON body and it gets pretty-printed back to you. Send application/x-www-form-urlencoded and the keys are parsed into a readable list. The hostname section is the part I appreciate most when running multiple replicas behind a service — you can immediately tell which pod answered.

Where I actually use it

A few real cases from the past year:

  • Verifying ingress rewrites. I drop http-echo behind a new Ingress rule and confirm the path the upstream actually receives.
  • Webhook debugging. Point a third-party provider (GitLab, Stripe, whatever) at an http-echo instance and read the body that comes in.
  • Proxy header forwarding. Confirm X-Forwarded-For, X-Real-IP, X-Forwarded-Proto are arriving the way you assume.
  • Service mesh sanity checks. Deploy two replicas, hit the service a few times, see which pod answers via the hostname line.

It’s also useful in CI as a target for integration tests that need a “real” HTTP server but don’t care about the response.

Status

http-echo is in maintenance mode. It does what I built it for and the surface area is small. Dependencies and security fixes get attention; new features are unlikely. That’s deliberate — debug tools that keep growing tend to get in their own way.

It’s MIT-licensed and lives at github.com/sgaunet/http-echo. If you spend any time at all dealing with proxies, ingresses or webhooks, give it a shot — it costs you one docker run and might save you an hour of guessing.