diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 436254a63..57ae0ffdd 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -4,9 +4,11 @@ on:
push:
branches:
- master
+ - v4
pull_request:
branches:
- master
+ - v4
workflow_dispatch:
permissions:
@@ -14,17 +16,17 @@ permissions:
env:
# run static analysis only with the latest Go version
- LATEST_GO_VERSION: "1.25"
+ LATEST_GO_VERSION: "1.27"
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up Go ${{ matrix.go }}
- uses: actions/setup-go@v5
+ uses: actions/setup-go@v6
with:
go-version: ${{ env.LATEST_GO_VERSION }}
check-latest: true
@@ -44,5 +46,3 @@ jobs:
go version
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
-
-
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index c7780fd21..1f26b0012 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -4,9 +4,11 @@ on:
push:
branches:
- master
+ - v4
pull_request:
branches:
- master
+ - v4
workflow_dispatch:
permissions:
@@ -14,7 +16,7 @@ permissions:
env:
# run coverage and benchmarks only with the latest Go version
- LATEST_GO_VERSION: "1.25"
+ LATEST_GO_VERSION: "1.27"
jobs:
test:
@@ -23,17 +25,17 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
# Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy
# Echo tests with last four major releases (unless there are pressing vulnerabilities)
- # As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when
- # we derive from last four major releases promise.
- go: ["1.22", "1.23", "1.24", "1.25"]
+ # As we depend on `golang.org/x/` libraries which only support the last 2 Go releases, we could have situations when
+ # we derive from the last four major releases promise.
+ go: ["1.26", "1.27"]
name: ${{ matrix.os }} @ Go ${{ matrix.go }}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up Go ${{ matrix.go }}
- uses: actions/setup-go@v5
+ uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
@@ -42,29 +44,30 @@ jobs:
- name: Upload coverage to Codecov
if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest'
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@v6
with:
token:
fail_ci_if_error: false
benchmark:
+ if: github.event_name == 'pull_request'
needs: test
name: Benchmark comparison
runs-on: ubuntu-latest
steps:
- name: Checkout Code (Previous)
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: ${{ github.base_ref }}
path: previous
- name: Checkout Code (New)
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
path: new
- name: Set up Go ${{ matrix.go }}
- uses: actions/setup-go@v5
+ uses: actions/setup-go@v6
with:
go-version: ${{ env.LATEST_GO_VERSION }}
@@ -74,12 +77,12 @@ jobs:
- name: Run Benchmark (Previous)
run: |
cd previous
- go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt
+ go test -run="-" -bench=".*" -benchtime=100ms -count=8 ./... > benchmark.txt
- name: Run Benchmark (New)
run: |
cd new
- go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt
+ go test -run="-" -bench=".*" -benchtime=100ms -count=8 ./... > benchmark.txt
- name: Run Benchstat
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7fd0e14e..6538dbe58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,173 @@
# Changelog
+## v4.16.5 - 2026-11-14
+
+**Security**
+
+* **Proxy middleware:** a client could remove `X-Forwarded-Proto` from the upstream request by naming it in the `Connection` header, on plain HTTP requests and WebSocket upgrades. The proxy now sets `X-Forwarded-Proto` again after Go removes hop-by-hop headers, as it already does for `X-Real-IP`. Fixes [GHSA-h6x5-phvw-c55c](https://github.com/labstack/echo/security/advisories/GHSA-h6x5-phvw-c55c). Reported by zachary-satterly.
+
+**Behavior change**
+
+A custom balancer that replaces the request and sets its own `X-Forwarded-Proto` no longer reaches the upstream with that value; the proxy uses the scheme of the original request. A custom `ProxyConfig.Transport` can still set the header.
+
+No public API, dependency or minimum Go-version changes; Go 1.26 or later remains required.
+
+## v4.16.4 - 2026-11-12
+
+**Security**
+
+* **Redirect middleware:** `HTTPSRedirect`, `HTTPSWWWRedirect`, `HTTPSNonWWWRedirect`, `WWWRedirect` and `NonWWWRedirect` now return `400 Bad Request` instead of redirecting when the host is empty, starts with `:` or `.`, contains `%`, or is empty after removing `www.`. Before this change, such hosts could produce an open redirect. The middleware no longer redirects `OPTIONS *` requests, maps `ws` and `wss` to `http` and `https` in redirect targets, and matches the `www.` prefix case-insensitively. Fixes [GHSA-3f6j-cw5h-qw9c](https://github.com/labstack/echo/security/advisories/GHSA-3f6j-cw5h-qw9c) ([#3217](https://github.com/labstack/echo/issues/3217)) in [#3224](https://github.com/labstack/echo/pull/3224).
+
+**Fixes**
+
+* **Trailing slash middleware:** `AddTrailingSlash` and `RemoveTrailingSlash` keep escaped path segments such as `%2F` in redirect and forward targets, so the request still matches its original route. Fixes [#3206](https://github.com/labstack/echo/issues/3206) in [#3225](https://github.com/labstack/echo/pull/3225).
+* **IPv6 zones:** `ExtractIPFromXFFHeader` and `ExtractIPFromRealIPHeader` trust zoned link-local proxy peers such as `[fe80::1%eth0]:8080`, with or without a port. Fixes [#3179](https://github.com/labstack/echo/issues/3179) in [#3226](https://github.com/labstack/echo/pull/3226).
+* **Header binding:** `BindHeaders` no longer panics on nil or empty header value slices. Fixes [#3169](https://github.com/labstack/echo/issues/3169) in [#3227](https://github.com/labstack/echo/pull/3227).
+
+**Behavior changes**
+
+* `Context.RealIP()` and `ExtractIPDirect()` now return a zoned link-local peer without its zone: `fe80::1`, not `fe80::1%eth0`. Update logs, allowlists or rate-limit keys that store the zoned form.
+* `HTTPSWWWRedirect` keeps its v4 behavior: it does not redirect plain HTTP requests for hosts that already start with `www.`. v5 redirects these requests.
+
+No public API, dependency or minimum Go-version changes; Go 1.26 or later remains required.
+
+## v4.16.3 - 2026-10-29
+
+**Security**
+
+* **Client IP defaults:** when `Echo#IPExtractor` is unset, `Context.RealIP()` now uses the connection peer and ignores `X-Forwarded-For` and `X-Real-IP`. This prevents clients from selecting their own default rate-limit bucket. Fixes [CVE-2026-118653](https://github.com/labstack/echo/security/advisories/GHSA-246p-cpwv-v3jq).
+* **Proxy identity:** Proxy middleware always replaces upstream `X-Real-IP` with the derived client identity, including after `Connection`-nominated header removal and on WebSocket upgrades. The identity is derived once and preserved across retries. Fixes [CVE-2026-118651](https://github.com/labstack/echo/security/advisories/GHSA-99jh-6h7p-pp36).
+
+**Migration required for applications behind proxies**
+
+Configure `Echo#IPExtractor` to trust only your deployment's proxies before upgrading. Otherwise, clients behind one proxy share its address for rate limiting, access controls, logging, and proxy forwarding. See the [README migration example](README.md#client-ip-security-migration-since-v4163) for explicit XFF trust ranges; use the same trust options with `ExtractIPFromRealIPHeader` for proxies that set `X-Real-IP`. Configure the outermost proxy to replace client-supplied identity headers. Explicit extractors and custom rate-limit identifiers retain their behavior.
+
+**Other changes**
+
+* The direct-peer fallback now accepts bare IPv4 and IPv6 addresses without a port.
+* Document v4 bug-fix and security-fix support dates (#3194).
+
+Implementation: [#3197](https://github.com/labstack/echo/pull/3197). No dependency or minimum Go-version changes; Go 1.26 or later remains required.
+
+## v4.16.0 - 2026-09-27
+
+**Security**
+
+This release fixes several security issues. Upgrading is recommended. Some fixes change behavior; read "Behavior changes to check before upgrading" below.
+
+* **Request scheme:** `Context.Scheme()` now uses the `X-Forwarded-Proto`, `X-Forwarded-Protocol`, `X-Forwarded-Ssl` and `X-Url-Scheme` headers only when the request comes directly from a loopback, link-local or private network address or a unix socket. Before this, any client could send `X-Forwarded-Proto: https` over plain HTTP and skip `HTTPSRedirect`. When `X-Forwarded-Proto` is present, only it is used (its last value), and the scheme is returned in lowercase. the new `Echo#SchemeExtractor` field selects the strategy: `ExtractSchemeFromHeaders(...TrustOption)` (default), `ExtractSchemeDirect()` or `LegacySchemeExtractor()`. The Secure middleware now sets HSTS based on `Context.Scheme()`. The Proxy middleware always sets `X-Forwarded-Proto` from `Context.Scheme()` and removes `X-Forwarded-Ssl`, `X-Forwarded-Protocol` and `X-Url-Scheme` before forwarding. [GHSA-2ffq-g2xg-c22p](https://github.com/labstack/echo/security/advisories/GHSA-2ffq-g2xg-c22p)
+* **JSONP:** `Context.JSONP` and `Context.JSONPBlob` accept only a callback that is empty, a JavaScript identifier or a dot-separated path of identifiers (ASCII letters, digits, `_` and `$`). Any other callback returns a 400 Bad Request error that wraps the new `ErrInvalidJSONPCallback`, and nothing is written. JSONP responses now carry `X-Content-Type-Options: nosniff`. JSONP lets any website read the response with the user's cookies, so do not use it for data that needs authentication. [GHSA-h9g5-28mm-hx3g](https://github.com/labstack/echo/security/advisories/GHSA-h9g5-28mm-hx3g)
+* **MethodOverride:** a POST can no longer be overridden to `GET`, `HEAD`, `OPTIONS`, `TRACE` or `CONNECT`. Before this, with the `MethodFromForm` or `MethodFromQuery` getter and MethodOverride registered with `Use` before the CSRF middleware, `_method=GET` skipped the CSRF check. Register MethodOverride with `Echo#Pre`. [GHSA-r7w9-592q-9vg4](https://github.com/labstack/echo/security/advisories/GHSA-r7w9-592q-9vg4)
+* **Redirects:** the trailing slash middlewares and the static directory redirect percent-encode control characters in the redirect path. Before this, `/%09/evil.example/` redirected browsers to `evil.example`. [GHSA-v753-g4cw-jm48](https://github.com/labstack/echo/security/advisories/GHSA-v753-g4cw-jm48)
+* **Static files:** with the default settings, the Static middleware resolves files from the same form of the path that the router matched, so `/admin%2Fsecret.txt` or `/%61dmin/secret.txt` can no longer reach a file under a guarded `/admin/*` route. [GHSA-375p-5qhx-8wq4](https://github.com/labstack/echo/security/advisories/GHSA-375p-5qhx-8wq4) The Static middleware and `StaticDirectoryHandler` (used by `Echo.Static`, `Echo.StaticFS`, `Group.Static` and `Group.StaticFS`) no longer serve paths with a `.`, `..` or empty segment, such as `/assets/../admin/secret.txt`, also after path unescaping. [GHSA-3pmx-cf9f-34xr](https://github.com/labstack/echo/security/advisories/GHSA-3pmx-cf9f-34xr)
+* **Dependencies:** update `golang.org/x/text` to v0.40.0 ([GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970)).
+
+**Client IP address (no code change in v4)**
+
+Without `Echo#IPExtractor`, `Context.RealIP()` in v4 trusts the `X-Forwarded-For` and `X-Real-IP` headers from any client, so the rate limiter can be bypassed and the Proxy middleware forwards a spoofed `X-Real-IP` ([GHSA-246p-cpwv-v3jq](https://github.com/labstack/echo/security/advisories/GHSA-246p-cpwv-v3jq), [GHSA-99jh-6h7p-pp36](https://github.com/labstack/echo/security/advisories/GHSA-99jh-6h7p-pp36)). Changing this default in v4 would put all clients behind a proxy into one rate-limit bucket, so v4 keeps it. Set an extractor that matches your deployment:
+```go
+e.IPExtractor = echo.ExtractIPDirect() // no proxy in front of the app
+e.IPExtractor = echo.ExtractIPFromXFFHeader() // behind proxies in private networks that set X-Forwarded-For
+// behind a proxy with public addresses (e.g. a CDN), also trust its ranges:
+// e.IPExtractor = echo.ExtractIPFromXFFHeader(echo.TrustIPRange(cdnRange))
+```
+v5 uses the direct peer address by default since v5.1.0.
+
+**Behavior changes to check before upgrading**
+
+* **Proxies or load balancers with public IP addresses.** If a proxy connects to your app from a public (or `100.64.0.0/10`) address, its `X-Forwarded-Proto` is now ignored: `HTTPSRedirect` redirects in a loop and the Secure middleware stops sending HSTS. This affects, for example, Cloudflare, CloudFront and Azure Front Door connecting to a public origin, the GCP external HTTP(S) load balancer including GKE Ingress (`35.191.0.0/16`, `130.211.0.0/22`), and networks that use `100.64.0.0/10` (such as Alibaba Cloud SLB or EKS custom networking). Trust the proxy's address ranges:
+ ```go
+ _, gclb1, _ := net.ParseCIDR("35.191.0.0/16")
+ _, gclb2, _ := net.ParseCIDR("130.211.0.0/22")
+ e.SchemeExtractor = echo.ExtractSchemeFromHeaders(echo.TrustIPRange(gclb1), echo.TrustIPRange(gclb2))
+ ```
+ Proxies on the same host, in a private network (AWS ALB, in-cluster ingress controllers such as ingress-nginx or Traefik, most PaaS routers) or on a unix socket keep working without changes. `echo.LegacySchemeExtractor()` restores the old behavior but is not safe unless every request passes through a proxy that sets these headers. Serverless adapters or middleware that set `RemoteAddr` to the client's address also make `X-Forwarded-Proto` ignored (or, if they take it from a header, spoofable).
+* **Trusted proxies must set `X-Forwarded-Proto`.** A proxy on a trusted address that passes the client's `X-Forwarded-Proto` through (for example nginx without `proxy_set_header X-Forwarded-Proto $scheme;`) still lets the client choose the scheme. An invalid `X-Forwarded-Proto` value now results in `http` instead of falling back to the other scheme headers.
+* **Your own tests.** `httptest.NewRequest` sets `RemoteAddr` to `192.0.2.1:1234`, which is not trusted, so tests that set `X-Forwarded-Proto` now see `http`. Set `req.RemoteAddr = "10.0.0.1:1234"` or use `e.SchemeExtractor = echo.LegacySchemeExtractor()` in such tests.
+* **Proxy middleware headers.** `X-Forwarded-Ssl`, `X-Forwarded-Protocol` and `X-Url-Scheme` are no longer forwarded to the upstream; `X-Forwarded-Proto` carries the scheme.
+* **MethodOverride.** Overriding a POST to `GET` (for example with `X-HTTP-Method-Override: GET` to send a long query in a POST body) is no longer done; such requests keep the POST method.
+* **Static files.** Paths with a double slash or dot segment (for example `/assets//app.js`) now return 404; in HTML5 mode the index is still served. The Static middleware no longer finds file names that the client sends with non-default escaping (for example `%2C`, `%40` or lowercase hex like `%c3%a9`) unless `StaticConfig.EnablePathUnescaping` is set; `Echo.Static` has behaved this way since v4.15.4. With `StaticConfig.EnablePathUnescaping` or `Echo#EnablePathUnescapingStaticFiles`, encoded dots (`%2e%2e`) no longer traverse directories, but encoded slashes are still decoded, so do not combine these options with route-based access control.
+* **JSONP.** `Context.JSONP` returns an error for callbacks that are not JavaScript identifiers.
+
+**Documentation**
+
+* Static middleware: when registered with `Echo#Use` it runs before route and group middleware, so route guards do not protect the files it serves.
+
+
+## v4.15.4 - 2026-06-15
+
+**Security**
+
+Fixes [GHSA-vfp3-v2gw-7wfq](https://github.com/labstack/echo/security/advisories/GHSA-vfp3-v2gw-7wfq)
+
+Make serving static file releated methods and middleware not unescape path by default - so how the way Router interprets paths and Static methods/middleware is consistent.
+
+Given following situation:
+```go
+// 0.
+// given folder structure:
+// private.txt
+// public/
+// public/index.html
+// public/text.txt
+// public/admin/private.txt
+
+// 1. share `public/` folder contents from the server root. This folder actually contains subfolder `admin` which
+// contents we want to forbid from downloading
+e.Static("/", "public")
+
+// 2. naively assume that everything under /admin folder is now forbidden
+e.GET("/admin/*", func(c *Context) error {
+ return ErrForbidden
+})
+```
+
+Then requests to `/admin%2fprivate.txt` would not be matched to `GET /admin/*` route (routing does not look unescaped path) and static file serving will use unescaped path to serve the file.
+
+Note: this way of "guarding" subfolders will never work for for paths like `/assets/../admin%2fprivate.txt` which will `path.Clean("/assets/../admin%2fprivate.txt")` to `/admin/private.txt` and are servable if static file serving is configured to unescape paths.
+
+If you want to guard routes - use middlewares on `Static*` methods and before `Static` middleware.
+
+**Breaking change / migration:** If you serve files whose names contain URL-encoded characters (e.g., `/hello%20world.txt` → `hello world.txt`), you must now opt in:
+
+```go
+ e := echo.New()
+ e.EnablePathUnescapingStaticFiles = true // <-- enable old behavior
+ e.Static("/", "public")
+```
+for static middleware
+```go
+ e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
+ EnablePathUnescaping: true, // <-- enable old behavior
+ }))
+```
+
+
+## v4.15.3 - 2026-06-14
+
+**Security**
+
+* fix(static): reject encoded path separators that bypass route-level middleware by @vishr in https://github.com/labstack/echo/pull/3011
+
+Fixes [GHSA-vfp3-v2gw-7wfq](https://github.com/labstack/echo/security/advisories/GHSA-vfp3-v2gw-7wfq): an encoded path separator (`%2F` or `%5C`) in a static file URL could bypass route-level middleware (e.g. authentication on a sibling route) and disclose static files. Both `StaticDirectoryHandler` (used by `Static`/`StaticFS`) and the `Static` middleware are affected. Backport of the v5 fix (#3009). Thanks to @a-tt-om and @oran-gugu for reporting.
+
+
+## v4.15.2 - 2026-05-01
+
+**Security**
+
+* `Context.Scheme()` should validate values taken from header by @aldas in https://github.com/labstack/echo/pull/2962
+
+Thanks to @shblue21 for reporting this [issue](https://github.com/labstack/echo/issues/2952).
+
+
+## v4.15.1 - 2026-02-22
+
+**Enhancements**
+
+* CSRF: support older token-based CSRF protection handler that want to render token into template by @aldas in https://github.com/labstack/echo/pull/2905
+
+
## v4.15.0 - 2026-01-01
diff --git a/README.md b/README.md
index 5e52d1d4e..2a1602509 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,112 @@
-[](https://sourcegraph.com/github.com/labstack/echo?badge)
-[](https://pkg.go.dev/github.com/labstack/echo/v4)
-[](https://goreportcard.com/report/github.com/labstack/echo)
-[](https://github.com/labstack/echo/actions)
-[](https://codecov.io/gh/labstack/echo)
-[](https://github.com/labstack/echo/discussions)
-[](https://twitter.com/labstack)
-[](https://raw.githubusercontent.com/labstack/echo/master/LICENSE)
+
-Help and questions: [Github Discussions](https://github.com/labstack/echo/discussions)
+## About
+This branch holds Echo v4, the maintenance line. Echo v5 is the current release; start new projects on [v5](https://github.com/labstack/echo).
-### Feature Overview
+### Support
-- Optimized HTTP router which smartly prioritize routes
-- Build robust and scalable RESTful APIs
-- Group APIs
-- Extensible middleware framework
-- Define middleware at root, group or route level
-- Data binding for JSON, XML and form payload
-- Handy functions to send variety of HTTP responses
+Echo v4 receives bug fixes until 2026-12-31 and free security fixes until 2027-12-31, on its latest release only. It gets no new features. See [SECURITY.md](https://github.com/labstack/echo/blob/master/SECURITY.md) for the full policy.
+
+Echo v4 requires Go 1.26 or later (since v4.16.1), to use patched `golang.org/x/` dependencies.
+
+[Echo Support](https://echo.labstack.com/pro#support) is a paid annual plan from LabStack for v4 and v5 applications. It includes a named contact, email support with a two-business-day response target, and an annual maintenance-status letter.
+
+## Upgrade notes
+
+### Client IP security migration since v4.16.3
+
+> **Important:** Behind a proxy, set `Echo#IPExtractor` before you upgrade to v4.16.3. Otherwise all clients behind the proxy share one IP address for rate limiting, access controls and logging.
+
+Echo v4.16.3 changes client IP defaults. Without `Echo#IPExtractor`, `Context.RealIP()` uses the connection peer and ignores `X-Forwarded-For` and `X-Real-IP`. Proxy middleware always replaces upstream `X-Real-IP` with this result. This fixes [CVE-2026-118651](https://github.com/labstack/echo/security/advisories/GHSA-99jh-6h7p-pp36) and [CVE-2026-118653](https://github.com/labstack/echo/security/advisories/GHSA-246p-cpwv-v3jq). Applications that already set `Echo#IPExtractor` keep their behavior. A custom rate-limit identifier that does not call `RealIP()` is also unaffected.
+
+Behind a proxy, configure `Echo#IPExtractor` before starting the server. Otherwise, clients behind the same proxy share its IP address for rate limiting, access controls and logging. Use `echo.ExtractIPDirect()` when there is no proxy. For proxies that set `X-Forwarded-For`, turn off the broad default trust ranges and trust only your proxy addresses:
+
+```go
+// After e := echo.New(); imports "log" and "net".
+_, proxyRange, err := net.ParseCIDR("10.0.0.10/32") // replace with your proxy's address or CIDR
+if err != nil {
+ log.Fatal(err)
+}
+e.IPExtractor = echo.ExtractIPFromXFFHeader(
+ echo.TrustLoopback(false),
+ echo.TrustLinkLocal(false),
+ echo.TrustPrivateNet(false),
+ echo.TrustIPRange(proxyRange),
+)
+```
+
+If your proxy uses `X-Real-IP`, use `echo.ExtractIPFromRealIPHeader` with the same trust options. Configure the outermost proxy to replace client-supplied identity headers.
+
+### Security defaults since v4.16.1
+
+See the [v4.16.1 release notes](https://github.com/labstack/echo/releases/tag/v4.16.1) for the advisories.
+
+`BodyDump` captures at most 5 MiB of the request and 5 MiB of the response by default. The application and client still receive the complete bodies. Set `BodyDumpConfig.MaxRequestBytes` and `MaxResponseBytes` to change the capture limits; `-1` removes a limit.
+
+`Decompress` limits decompressed request bodies to 100 MiB by default. Reading beyond the limit returns HTTP 413, including through JSON and XML binding. Set `DecompressConfig.MaxDecompressedSize` to change the limit; `-1` removes it. Apply any smaller application body limit after decompression.
+
+The proxy middleware validates WebSocket upgrade responses, closes both WebSocket connections when either side disconnects, and removes incoming `Forwarded` headers. Backends should trust only the forwarding headers the proxy sets and enforce their own authorization.
+
+### Request scheme since v4.16.0
+
+`Context.Scheme()` uses `X-Forwarded-Proto` and related headers only when the request comes directly from a loopback, link-local or private network address, or a unix socket. If a proxy with a public address terminates TLS and forwards plain HTTP to Echo, `HTTPSRedirect` keeps redirecting until you set `Echo#SchemeExtractor` to trust that proxy. See the [v4.16.0 changelog](https://github.com/labstack/echo/blob/v4/CHANGELOG.md#v4160---2026-09-27) for this and the other fixes in that release.
+
+## Features
+
+- Router with static, parameter and wildcard routes; static routes match first
+- RESTful APIs with route groups
+- Middleware at the root, group or route level
+- Data binding for JSON, XML, form, query and path parameters
+- Helpers for common HTTP responses
- Centralized HTTP error handling
- Template rendering with any template engine
-- Define your format for the logger
-- Highly customizable
-- Automatic TLS via Let’s Encrypt
-- HTTP/2 support
+- Customizable logging
+- Automatic TLS through Let's Encrypt
+- HTTP/2, including h2c
## Sponsors
-