* test: guard group catch-all against 405-masking and route shadowing
Adds regression tests around automatic group catch-all (404) route
registration. v5 removed that auto-registration; if it is restored
(e.g. PR #2996) these tests ensure it does not bring back the issues
that motivated the removal:
- wrong HTTP method on an existing group route must still return 405
(with Allow header), not be masked to 404 by the catch-all;
- the group catch-all must not shadow concrete sibling routes, root
routes, or other sibling groups' routes.
All four pass on current master (v5). The 405 test fails against the
restore-v4-behavior approach in #2996, pinning down that tradeoff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: rewrite group method-handling tests after review
Addresses review of the original regression file (PR #3003): its comments
described an automatic group catch-all "triggered by middleware" that does
not exist on master (v5), so the tests passed for the wrong reason and the
no-op middleware was inert.
Rewrite to assert v5's actual, verified behavior:
- method mismatch on a group route -> 405 with full Allow header
- OPTIONS on a registered group route -> 204 with Allow (preflight-relevant)
- concrete routes resolve; group prefix does not affect root routes
The 405 and OPTIONS tests are real gates: a group-level catch-all (manual or
the auto-registration proposed in #2996) masks both as 404, which these tests
would then catch. Drops the false premise, the inert middleware, and the
in-comment PR reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add rewritten group method-handling tests
The previous commit recorded only the deletion of the old file; this adds
the rewritten suite (group_method_handling_test.go).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add companion test demonstrating group catch-all masking
Addresses final review: converts the "verified empirically" comment into an
actual test. TestGroupRoute_catchAllMasksMethodHandling registers a group-wide
catch-all and asserts it masks both the 405 method-mismatch and the automatic
OPTIONS (204) response as 404 — the regression the 405/OPTIONS gate tests guard
against. Makes the rationale self-proving in-repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add liveness signals to README and a public ROADMAP
- Add dynamic latest-release and last-commit badges (can't go stale)
- Add positioning vs net/http and an "actively maintained" note
- Add ROADMAP.md with version policy (v5 current, v4 LTS to 2026-12-31)
and surface it from the README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: fix review findings in README/ROADMAP
- Correct net/http interop claim: Echo handlers are not http.Handler;
interop is via WrapHandler/WrapMiddleware
- Clarify binding ships with a pluggable validator (not built-in validation)
- ROADMAP: reference canonical auto-HEAD issue/PR (#2895/#2949) instead of
the duplicate PRs (#2944/#2937) slated for closure
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* <Description>
The proxy middleware's WebSocket path currently sets `X-Forwarded-For` only
when the header is empty, dropping the proxy's own peer IP from the chain
whenever upstream proxies had already added entries. This breaks downstream
services that rely on the "rightmost untrusted" rule to extract the real
client IP, including echo's own `ExtractIPFromXFFHeader`.
The HTTP path delegates to `net/http/httputil.ReverseProxy`, which appends
`RemoteAddr` to the existing `X-Forwarded-For` chain — either inline in
`ServeHTTP`'s default Director path
([reverseproxy.go#L519-L531](https://github.com/golang/go/blob/go1.26.3/src/net/http/httputil/reverseproxy.go#L519-L531))
or via the explicit
[`(*ProxyRequest).SetXForwarded`](https://github.com/golang/go/blob/go1.26.3/src/net/http/httputil/reverseproxy.go#L82-L96)
when a `Rewrite` callback is configured. The WebSocket path uses `proxyRaw`,
which writes the request verbatim via `r.Write(out)`, so this middleware is
the only place where the appending happens for WebSocket Upgrade requests.
<Change>
Replace the "set if empty" guard with always-append. Read values via map
access to preserve multi-line `X-Forwarded-For` headers (RFC 9110 §5.3
allows combining them by joining values with commas).
<Test>
Added TestProxyWebSocketXForwardedFor exercising 4 cases:
- no incoming X-Forwarded-For → only c.RealIP()
- single-line single-entry → preserved + c.RealIP() at the tail
- ingle-line comma-separated → preserved + c.RealIP() at the tail
- multi-line headers (multiple X-Forwarded-For occurrences) → joined with , + c.RealIP() at the tail
Each case captures the request header at WebSocket Upgrade time inside the
upstream handler and asserts both the appended tail and the preserved prefix.
<Backwards compatibility>
The change is additive: existing entries are preserved and the proxy's own
peer IP is added at the tail. Downstream readers using the standard
"rightmost untrusted" rule (e.g. echo.ExtractIPFromXFFHeader) see no
behavioral difference for chains where they already worked, and start
returning the correct IP for chains where the proxy IP was previously
dropped.
<Background>
We hit this in production with an Echo-based WebSocket reverse proxy
fronting an internal service that uses echo.ExtractIPFromXFFHeader for
IP-based authorization. Multi-hop deployments (customer proxy → our reverse
proxy → backend) broke because the reverse proxy's egress IP was missing
from the chain reaching the backend.
* Set up upstream per test
DefaultKeyAuthConfig sets KeyLookup to "header:Authorization:Bearer "
in v5, but the KeyAuthConfig.KeyLookup comment still shows the v4
default of "header:Authorization". Update the comment to match.
Signed-off-by: leestana01 <leestana01@naver.com>
The non-Must variants (UnixTime, UnixTimeMilli, UnixTimeNano) all say
"binds parameter to time.Time variable", but the three Must* versions
copy-pasted the Duration variant's docstring and say time.Duration.
The function signature on each takes *time.Time. Also dropped the stray
double space and fixed "nano second" -> "nanosecond" on the
Nano one while in there.
Signed-off-by: Charlie Tonneslan <cst0520@gmail.com>
Modernizes the codebase using the Go 1.26 gofix tool to adopt newer
idioms and library features while maintaining compatibility with
the current toolchain.
* refactor: replace Split in loops with more efficient SplitSeq
Signed-off-by: box4wangjing <box4wangjing@outlook.com>
* refactor: replace Split in loops with more efficient SplitSeq
Signed-off-by: box4wangjing <box4wangjing@outlook.com>
---------
Signed-off-by: box4wangjing <box4wangjing@outlook.com>
Fix multiple typos found across the codebase:
- response.go: rename unexported field "commited" to "committed"
- server.go: fix "addres" to "address" in comment
- middleware/csrf.go: fix "formated" to "formatted" in comment
- middleware/request_logger.go: fix "commited" to "committed" in comment
- echotest/context.go: fix "wil" to "will" in comments
Fix directory traversal vulnerability under Windows in Static middleware when default Echo filesystem is used. Reported by @shblue21.
This applies to cases when:
- Windows is used as OS
- `middleware.StaticConfig.Filesystem` is `nil` (default)
- `echo.Filesystem` is has not been set explicitly (default)
Exposure is restricted to the active process working directory and its subfolders.