Skip to content

echo

Echoes each HTTP request back as JSON, which is handy for inspecting what a client or proxy actually sends. Similar to mendhak/docker-http-https-echo.

$ curl -s 'http://localhost:8080/hello?name=world' -d 'hi'
{
  "timestamp": "2026-06-14T19:00:00Z",
  "protocol": "http",
  "method": "POST",
  "host": "localhost:8080",
  "hostname": "localhost",
  "path": "/hello",
  "url": "/hello?name=world",
  "query": { "name": ["world"] },
  "headers": { "Content-Type": ["application/x-www-form-urlencoded"] },
  "body": "hi",
  "remoteAddr": "127.0.0.1:54321",
  "ip": "127.0.0.1",
  "os": { "hostname": "echo-7d9c" }
}
  • Any path or method is echoed as JSON.
  • /ws upgrades to a WebSocket and echoes each message; a plain request to /ws is echoed normally.
  • /healthz (liveness) and /readyz (readiness) return {"status":"ok"} on the main echo port; /metrics serves Prometheus metrics on its own optional port (:8081), separate from the echo port.
  • Plain HTTP only; terminate TLS at the ingress. protocol is read from X-Forwarded-Proto when the peer is a trusted proxy.
  • Responses are application/json with X-Content-Type-Options: nosniff. Bodies are capped (1 MiB default) and flagged when truncated.
  • The client IP is read from X-Forwarded-For for trusted proxies.
  • With ECHO_KUBERNETES=true (chart config.kubernetes), adds a kubernetes block (pod, namespace, IP, node) from the Downward API. Off by default.
  • Callers can shape the response (status code, delay, extra headers) and pretty-print it — see below.

Shaping the response

Beyond reflecting the request, a caller can tell echo how to respond, which turns it into a test target for ingress, proxies, retries, and timeouts. Each directive is read from an echo-* query parameter or the matching X-Echo-* header (the query parameter wins if both are set):

Directive Query / header Effect
Status code echo-code / X-Echo-Code Respond with this status (200–599); invalid values are ignored.
Delay echo-delay / X-Echo-Delay Wait this long before responding (Go duration), capped at ECHO_MAX_DELAY.
Response header echo-header / X-Echo-Header Add a Name: Value response header; repeat for more than one.
Response cookie echo-cookie / X-Echo-Cookie Set a name:value response cookie; repeat for more than one.
Pretty-print echo-pretty-print / X-Echo-Pretty-Print Indent the JSON response. Always available, even when commands are off.
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:8080/?echo-code=503'
503
$ curl -s 'http://localhost:8080/?echo-delay=2s&echo-header=X-Test:1&echo-pretty-print'
# ...waits 2s, sets X-Test: 1, and returns indented JSON with an "applied" block

The response echoes which shaping directives were honored in an applied block, so a directive that was ignored (out of range, or commands disabled) is easy to spot. Response shaping is gated by ECHO_COMMANDS_ENABLED (on by default); set it to false to make echo a pure reflector. Pretty-printing is independent and always available. Header names use dashes — underscored header names are silently dropped by some proxies (e.g. ingress-nginx).

echo-header can set any header except echo's own Content-Type, Content-Length, X-Content-Type-Options, and Cache-Control, which are reserved so the JSON response stays coherent and inert. echo-cookie sets a bare name=value cookie; for attributes (Path, HttpOnly, …) use echo-header=Set-Cookie:... — the Domain attribute is stripped so a cookie cannot be scoped to a shared parent domain. Request cookies are always reflected back in the cookies field. Because echo-delay holds the connection open, keep ECHO_MAX_DELAY modest (or set ECHO_COMMANDS_ENABLED=false) when echo is reachable from untrusted networks, and rate-limit at the ingress.

Configuration

Set via environment variables:

Variable Default Description
ECHO_HTTP_PORT 8080 HTTP listen port (also serves the /healthz probe)
ECHO_METRICS_ENABLED true Expose Prometheus metrics; disabling removes the metrics listener
ECHO_METRICS_PORT 8081 Metrics listen port (/metrics only)
ECHO_LOG_LEVEL info debug, info, warn, or error
ECHO_LOG_FORMAT json json or text
ECHO_DISABLE_REQUEST_LOGS false Silence the per-request access log
ECHO_BACK_TO_CLIENT true Return the JSON body, or 204 when false
ECHO_MAX_BODY_BYTES 1048576 Max request body bytes read and echoed
ECHO_COMMANDS_ENABLED true Allow callers to shape the response (echo-* query / X-Echo-* headers)
ECHO_MAX_DELAY 10s Cap on the caller-requested echo-delay; itself capped under the 30s write timeout
ECHO_PRETTY_PRINT false Indent the JSON response by default (overridable with echo-pretty-print)
ECHO_WS_ENABLED true Serve the WebSocket echo at /ws
ECHO_WS_ALLOWED_ORIGINS (empty) Allowed WebSocket Origin host patterns (comma-separated); empty allows any
ECHO_WS_IDLE_TIMEOUT 5m Close a WebSocket that has sent nothing for this long; 0 disables
ECHO_TRUSTED_PROXIES (empty) Trusted-proxy CIDRs for X-Forwarded-For/-Proto (comma-separated)
ECHO_KUBERNETES false Add a kubernetes block (pod/node identity via the Downward API)
ECHO_SHUTDOWN_TIMEOUT 15s Graceful shutdown timeout

Running

Container:

docker run --rm -p 8080:8080 ghcr.io/home-operations/echo:rolling

Helm (Cosign-signed OCI chart; values are documented in charts/echo):

helm install echo oci://ghcr.io/home-operations/charts/echo --version <version>

Development

mise manages the toolchain.

mise install            # install pinned tools (go, golangci-lint, helm, etc.)
mise run build          # go build ./...
mise run test           # go test -race
mise run lint           # golangci-lint
mise run helm-unittest  # chart tests
mise run generate       # regenerate chart README + schema

License

See LICENSE.

About

An HTTP server that echoes each request back to the caller as JSON

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages