+
diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro
new file mode 100644
index 00000000..fe49f659
--- /dev/null
+++ b/site/src/components/Footer.astro
@@ -0,0 +1,19 @@
+---
+import Default from '@astrojs/starlight/components/Footer.astro';
+import AskEcho from './AskEcho.astro';
+
+const year = new Date().getFullYear();
+---
+
+
+
diff --git a/site/src/components/HomeHero.astro b/site/src/components/HomeHero.astro
new file mode 100644
index 00000000..4d5dad99
--- /dev/null
+++ b/site/src/components/HomeHero.astro
@@ -0,0 +1,109 @@
+---
+// Living Terminal hero: code window + a terminal pane that types `curl`
+// and streams Echo's JSON response. Animation is client-side, with a
+// static final-state fallback for reduced-motion / no-JS.
+import { starsLabel } from '../data/github.ts';
+---
+
+
+
+ Echo v5.2 — now released
+
Build fast Go APIs. Without the bloat.
+
+ A high-performance, minimalist Go web framework — a zero-allocation
+ router, batteries-included middleware, and an expressive API. Ship
+ production services in minutes.
+
+
+
+
+
+```
diff --git a/site/src/content/docs/cookbook/graceful-shutdown.md b/site/src/content/docs/cookbook/graceful-shutdown.md
new file mode 100644
index 00000000..2caf6591
--- /dev/null
+++ b/site/src/content/docs/cookbook/graceful-shutdown.md
@@ -0,0 +1,86 @@
+---
+title: Graceful Shutdown
+description: Drain in-flight requests before stopping the server on an interrupt signal.
+sidebar:
+ order: 8
+---
+
+A graceful shutdown lets in-flight requests finish before the process exits. The
+simplest approach is to pass a cancellable context to `StartConfig.Start` and set
+a `GracefulTimeout`. When the context is cancelled by an interrupt signal, Echo
+stops accepting new connections and waits up to the timeout for active requests to
+complete.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/labstack/echo/v5"
+)
+
+func main() {
+ // Setup
+ e := echo.New()
+ e.GET("/", func(c *echo.Context) error {
+ time.Sleep(5 * time.Second)
+ return c.JSON(http.StatusOK, "OK")
+ })
+
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer stop()
+
+ sc := echo.StartConfig{
+ Address: ":1323",
+ GracefulTimeout: 5 * time.Second,
+ }
+ if err := sc.Start(ctx, e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Using a custom HTTP server
+
+If you manage the `http.Server` yourself, start it in a goroutine, wait on the
+signal context, then call `Shutdown` with a timeout:
+
+```go
+func mainWithHTTPServer() {
+ // Setup
+ e := echo.New()
+ e.GET("/", func(c *echo.Context) error {
+ time.Sleep(5 * time.Second)
+ return c.JSON(http.StatusOK, "OK")
+ })
+
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer stop()
+
+ s := http.Server{Addr: ":1323", Handler: e}
+ // Start server
+ go func() {
+ if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+ }()
+
+ // Wait for interrupt signal to gracefully shut down the server with a timeout of 10 seconds.
+ <-ctx.Done()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ if err := s.Shutdown(ctx); err != nil {
+ e.Logger.Error("failed to stop server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/hello-world.md b/site/src/content/docs/cookbook/hello-world.md
new file mode 100644
index 00000000..e41fb41d
--- /dev/null
+++ b/site/src/content/docs/cookbook/hello-world.md
@@ -0,0 +1,43 @@
+---
+title: Hello World
+description: A minimal Echo server that responds with a greeting.
+sidebar:
+ order: 1
+---
+
+A minimal Echo application: create an instance, register the Logger and Recover
+middleware, add a single route, and start the server.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ // Echo instance
+ e := echo.New()
+
+ // Middleware
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ // Route => handler
+ e.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Hello, World!\n")
+ })
+
+ // Start server
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/http2-server-push.md b/site/src/content/docs/cookbook/http2-server-push.md
new file mode 100644
index 00000000..c11004c8
--- /dev/null
+++ b/site/src/content/docs/cookbook/http2-server-push.md
@@ -0,0 +1,130 @@
+---
+title: HTTP/2 Server Push
+description: Push web assets to the client proactively over HTTP/2.
+sidebar:
+ order: 10
+---
+
+HTTP/2 server push lets the server send resources to the client before they are
+requested, eliminating a round trip for assets the page is known to need. This
+recipe pushes a page's CSS, JavaScript, and image alongside the HTML response.
+
+:::note
+Server push requires an HTTP/2 connection. Follow [Generate a self-signed X.509
+TLS certificate](/cookbook/http2/#1-generate-a-self-signed-x509-tls-certificate)
+to create the certificate used below.
+:::
+
+## 1. Register a route to serve web assets
+
+```go
+e.Static("/", "static")
+```
+
+## 2. Serve index.html and push its dependencies
+
+Unwrap the response to access the underlying `http.ResponseWriter`, then push each
+asset if the writer implements `http.Pusher`:
+
+```go
+e.GET("/", func(c *echo.Context) (err error) {
+ rw, err := echo.UnwrapResponse(c.Response())
+ if err != nil {
+ return
+ }
+ if pusher, ok := rw.ResponseWriter.(http.Pusher); ok {
+ if err = pusher.Push("/app.css", nil); err != nil {
+ return
+ }
+ if err = pusher.Push("/app.js", nil); err != nil {
+ return
+ }
+ if err = pusher.Push("/echo.png", nil); err != nil {
+ return
+ }
+ }
+ return c.File("index.html")
+})
+```
+
+:::tip
+When `http.Pusher` is supported, the web assets are pushed proactively; otherwise
+the client falls back to requesting them separately.
+:::
+
+## 3. Start the TLS server
+
+```go
+sc := echo.StartConfig{Address: ":1323"}
+if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+}
+```
+
+## Source code
+
+### index.html
+
+```html
+
+
+
+
+
+
+ HTTP/2 Server Push
+
+
+
+
+
+
The following static files are served via HTTP/2 server push
+
+
/app.css
+
/app.js
+
/echo.png
+
+
+
+```
+
+### server.go
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+)
+
+func main() {
+ e := echo.New()
+ e.Static("/", "static")
+ e.GET("/", func(c *echo.Context) (err error) {
+ rw, err := echo.UnwrapResponse(c.Response())
+ if err != nil {
+ return
+ }
+ if pusher, ok := rw.ResponseWriter.(http.Pusher); ok {
+ if err = pusher.Push("/app.css", nil); err != nil {
+ return
+ }
+ if err = pusher.Push("/app.js", nil); err != nil {
+ return
+ }
+ if err = pusher.Push("/echo.png", nil); err != nil {
+ return
+ }
+ }
+ return c.File("index.html")
+ })
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/http2.md b/site/src/content/docs/cookbook/http2.md
new file mode 100644
index 00000000..1eeef540
--- /dev/null
+++ b/site/src/content/docs/cookbook/http2.md
@@ -0,0 +1,116 @@
+---
+title: HTTP/2 Server
+description: Serve traffic over HTTP/2 by starting Echo with a TLS certificate.
+sidebar:
+ order: 9
+---
+
+HTTP/2 improves latency through request multiplexing, header compression, and
+server push. Go's HTTP server negotiates HTTP/2 automatically over TLS, so serving
+HTTP/2 with Echo is a matter of starting the server with a certificate.
+
+## 1. Generate a self-signed X.509 TLS certificate
+
+Run the following command to generate `cert.pem` and `key.pem`:
+
+```sh
+go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost
+```
+
+:::note
+For demonstration purposes we use a self-signed certificate. In production, obtain
+a certificate from a [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority).
+:::
+
+## 2. Create a handler that echoes request information
+
+```go
+e.GET("/request", func(c *echo.Context) error {
+ req := c.Request()
+ format := `
+
+ Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+ `
+ return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path))
+})
+```
+
+## 3. Start the TLS server
+
+Start the server with the generated certificate and key:
+
+```go
+sc := echo.StartConfig{Address: ":1323"}
+if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+}
+```
+
+Alternatively, use a custom `http.Server` with your own `tls.Config`:
+
+```go
+s := http.Server{
+ Addr: ":8443",
+ Handler: e, // set Echo as handler
+ TLSConfig: &tls.Config{
+ //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field
+ },
+ //ReadTimeout: 30 * time.Second, // use custom timeouts
+}
+if err := s.ListenAndServeTLS("cert.pem", "key.pem"); err != http.ErrServerClosed {
+ log.Fatal(err)
+}
+```
+
+## 4. Verify
+
+Start the server and browse to `https://localhost:1323/request`. You should see
+output similar to:
+
+```sh
+Protocol: HTTP/2.0
+Host: localhost:1323
+Remote Address: [::1]:60288
+Method: GET
+Path: /
+```
+
+## Source code
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+)
+
+func main() {
+ e := echo.New()
+ e.GET("/request", func(c *echo.Context) error {
+ req := c.Request()
+ format := `
+
+ Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+ `
+ return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path))
+ })
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/jsonp.md b/site/src/content/docs/cookbook/jsonp.md
new file mode 100644
index 00000000..1b8f4857
--- /dev/null
+++ b/site/src/content/docs/cookbook/jsonp.md
@@ -0,0 +1,95 @@
+---
+title: JSONP
+description: Serve JSONP responses for cross-domain requests with Context#JSONP.
+sidebar:
+ order: 13
+---
+
+JSONP is a technique that allows cross-domain server calls from the browser. Echo
+serves JSONP responses with `c.JSONP()`, which wraps the JSON payload in a call to
+the callback function named in the request.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "math/rand"
+ "net/http"
+ "time"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ e := echo.New()
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ e.Static("/", "public")
+
+ // JSONP
+ e.GET("/jsonp", func(c *echo.Context) error {
+ callback := c.QueryParam("callback")
+ var content struct {
+ Response string `json:"response"`
+ Timestamp time.Time `json:"timestamp"`
+ Random int `json:"random"`
+ }
+ content.Response = "Sent via JSONP"
+ content.Timestamp = time.Now().UTC()
+ content.Random = rand.Intn(1000)
+ return c.JSONP(http.StatusOK, callback, &content)
+ })
+
+ // Start server
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Client
+
+```html
+
+
+
+
+
+
+ JSONP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/site/src/content/docs/cookbook/jwt.md b/site/src/content/docs/cookbook/jwt.md
new file mode 100644
index 00000000..c4f126f1
--- /dev/null
+++ b/site/src/content/docs/cookbook/jwt.md
@@ -0,0 +1,251 @@
+---
+title: JWT
+description: Authenticate requests with JSON Web Tokens using the echo-jwt middleware.
+sidebar:
+ order: 11
+---
+
+This recipe demonstrates JWT authentication with Echo using the
+[`echo-jwt`](https://github.com/labstack/echo-jwt) middleware:
+
+- JWT authentication using the HS256 algorithm.
+- The token is read from the `Authorization` request header.
+
+See the [JWT middleware](/middleware/jwt/) page for full configuration options.
+
+## Server
+
+### Using custom claims
+
+Define a claims type that embeds `jwt.RegisteredClaims`, then point the middleware
+at it with `NewClaimsFunc`. Inside the restricted handler, retrieve the parsed token
+from the context with the generic `echo.ContextGet`.
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ echojwt "github.com/labstack/echo-jwt/v5"
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+// jwtCustomClaims are custom claims extending default ones.
+// See https://github.com/golang-jwt/jwt for more examples
+type jwtCustomClaims struct {
+ Name string `json:"name"`
+ Admin bool `json:"admin"`
+ jwt.RegisteredClaims
+}
+
+func login(c *echo.Context) error {
+ username := c.FormValue("username")
+ password := c.FormValue("password")
+
+ // Throws unauthorized error
+ if username != "jon" || password != "shhh!" {
+ return echo.ErrUnauthorized
+ }
+
+ // Set custom claims
+ claims := &jwtCustomClaims{
+ "Jon Snow",
+ true,
+ jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
+ },
+ }
+
+ // Create token with claims
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+
+ // Generate encoded token and send it as response.
+ t, err := token.SignedString([]byte("secret"))
+ if err != nil {
+ return err
+ }
+
+ return c.JSON(http.StatusOK, map[string]string{
+ "token": t,
+ })
+}
+
+func accessible(c *echo.Context) error {
+ return c.String(http.StatusOK, "Accessible")
+}
+
+func restricted(c *echo.Context) error {
+ token, err := echo.ContextGet[*jwt.Token](c, "user")
+ if err != nil {
+ return echo.ErrUnauthorized.Wrap(err)
+ }
+ claims := token.Claims.(*jwtCustomClaims)
+ name := claims.Name
+ return c.String(http.StatusOK, "Welcome "+name+"!")
+}
+
+func main() {
+ e := echo.New()
+
+ // Middleware
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ // Login route
+ e.POST("/login", login)
+
+ // Unauthenticated route
+ e.GET("/", accessible)
+
+ // Restricted group
+ r := e.Group("/restricted")
+
+ // Configure middleware with the custom claims type
+ config := echojwt.Config{
+ NewClaimsFunc: func(c *echo.Context) jwt.Claims {
+ return new(jwtCustomClaims)
+ },
+ SigningKey: []byte("secret"),
+ }
+ r.Use(echojwt.WithConfig(config))
+ r.GET("", restricted)
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+### Using a user-defined KeyFunc
+
+When tokens are signed by an external identity provider, supply a `KeyFunc` that
+resolves the signing key dynamically. This example validates tokens issued by
+Google Sign-In by fetching Google's public key set.
+
+```go
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ echojwt "github.com/labstack/echo-jwt/v5"
+
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+ "github.com/lestrrat-go/jwx/v3/jwk"
+)
+
+func getKey(token *jwt.Token) (any, error) {
+
+ // For a demonstration purpose, Google Sign-in is used.
+ // https://developers.google.com/identity/sign-in/web/backend-auth
+ //
+ // This user-defined KeyFunc verifies tokens issued by Google Sign-In.
+ //
+ // Note: In this example, it downloads the keyset every time the restricted route is accessed.
+ keySet, err := jwk.Fetch(context.Background(), "https://www.googleapis.com/oauth2/v3/certs")
+ if err != nil {
+ return nil, err
+ }
+
+ keyID, ok := token.Header["kid"].(string)
+ if !ok {
+ return nil, errors.New("expecting JWT header to have a key ID in the kid field")
+ }
+
+ key, found := keySet.LookupKeyID(keyID)
+
+ if !found {
+ return nil, fmt.Errorf("unable to find key %q", keyID)
+ }
+
+ return key.PublicKey()
+}
+
+func accessible(c *echo.Context) error {
+ return c.String(http.StatusOK, "Accessible")
+}
+
+func restricted(c *echo.Context) error {
+ user, err := echo.ContextGet[*jwt.Token](c, "user")
+ if err != nil {
+ return echo.ErrUnauthorized.Wrap(err)
+ }
+ claims := user.Claims.(jwt.MapClaims)
+ name := claims["name"].(string)
+ return c.String(http.StatusOK, "Welcome "+name+"!")
+}
+
+func main() {
+ e := echo.New()
+
+ // Middleware
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ // Unauthenticated route
+ e.GET("/", accessible)
+
+ // Restricted group
+ r := e.Group("/restricted")
+ {
+ config := echojwt.Config{
+ KeyFunc: getKey,
+ }
+ r.Use(echojwt.WithConfig(config))
+ r.GET("", restricted)
+ }
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+:::caution
+Fetching the key set on every request, as shown above, is for demonstration only.
+In production, cache the key set and refresh it periodically.
+:::
+
+## Client
+
+### Login
+
+Log in with a username and password to retrieve a token.
+
+```sh
+curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login
+```
+
+Response:
+
+```js
+{
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
+}
+```
+
+### Request
+
+Request a restricted resource using the token in the `Authorization` request header.
+
+```sh
+curl localhost:1323/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
+```
+
+Response:
+
+```sh
+Welcome Jon Snow!
+```
diff --git a/site/src/content/docs/cookbook/load-balancing.md b/site/src/content/docs/cookbook/load-balancing.md
new file mode 100644
index 00000000..74be0c50
--- /dev/null
+++ b/site/src/content/docs/cookbook/load-balancing.md
@@ -0,0 +1,117 @@
+---
+title: Load Balancing
+description: Use Nginx as a reverse proxy to load balance traffic across multiple Echo servers.
+sidebar:
+ order: 20
+---
+
+This recipe demonstrates how to use Nginx as a reverse proxy server to load
+balance traffic across multiple Echo servers.
+
+## Echo
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+var index = `
+
+
+
+
+
+
+ Upstream Server
+
+
+
+
+ Hello from upstream server %s
+
+
+
+`
+
+func main() {
+ name := os.Args[1]
+ port := os.Args[2]
+
+ e := echo.New()
+ e.Use(middleware.Recover())
+ e.Use(middleware.RequestLogger())
+
+ e.GET("/", func(c *echo.Context) error {
+ return c.HTML(http.StatusOK, fmt.Sprintf(index, name))
+ })
+
+ sc := echo.StartConfig{Address: port}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+### Start servers
+
+```sh
+cd upstream
+go run server.go server1 :8081
+go run server.go server2 :8082
+```
+
+## Nginx
+
+### 1) Install Nginx
+
+See the [Nginx installation guide](https://www.nginx.com/resources/wiki/start/topics/tutorials/install).
+
+### 2) Configure Nginx
+
+Create a file `/etc/nginx/sites-enabled/localhost` with the following content:
+
+```nginx
+upstream localhost {
+ server localhost:8081;
+ server localhost:8082;
+}
+
+server {
+ listen 8080;
+ server_name localhost;
+ access_log /var/log/nginx/localhost.access.log combined;
+
+ location / {
+ proxy_pass http://localhost;
+ }
+}
+```
+
+:::note
+Adjust `listen`, `server_name`, and `access_log` to suit your environment.
+:::
+
+### 3) Restart Nginx
+
+```sh
+service nginx restart
+```
+
+Browse to `https://localhost:8080`, and you should see a webpage served from
+either "server 1" or "server 2".
+
+```sh
+Hello from upstream server server1
+```
diff --git a/site/src/content/docs/cookbook/middleware.md b/site/src/content/docs/cookbook/middleware.md
new file mode 100644
index 00000000..9217f341
--- /dev/null
+++ b/site/src/content/docs/cookbook/middleware.md
@@ -0,0 +1,140 @@
+---
+title: Custom Middleware
+description: Write custom Echo middleware to collect request statistics and set response headers.
+sidebar:
+ order: 12
+---
+
+This recipe shows how to write custom middleware:
+
+- A middleware that collects the request count, response statuses, and uptime.
+- A middleware that writes a custom `Server` header to every response.
+
+A middleware in Echo is a function with the signature
+`func(next echo.HandlerFunc) echo.HandlerFunc`. The `Stats.Process` method below
+satisfies that signature directly, while `ServerHeader` is a plain function.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/labstack/echo/v5"
+)
+
+type (
+ Stats struct {
+ Uptime time.Time `json:"uptime"`
+ RequestCount uint64 `json:"requestCount"`
+ Statuses map[int]uint64 `json:"statuses"`
+ mutex sync.RWMutex
+ }
+)
+
+func NewStats() *Stats {
+ return &Stats{
+ Uptime: time.Now(),
+ Statuses: map[int]uint64{},
+ }
+}
+
+// Process is the middleware function.
+func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+ err := next(c)
+
+ status := http.StatusInternalServerError
+ if err != nil {
+ var sc echo.HTTPStatusCoder
+ if ok := errors.As(err, &sc); ok {
+ status = sc.StatusCode()
+ }
+ } else {
+ rw, uErr := echo.UnwrapResponse(c.Response())
+ if uErr == nil {
+ status = rw.Status
+ }
+ err = uErr
+ }
+
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ s.RequestCount++
+ s.Statuses[status]++
+
+ return err
+ }
+}
+
+// Handle is the endpoint to get stats.
+func (s *Stats) Handle(c *echo.Context) error {
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+ return c.JSON(http.StatusOK, s)
+}
+
+// ServerHeader middleware adds a `Server` header to the response.
+func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+ c.Response().Header().Set(echo.HeaderServer, "Echo/5.0")
+ return next(c)
+ }
+}
+
+func main() {
+ e := echo.New()
+
+ //-------------------
+ // Custom middleware
+ //-------------------
+ // Stats
+ s := NewStats()
+ e.Use(s.Process)
+ e.GET("/stats", s.Handle) // Endpoint to get stats
+
+ // Server header
+ e.Use(ServerHeader)
+
+ // Handler
+ e.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Hello, World!")
+ })
+
+ // Start server
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Response
+
+### Headers
+
+```sh
+Content-Length:122
+Content-Type:application/json; charset=utf-8
+Date:Thu, 14 Apr 2016 20:31:46 GMT
+Server:Echo/5.0
+```
+
+### Body
+
+```js
+{
+ "uptime": "2016-04-14T13:28:48.486548936-07:00",
+ "requestCount": 5,
+ "statuses": {
+ "200": 4,
+ "404": 1
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/reverse-proxy.md b/site/src/content/docs/cookbook/reverse-proxy.md
new file mode 100644
index 00000000..d4650fa0
--- /dev/null
+++ b/site/src/content/docs/cookbook/reverse-proxy.md
@@ -0,0 +1,202 @@
+---
+title: Reverse Proxy
+description: Use Echo as a reverse proxy and load balancer in front of upstream applications.
+sidebar:
+ order: 19
+---
+
+This recipe demonstrates how to use Echo as a reverse proxy and load balancer in
+front of your applications, such as WordPress, Node.js, Java, Python, Ruby, or Go.
+For simplicity, the upstreams here are Go servers that also handle WebSocket.
+
+## 1) Identify upstream target URL(s)
+
+```go
+url1, err := url.Parse("http://localhost:8081")
+if err != nil {
+ e.Logger.Error("failed parse url", "error", err)
+}
+url2, err := url.Parse("http://localhost:8082")
+if err != nil {
+ e.Logger.Error("failed parse url", "error", err)
+}
+targets := []*middleware.ProxyTarget{
+ {
+ URL: url1,
+ },
+ {
+ URL: url2,
+ },
+}
+```
+
+## 2) Set up proxy middleware with upstream targets
+
+The snippet below uses round-robin load balancing. You can also use
+`middleware.NewRandomBalancer()`.
+
+```go
+e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))
+```
+
+To set up a proxy for a sub-route, use `Echo#Group()`.
+
+```go
+g := e.Group("/blog")
+g.Use(middleware.Proxy(...))
+```
+
+## 3) Start upstream servers
+
+```sh
+cd upstream
+go run server.go server1 :8081
+go run server.go server2 :8082
+```
+
+## 4) Start the proxy server
+
+```sh
+go run server.go
+```
+
+Browse to `http://localhost:1323`, and you should see a webpage with an HTTP
+request served from "server 1" and a WebSocket request served from "server 2".
+
+```sh
+HTTP
+
+Hello from upstream server server1
+
+WebSocket
+
+Hello from upstream server server2!
+Hello from upstream server server2!
+Hello from upstream server server2!
+```
+
+## Source code
+
+### Upstream server
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+ "golang.org/x/net/websocket"
+)
+
+var index = `
+
+
+
+
+
+
+ Upstream Server
+
+
+
+
HTTP
+
+ Hello from upstream server %s
+
+
WebSocket
+
+
+
+
+`
+
+func main() {
+ name := os.Args[1]
+ port := os.Args[2]
+ e := echo.New()
+
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ e.GET("/", func(c *echo.Context) error {
+ return c.HTML(http.StatusOK, fmt.Sprintf(index, name))
+ })
+
+ // WebSocket handler
+ e.GET("/ws", func(c *echo.Context) error {
+ websocket.Handler(func(ws *websocket.Conn) {
+ defer ws.Close()
+ for {
+ // Write
+ err := websocket.Message.Send(ws, fmt.Sprintf("Hello from upstream server %s!", name))
+ if err != nil {
+ e.Logger.Error("failed to send message", "error", err)
+ }
+ select {
+ case <-ws.Request().Context().Done():
+ return
+ case <-time.After(1 * time.Second):
+ continue
+ }
+ }
+ }).ServeHTTP(c.Response(), c.Request())
+ return nil
+ })
+
+ sc := echo.StartConfig{Address: port}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+### Proxy server
+
+```go
+package main
+
+import (
+ "context"
+ "net/url"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ e := echo.New()
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ // Setup proxy
+ url1, _ := url.Parse("http://localhost:8081")
+ url2, _ := url.Parse("http://localhost:8082")
+ targets := []*middleware.ProxyTarget{
+ {URL: url1},
+ {URL: url2},
+ }
+ e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/sse.md b/site/src/content/docs/cookbook/sse.md
new file mode 100644
index 00000000..2227f4af
--- /dev/null
+++ b/site/src/content/docs/cookbook/sse.md
@@ -0,0 +1,305 @@
+---
+title: Server-Sent Events (SSE)
+description: Stream server-sent events from an Echo handler, either per connection or broadcast to many clients.
+sidebar:
+ order: 14
+---
+
+[Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)
+can be used in several ways. The first example below is per-connection, per-handler
+SSE. For more complex broadcasting logic, see the second example using
+[r3labs/sse](https://github.com/r3labs/sse).
+
+:::caution
+SSE connections are long-lived, so the server's write timeout must be disabled.
+Both examples set `s.WriteTimeout = 0` via `BeforeServeFunc`.
+:::
+
+## Using SSE
+
+### Server
+
+The handler writes the SSE headers, then emits an event every second until the
+client disconnects. `http.NewResponseController(w).Flush()` pushes each event to
+the client immediately.
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ e := echo.New()
+
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+ e.File("/", "./index.html")
+
+ e.GET("/sse", func(c *echo.Context) error {
+ log.Printf("SSE client connected, ip: %v", c.RealIP())
+
+ w := c.Response()
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+
+ ticker := time.NewTicker(1 * time.Second)
+ defer ticker.Stop()
+ count := uint64(0)
+ for {
+ select {
+ case <-c.Request().Context().Done():
+ log.Printf("SSE client disconnected, ip: %v", c.RealIP())
+ return nil
+ case <-ticker.C:
+ count++
+ event := Event{
+ Data: []byte(fmt.Sprintf("count: %d, time: %s\n\n", count, time.Now().Format(time.RFC3339Nano))),
+ }
+ if err := event.MarshalTo(w); err != nil {
+ return err
+ }
+ if err := http.NewResponseController(w).Flush(); err != nil {
+ return err
+ }
+ }
+ }
+ })
+
+ sc := echo.StartConfig{
+ Address: ":8080",
+ BeforeServeFunc: func(s *http.Server) error {
+ s.WriteTimeout = 0 // IMPORTANT: disable for SSE
+ return nil
+ },
+ }
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c
+ defer cancel()
+
+ if err := sc.Start(ctx, e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+### Event structure and Marshal method
+
+```go
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+)
+
+// Event represents Server-Sent Event.
+// SSE explanation: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
+type Event struct {
+ // ID is used to set the EventSource object's last event ID value.
+ ID []byte
+ // Data field is for the message. When the EventSource receives multiple consecutive lines
+ // that begin with data:, it concatenates them, inserting a newline character between each one.
+ // Trailing newlines are removed.
+ Data []byte
+ // Event is a string identifying the type of event described. If this is specified, an event
+ // will be dispatched on the browser to the listener for the specified event name; the website
+ // source code should use addEventListener() to listen for named events. The onmessage handler
+ // is called if no event name is specified for a message.
+ Event []byte
+ // Retry is the reconnection time. If the connection to the server is lost, the browser will
+ // wait for the specified time before attempting to reconnect. This must be an integer, specifying
+ // the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored.
+ Retry []byte
+ // Comment line can be used to prevent connections from timing out; a server can send a comment
+ // periodically to keep the connection alive.
+ Comment []byte
+}
+
+// MarshalTo marshals Event to given Writer
+func (ev *Event) MarshalTo(w io.Writer) error {
+ // Marshalling part is taken from: https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/http.go#L16
+ if len(ev.Data) == 0 && len(ev.Comment) == 0 {
+ return nil
+ }
+
+ if len(ev.Data) > 0 {
+ if _, err := fmt.Fprintf(w, "id: %s\n", ev.ID); err != nil {
+ return err
+ }
+
+ sd := bytes.Split(ev.Data, []byte("\n"))
+ for i := range sd {
+ if _, err := fmt.Fprintf(w, "data: %s\n", sd[i]); err != nil {
+ return err
+ }
+ }
+
+ if len(ev.Event) > 0 {
+ if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil {
+ return err
+ }
+ }
+
+ if len(ev.Retry) > 0 {
+ if _, err := fmt.Fprintf(w, "retry: %s\n", ev.Retry); err != nil {
+ return err
+ }
+ }
+ }
+
+ if len(ev.Comment) > 0 {
+ if _, err := fmt.Fprintf(w, ": %s\n", ev.Comment); err != nil {
+ return err
+ }
+ }
+
+ if _, err := fmt.Fprint(w, "\n"); err != nil {
+ return err
+ }
+
+ return nil
+}
+```
+
+### HTML serving SSE
+
+```html
+
+
+
+
+
Getting server-sent updates
+
+
+
+
+
+
+```
+
+## Broadcasting with r3labs/sse
+
+When you need to broadcast a single stream of events to many subscribers, the
+[r3labs/sse](https://github.com/r3labs/sse) library handles stream and subscriber
+management for you.
+
+### Server
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+ "github.com/r3labs/sse/v2"
+)
+
+func main() {
+ e := echo.New()
+
+ server := sse.New() // create SSE broadcaster server
+ server.AutoReplay = false // do not replay messages for each new subscriber that connects
+ _ = server.CreateStream("time") // EventSource in "index.html" connecting to stream named "time"
+
+ go func(s *sse.Server) {
+ ticker := time.NewTicker(1 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ s.Publish("time", &sse.Event{
+ Data: []byte("time: " + time.Now().Format(time.RFC3339Nano)),
+ })
+ }
+ }
+ }(server)
+
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+ e.File("/", "./index.html")
+
+ //e.GET("/sse", echo.WrapHandler(server))
+
+ e.GET("/sse", func(c *echo.Context) error { // longer variant with disconnect logic
+ e.Logger.Info("New client connected", "ip", c.RealIP())
+ go func() {
+ <-c.Request().Context().Done() // Received Browser Disconnection
+ e.Logger.Info("Client disconnected", "ip", c.RealIP())
+ }()
+
+ server.ServeHTTP(c.Response(), c.Request())
+ return nil
+ })
+
+ sc := echo.StartConfig{
+ Address: ":8080",
+ BeforeServeFunc: func(s *http.Server) error {
+ s.WriteTimeout = 0 // IMPORTANT: disable for SSE
+ return nil
+ },
+ }
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c
+ defer cancel()
+
+ if err := sc.Start(ctx, e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+### HTML serving SSE
+
+```html
+
+
+
+
+
Getting server-sent updates
+
+
+
+
+
+
+```
diff --git a/site/src/content/docs/cookbook/streaming-response.md b/site/src/content/docs/cookbook/streaming-response.md
new file mode 100644
index 00000000..98c10613
--- /dev/null
+++ b/site/src/content/docs/cookbook/streaming-response.md
@@ -0,0 +1,95 @@
+---
+title: Streaming Response
+description: Stream data to the client as it is produced using chunked transfer encoding.
+sidebar:
+ order: 15
+---
+
+This recipe streams a JSON response to the client as each record is produced,
+using chunked transfer encoding:
+
+- Send data as it is produced.
+- Stream a JSON response with chunked transfer encoding.
+
+The handler encodes one record at a time and calls
+`http.NewResponseController(...).Flush()` after each to push it to the client
+immediately, pausing one second between records.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "github.com/labstack/echo/v5"
+)
+
+type (
+ Geolocation struct {
+ Altitude float64
+ Latitude float64
+ Longitude float64
+ }
+)
+
+var (
+ locations = []Geolocation{
+ {-97, 37.819929, -122.478255},
+ {1899, 39.096849, -120.032351},
+ {2619, 37.865101, -119.538329},
+ {42, 33.812092, -117.918974},
+ {15, 37.77493, -122.419416},
+ }
+)
+
+func main() {
+ e := echo.New()
+ e.GET("/", func(c *echo.Context) error {
+ c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ c.Response().WriteHeader(http.StatusOK)
+
+ enc := json.NewEncoder(c.Response())
+ for _, l := range locations {
+ if err := enc.Encode(l); err != nil {
+ return err
+ }
+ if err := http.NewResponseController(c.Response()).Flush(); err != nil {
+ return err
+ }
+ select {
+ case <-c.Request().Context().Done():
+ return nil
+ case <-time.After(1 * time.Second):
+ continue
+ }
+ }
+ return nil
+ })
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Client
+
+```sh
+curl localhost:1323
+```
+
+### Output
+
+```js
+{"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255}
+{"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351}
+{"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329}
+{"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974}
+{"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416}
+```
diff --git a/site/src/content/docs/cookbook/subdomain.md b/site/src/content/docs/cookbook/subdomain.md
new file mode 100644
index 00000000..050beab0
--- /dev/null
+++ b/site/src/content/docs/cookbook/subdomain.md
@@ -0,0 +1,78 @@
+---
+title: Subdomain
+description: Route requests to different Echo instances per host using a virtual host handler.
+sidebar:
+ order: 17
+---
+
+This recipe routes requests to separate `Echo` instances based on the request
+host, so each subdomain has its own routes and middleware. The instances are
+combined with `echo.NewVirtualHostHandler`, which dispatches by host name.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ // Hosts
+ vHosts := make(map[string]*echo.Echo)
+
+ //-----
+ // API
+ //-----
+
+ api := echo.New()
+ api.Use(middleware.RequestLogger())
+ api.Use(middleware.Recover())
+
+ vHosts["api.localhost:1323"] = api
+
+ api.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "API")
+ })
+
+ //------
+ // Blog
+ //------
+
+ blog := echo.New()
+ blog.Use(middleware.RequestLogger())
+ blog.Use(middleware.Recover())
+
+ vHosts["blog.localhost:1323"] = blog
+
+ blog.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Blog")
+ })
+
+ //---------
+ // Website
+ //---------
+
+ site := echo.New()
+ site.Use(middleware.RequestLogger())
+ site.Use(middleware.Recover())
+
+ vHosts["localhost:1323"] = site
+
+ site.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Website")
+ })
+
+ e := echo.NewVirtualHostHandler(vHosts)
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/timeout.md b/site/src/content/docs/cookbook/timeout.md
new file mode 100644
index 00000000..df8f8a95
--- /dev/null
+++ b/site/src/content/docs/cookbook/timeout.md
@@ -0,0 +1,53 @@
+---
+title: Timeout
+description: Apply a request timeout to handlers with the ContextTimeout middleware.
+sidebar:
+ order: 18
+---
+
+The [`ContextTimeout`](/middleware/context-timeout/) middleware sets a deadline on the
+request's `context.Context`. When the deadline passes, the context is cancelled,
+and handlers that watch `c.Request().Context().Done()` can return promptly instead
+of running to completion.
+
+In the example below the middleware imposes a 5-second timeout while the handler
+would otherwise take 10 seconds, so the request returns a `408 Request Timeout`.
+
+## Server
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ // Echo instance
+ e := echo.New()
+
+ // Middleware
+ e.Use(middleware.ContextTimeout(5 * time.Second))
+
+ // Route => handler
+ e.GET("/", func(c *echo.Context) error {
+ select {
+ case <-c.Request().Context().Done():
+ return echo.NewHTTPError(http.StatusRequestTimeout, "Request timed out")
+ case <-time.After(10 * time.Second):
+ return c.String(http.StatusOK, "Hello, World!\n")
+ }
+ })
+
+ // Start server
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/cookbook/websocket.md b/site/src/content/docs/cookbook/websocket.md
new file mode 100644
index 00000000..06bcab36
--- /dev/null
+++ b/site/src/content/docs/cookbook/websocket.md
@@ -0,0 +1,189 @@
+---
+title: WebSocket
+description: Handle WebSocket connections in Echo using golang.org/x/net/websocket or gorilla/websocket.
+sidebar:
+ order: 16
+---
+
+Echo handlers can serve WebSocket connections by upgrading the underlying
+HTTP connection. This recipe shows two approaches: the standard
+`golang.org/x/net/websocket` package and the popular
+[`gorilla/websocket`](https://github.com/gorilla/websocket) library.
+
+## Using net WebSocket
+
+### Server
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+ "golang.org/x/net/websocket"
+)
+
+func hello(c *echo.Context) error {
+ websocket.Handler(func(ws *websocket.Conn) {
+ defer ws.Close()
+ for {
+ // Write
+ if err := websocket.Message.Send(ws, "Hello, Client!"); err != nil {
+ c.Logger().Error("failed to write WS message", "error", err)
+ }
+
+ // Read
+ msg := ""
+ if err := websocket.Message.Receive(ws, &msg); err != nil {
+ c.Logger().Error("failed to write WS message", "error", err)
+ }
+ fmt.Printf("%s\n", msg)
+ }
+ }).ServeHTTP(c.Response(), c.Request())
+ return nil
+}
+
+func main() {
+ e := echo.New()
+
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ e.Static("/", "../public")
+ e.GET("/ws", hello)
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Using gorilla WebSocket
+
+### Server
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/gorilla/websocket"
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+var (
+ upgrader = websocket.Upgrader{}
+)
+
+func hello(c *echo.Context) error {
+ ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
+ if err != nil {
+ return err
+ }
+ defer ws.Close()
+
+ for {
+ // Write
+ err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!"))
+ if err != nil {
+ c.Logger().Error("failed to write WS message", "error", err)
+ }
+
+ // Read
+ _, msg, err := ws.ReadMessage()
+ if err != nil {
+ c.Logger().Error("failed to read WS message", "error", err)
+ }
+ fmt.Printf("%s\n", msg)
+ }
+}
+
+func main() {
+ e := echo.New()
+
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ e.Static("/", "../public")
+
+ e.GET("/ws", hello)
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Client
+
+```html
+
+
+
+
+
+ WebSocket
+
+
+
+
+
+
+
+
+
+```
+
+## Output
+
+**Server**
+
+```sh
+Hello, Server!
+Hello, Server!
+Hello, Server!
+Hello, Server!
+Hello, Server!
+```
+
+**Client**
+
+```sh
+Hello, Client!
+Hello, Client!
+Hello, Client!
+Hello, Client!
+Hello, Client!
+```
diff --git a/site/src/content/docs/guide/binding.md b/site/src/content/docs/guide/binding.md
new file mode 100644
index 00000000..894cd411
--- /dev/null
+++ b/site/src/content/docs/guide/binding.md
@@ -0,0 +1,151 @@
+---
+title: Binding
+description: Parse request data into typed Go structs from path, query, header, and body.
+sidebar:
+ order: 5
+---
+
+Parsing request data is a crucial part of a web application. In Echo this is called
+_binding_, and it can read from four parts of an HTTP request:
+
+- URL path parameters
+- URL query parameters
+- Headers
+- Request body
+
+## Struct tag binding
+
+Define a struct with tags specifying the data source and key, then call `c.Bind()`
+with a pointer to it. Here the query parameter `id` binds to the `ID` field:
+
+```go
+type User struct {
+ ID string `query:"id"`
+}
+
+// handler for /users?id=
+var user User
+if err := c.Bind(&user); err != nil {
+ return c.String(http.StatusBadRequest, "bad request")
+}
+```
+
+### Data sources
+
+| Tag | Source |
+| -------- | ------ |
+| `query` | Query parameter |
+| `param` | Path parameter |
+| `header` | Header value |
+| `form` | Form data (query + body) |
+| `json` | Request body (`encoding/json`) |
+| `xml` | Request body (`encoding/xml`) |
+
+Path, query, header, and form fields require an **explicit tag**. JSON and XML fall
+back to the struct field name when the tag is omitted, matching the standard library.
+
+### Body content types
+
+When decoding the request body, the `Content-Type` header selects the decoder:
+
+- `application/json`
+- `application/xml`
+- `application/x-www-form-urlencoded`
+
+### Multiple sources & precedence
+
+A field can declare several sources. Data is bound in this order, each step
+overwriting the previous:
+
+1. Path parameters
+2. Query parameters (GET / DELETE only)
+3. Request body
+
+```go
+type User struct {
+ ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"`
+}
+```
+
+### Direct binding from one source
+
+```go
+echo.BindBody(c, &payload) // request body
+echo.BindQueryParams(c, &payload) // query parameters
+echo.BindPathValues(c, &payload) // path parameters
+echo.BindHeaders(c, &payload) // headers
+```
+
+:::note
+Headers are **not** included by `c.Bind()`. Bind them with `echo.BindHeaders` directly.
+:::
+
+:::caution[Security]
+Don't bind directly into business structs. If a bound struct exposed an `IsAdmin bool`
+field, a request body of `{"IsAdmin": true}` would set it. Use a dedicated DTO and map
+it explicitly:
+:::
+
+```go
+type UserDTO struct {
+ Name string `json:"name" form:"name" query:"name"`
+ Email string `json:"email" form:"email" query:"email"`
+}
+
+e.POST("/users", func(c *echo.Context) error {
+ var dto UserDTO
+ if err := c.Bind(&dto); err != nil {
+ return c.String(http.StatusBadRequest, "bad request")
+ }
+ user := User{Name: dto.Name, Email: dto.Email, IsAdmin: false}
+ executeSomeBusinessLogic(user)
+ return c.JSON(http.StatusOK, user)
+})
+```
+
+## Fluent binding
+
+For explicit, type-safe binding from a single source, use the fluent binders. They
+chain configuration and execute, collecting errors:
+
+```go
+// /api/search?active=true&id=1&id=2&id=3&length=25
+var opts struct {
+ IDs []int64
+ Active bool
+}
+length := int64(50)
+
+err := echo.QueryParamsBinder(c).
+ Int64("length", &length).
+ Int64s("id", &opts.IDs).
+ Bool("active", &opts.Active).
+ BindError() // first error, if any
+```
+
+Available binders: `echo.QueryParamsBinder(c)`, `echo.PathValuesBinder(c)`,
+`echo.FormFieldBinder(c)`. End a chain with `BindError()` (first error) or
+`BindErrors()` (all errors). `FailFast(false)` runs the whole chain; it's on by default.
+
+Each supported type offers `Type(...)`, `MustType(...)`, `Types(...)` (slices), and
+`MustTypes(...)` methods — e.g. `Int64`, `MustInt64`, `Int64s`. Use
+`BindWithDelimiter("id", &dest, ",")` to split comma-joined values.
+
+## Custom binder
+
+Register a custom binder via `Echo#Binder`:
+
+```go
+type CustomBinder struct{}
+
+func (cb *CustomBinder) Bind(c *echo.Context, i any) error {
+ db := new(echo.DefaultBinder)
+ if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType {
+ return err
+ }
+ // custom logic here
+ return nil
+}
+
+e.Binder = &CustomBinder{}
+```
diff --git a/site/src/content/docs/guide/context.md b/site/src/content/docs/guide/context.md
new file mode 100644
index 00000000..2b4699d3
--- /dev/null
+++ b/site/src/content/docs/guide/context.md
@@ -0,0 +1,78 @@
+---
+title: Context
+description: The per-request object carrying the request, response, params, and helpers.
+sidebar:
+ order: 4
+---
+
+`echo.Context` represents the context of the current HTTP request. A pointer to it
+(`*echo.Context`) is passed to every handler and middleware, carrying the request and
+response, path parameters, bound data, and helpers for building responses.
+
+```go
+func handler(c *echo.Context) error {
+ // ...
+ return nil
+}
+```
+
+## Reading input
+
+```go
+id := c.Param("id") // path parameter
+q := c.QueryParam("q") // query string value
+all := c.QueryParams() // url.Values of all query params
+name := c.FormValue("name") // form field (URL + body)
+ua := c.Request().Header.Get(echo.HeaderUserAgent)
+```
+
+There are matching `*Or` helpers that return a default when a value is absent —
+`c.ParamOr("id", "0")`, `c.QueryParamOr("page", "1")`, `c.FormValueOr(...)`.
+
+## Writing responses
+
+```go
+c.String(http.StatusOK, "plain text")
+c.JSON(http.StatusOK, payload)
+c.JSONPretty(http.StatusOK, payload, " ")
+c.HTML(http.StatusOK, "hi")
+c.XML(http.StatusOK, payload)
+c.Blob(http.StatusOK, "application/pdf", bytes)
+c.Stream(http.StatusOK, "application/octet-stream", reader)
+c.NoContent(http.StatusNoContent)
+c.Redirect(http.StatusFound, "/elsewhere")
+```
+
+## Files
+
+```go
+c.File("public/report.pdf") // serve a file
+c.Attachment("invoice.pdf", "inv.pdf") // prompt download
+c.Inline("photo.png", "photo.png") // render inline
+```
+
+## Per-request storage
+
+Share data between middleware and handlers with `Get`/`Set`:
+
+```go
+c.Set("user", u)
+u, _ := c.Get("user").(*User)
+```
+
+Typed access is available via the generics helpers:
+
+```go
+u, err := echo.ContextGet[*User](c, "user")
+```
+
+## Binding & validation
+
+`c.Bind()` parses request data into a struct; see [Binding](/guide/binding/).
+
+```go
+var dto CreateUser
+if err := c.Bind(&dto); err != nil {
+ return echo.ErrBadRequest
+}
+```
diff --git a/site/src/content/docs/guide/cookies.md b/site/src/content/docs/guide/cookies.md
new file mode 100644
index 00000000..fec956f9
--- /dev/null
+++ b/site/src/content/docs/guide/cookies.md
@@ -0,0 +1,72 @@
+---
+title: Cookies
+description: Create, read, and list HTTP cookies using the standard http.Cookie type.
+sidebar:
+ order: 11
+---
+
+A cookie is a small piece of data a server sends to the browser, which the browser
+stores and sends back on subsequent requests. Cookies let websites remember stateful
+information such as a shopping cart, authentication state, or previously entered form
+values.
+
+Echo uses Go's standard `http.Cookie` type to add and retrieve cookies from the
+`echo.Context` in a handler.
+
+## Cookie attributes
+
+| Attribute | Optional |
+| ---------- | -------- |
+| `Name` | No |
+| `Value` | No |
+| `Path` | Yes |
+| `Domain` | Yes |
+| `Expires` | Yes |
+| `Secure` | Yes |
+| `HttpOnly` | Yes |
+
+## Create a cookie
+
+```go
+func writeCookie(c *echo.Context) error {
+ cookie := new(http.Cookie)
+ cookie.Name = "username"
+ cookie.Value = "jon"
+ cookie.Expires = time.Now().Add(24 * time.Hour)
+ c.SetCookie(cookie)
+ return c.String(http.StatusOK, "write a cookie")
+}
+```
+
+- Create the cookie with `new(http.Cookie)`.
+- Set attributes on the `http.Cookie` fields.
+- Call `c.SetCookie(cookie)` to add a `Set-Cookie` header to the response.
+
+## Read a cookie
+
+```go
+func readCookie(c *echo.Context) error {
+ cookie, err := c.Cookie("username")
+ if err != nil {
+ return err
+ }
+ fmt.Println(cookie.Name)
+ fmt.Println(cookie.Value)
+ return c.String(http.StatusOK, "read a cookie")
+}
+```
+
+- Read a cookie by name with `c.Cookie("username")`.
+- Access its attributes through the `http.Cookie` fields.
+
+## Read all cookies
+
+```go
+func readAllCookies(c *echo.Context) error {
+ for _, cookie := range c.Cookies() {
+ fmt.Println(cookie.Name)
+ fmt.Println(cookie.Value)
+ }
+ return c.String(http.StatusOK, "read all the cookies")
+}
+```
diff --git a/site/src/content/docs/guide/customization.md b/site/src/content/docs/guide/customization.md
new file mode 100644
index 00000000..72e98796
--- /dev/null
+++ b/site/src/content/docs/guide/customization.md
@@ -0,0 +1,63 @@
+---
+title: Customization
+description: Customize Echo's logger, validator, binder, renderer, serializer, and error handling.
+sidebar:
+ order: 12
+---
+
+Echo exposes a set of fields on the `Echo` instance that let you replace built-in
+behavior with your own implementations.
+
+## Logging
+
+`Echo#Logger` writes structured logs. The default handler emits JSON to `os.Stdout`.
+
+### Custom logger
+
+The logger is an `*slog.Logger`, so you can register any `slog` handler:
+
+```go
+e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
+```
+
+## Validator
+
+`Echo#Validator` registers a validator for request payload validation.
+
+[Learn more](/guide/request/#validate-data)
+
+## Custom binder
+
+`Echo#Binder` registers a custom binder for binding request payloads.
+
+[Learn more](/guide/binding/#custom-binder)
+
+## Custom JSON serializer
+
+`Echo#JSONSerializer` registers a custom JSON serializer. See `DefaultJSONSerializer`
+in [json.go](https://github.com/labstack/echo/blob/master/json.go).
+
+## Renderer
+
+`Echo#Renderer` registers a renderer for template rendering.
+
+[Learn more](/guide/templates/)
+
+## HTTP error handler
+
+`Echo#HTTPErrorHandler` registers a custom HTTP error handler.
+
+[Learn more](/guide/error-handling/)
+
+## Route callback
+
+`Echo#OnAddRoute` registers a callback invoked whenever a new route is added to the
+router.
+
+## IP extractor
+
+`Echo#IPExtractor` controls how the real client IP address is determined. To
+retrieve it reliably and securely, your application must be aware of your entire
+infrastructure.
+
+[Learn more](/guide/ip-address/)
diff --git a/site/src/content/docs/guide/error-handling.md b/site/src/content/docs/guide/error-handling.md
new file mode 100644
index 00000000..f754fe72
--- /dev/null
+++ b/site/src/content/docs/guide/error-handling.md
@@ -0,0 +1,82 @@
+---
+title: Error Handling
+description: Centralized HTTP error handling by returning errors from handlers and middleware.
+sidebar:
+ order: 6
+---
+
+Echo advocates **centralized** error handling: handlers and middleware return an
+`error`, and a single error handler turns it into an HTTP response. This keeps logging
+and response formatting in one place.
+
+Return a plain `error` or an `*echo.HTTPError`:
+
+```go
+e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+ if !authenticated(c) {
+ // invalid credentials → abort with 401
+ return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")
+ }
+ return next(c)
+ }
+})
+```
+
+`echo.NewHTTPError(code)` without a message uses the status text (e.g. `"Unauthorized"`).
+Echo also ships sentinel errors like `echo.ErrBadRequest`, `echo.ErrNotFound`, and
+`echo.ErrUnauthorized`.
+
+## Default error handler
+
+Echo's default handler responds in JSON:
+
+```json
+{ "message": "error connecting to redis" }
+```
+
+A plain `error` becomes `500 Internal Server Error` (the original message is included
+when running with errors exposed). An `*HTTPError` uses its status code and message.
+
+## Custom error handler
+
+Set your own via `e.HTTPErrorHandler` — useful for error pages, notifications, or
+sending errors to a centralized system.
+
+Check whether the response was already sent with `echo.UnwrapResponse()`, and find a
+status code in the error chain via `echo.HTTPStatusCoder`:
+
+```go
+func customHTTPErrorHandler(c *echo.Context, err error) {
+ if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil {
+ if resp.Committed {
+ return // already sent by a handler/middleware
+ }
+ }
+
+ code := http.StatusInternalServerError
+ var sc echo.HTTPStatusCoder
+ if errors.As(err, &sc) {
+ if tmp := sc.StatusCode(); tmp != 0 {
+ code = tmp
+ }
+ }
+
+ var cErr error
+ if c.Request().Method == http.MethodHead {
+ cErr = c.NoContent(code)
+ } else {
+ cErr = c.File(fmt.Sprintf("%d.html", code)) // e.g. 404.html, 500.html
+ }
+ if cErr != nil {
+ c.Logger().Error("failed to send error page", "error", errors.Join(err, cErr))
+ }
+}
+
+e.HTTPErrorHandler = customHTTPErrorHandler
+```
+
+:::tip
+Instead of (or in addition to) the logger, forward errors to an external service like
+Sentry, Elasticsearch, or Splunk from the central handler.
+:::
diff --git a/site/src/content/docs/guide/installation.md b/site/src/content/docs/guide/installation.md
new file mode 100644
index 00000000..e8a47883
--- /dev/null
+++ b/site/src/content/docs/guide/installation.md
@@ -0,0 +1,57 @@
+---
+title: Installation
+description: Add Echo v5 to your Go module.
+sidebar:
+ order: 2
+---
+
+Echo is distributed as a Go module: `github.com/labstack/echo/v5`.
+
+## Requirements
+
+Echo v5 requires **Go 1.25 or newer**.
+
+```bash
+go version
+```
+
+## Add to a project
+
+Inside an existing module:
+
+```bash
+go get github.com/labstack/echo/v5
+```
+
+Or start a new module:
+
+```bash
+mkdir myapp && cd myapp
+go mod init myapp
+go get github.com/labstack/echo/v5
+```
+
+Import it in your code:
+
+```go
+import "github.com/labstack/echo/v5"
+```
+
+## Versions
+
+| Version | Import path | Status |
+| ------- | ------------------------------- | ------ |
+| **v5** | `github.com/labstack/echo/v5` | Current |
+| v4 | `github.com/labstack/echo/v4` | LTS (maintenance) |
+
+:::note
+Echo follows [semantic import versioning](https://go.dev/blog/v2-go-modules) — the
+major version is part of the import path, so v4 and v5 can coexist during a migration.
+:::
+
+## Keeping up to date
+
+```bash
+go get github.com/labstack/echo/v5
+go mod tidy
+```
diff --git a/site/src/content/docs/guide/ip-address.md b/site/src/content/docs/guide/ip-address.md
new file mode 100644
index 00000000..fb5f9542
--- /dev/null
+++ b/site/src/content/docs/guide/ip-address.md
@@ -0,0 +1,118 @@
+---
+title: IP Address
+description: Retrieve the real client IP address securely behind proxies.
+sidebar:
+ order: 14
+---
+
+The IP address plays a fundamental role in HTTP — it is used for access control,
+auditing, geo-based analysis, and more. Echo exposes `Context#RealIP()` to retrieve
+it.
+
+Retrieving the _real_ client IP is not trivial, especially when L7 proxies sit in
+front of your application. In that case the real IP must be relayed over HTTP from
+the proxies to your app — but you must not trust HTTP headers unconditionally, or you
+risk being deceived. **This is a security risk.**
+
+To retrieve the IP reliably and securely, your application must be aware of your
+entire infrastructure. In Echo, you configure this through `Echo#IPExtractor`.
+
+:::caution
+If you do not set `Echo#IPExtractor` explicitly, Echo falls back to legacy behavior,
+which is not a secure default.
+:::
+
+Start with two questions to find the right approach:
+
+1. Do you put any HTTP (L7) proxy in front of the application? This includes cloud
+ load balancers (such as AWS ALB or GCP HTTP LB) and open-source proxies (such as
+ Nginx, Envoy, or an Istio ingress gateway).
+2. If so, which HTTP header do your proxies use to pass the client IP to the
+ application?
+
+## Case 1: No proxy
+
+If there is no proxy (the app faces the internet directly), the only address you can
+trust is the one from the network layer. Every HTTP header is untrustworthy because
+clients have full control over them.
+
+Use `echo.ExtractIPDirect()`:
+
+```go
+e.IPExtractor = echo.ExtractIPDirect()
+```
+
+## Case 2: Proxies using the X-Forwarded-For header
+
+[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
+is the most common header for relaying client IPs. At each hop, the proxy appends the
+request IP to the end of the header.
+
+```text
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
+───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │
+ │ (IP: b) │ │ (IP: c) │ │ │
+ └──────────┘ └──────────┘ └──────────┘
+
+Case 1.
+XFF: "" "a" "a, b"
+ ~~~~~~
+Case 2.
+XFF: "x" "x, a" "x, a, b"
+ ~~~~~~~~~
+ ↑ What your app will see
+```
+
+In this case, take the **first untrustworthy IP reading from the right**. Never take
+the first one from the left, since the client controls it. Here "trustworthy" means
+you are sure the IP belongs to your infrastructure. In the example above, if `b` and
+`c` are trustworthy, the client IP is `a` in both cases — never `x`.
+
+Use `ExtractIPFromXFFHeader(...TrustOption)`:
+
+```go
+e.IPExtractor = echo.ExtractIPFromXFFHeader()
+```
+
+By default it trusts internal IP addresses — loopback, link-local unicast,
+private-use, and unique local addresses from
+[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890),
+[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291), and
+[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193). Control this with `TrustOption`s:
+
+```go
+e.IPExtractor = echo.ExtractIPFromXFFHeader(
+ echo.TrustLoopback(false), // e.g. IPv4 starting with 127.
+ echo.TrustLinkLocal(false), // e.g. IPv4 starting with 169.254.
+ echo.TrustPrivateNet(false), // e.g. IPv4 starting with 10. or 192.168.
+ echo.TrustIPRange(lbIPRange),
+)
+```
+
+## Case 3: Proxies using the X-Real-IP header
+
+`X-Real-IP` is another header for relaying the client IP, but unlike XFF it carries
+only a single address.
+
+If your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`:
+
+```go
+e.IPExtractor = echo.ExtractIPFromRealIPHeader()
+```
+
+As with XFF, it trusts internal IP addresses by default and accepts the same
+`TrustOption`s.
+
+:::danger
+**Never forget** to configure the outermost proxy (at the edge of your
+infrastructure) **not to pass through incoming headers**. Otherwise a client can
+forge them, opening the door to fraud.
+:::
+
+## Default behavior
+
+By default, Echo considers the first XFF header, the X-Real-IP header, and the IP
+from the network layer all at once.
+
+As this article should make clear, that is not a good choice. It remains the default
+only for backward compatibility.
diff --git a/site/src/content/docs/guide/quickstart.md b/site/src/content/docs/guide/quickstart.md
new file mode 100644
index 00000000..e780b0c1
--- /dev/null
+++ b/site/src/content/docs/guide/quickstart.md
@@ -0,0 +1,76 @@
+---
+title: Quickstart
+description: Build a production-ready Echo API in under five minutes.
+sidebar:
+ order: 1
+---
+
+Echo is a high performance, minimalist Go web framework. This guide gets a server
+running in under five minutes.
+
+## Requirements
+
+Echo requires **Go 1.25 or newer**. Check your version:
+
+```bash
+go version
+```
+
+## Install
+
+Create a module and add Echo:
+
+```bash
+go mod init myapp
+go get github.com/labstack/echo/v5
+```
+
+## Hello, World
+
+Create `main.go`:
+
+```go
+package main
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/middleware"
+)
+
+func main() {
+ e := echo.New()
+
+ e.Use(middleware.RequestLogger())
+ e.Use(middleware.Recover())
+
+ e.GET("/", func(c *echo.Context) error {
+ return c.JSON(http.StatusOK, map[string]string{"message": "Hello, World!"})
+ })
+
+ if err := e.Start(":1323"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+Run it:
+
+```bash
+go run main.go
+```
+
+Your server is live at `http://localhost:1323`. Echo's router dispatches requests
+with **zero dynamic memory allocation** per route.
+
+:::tip[Ask Echo]
+Stuck? Press the **Ask Echo** button (bottom-right) and ask
+*"How do I add JWT auth?"* — answers come straight from these docs.
+:::
+
+## Next steps
+
+- [Routing](/guide/routing/) — static, parameterized, and wildcard routes.
+- [Context](/guide/context/) — the per-request request/response object.
+- [Binding](/guide/binding/) — parse request data into typed structs.
diff --git a/site/src/content/docs/guide/request.md b/site/src/content/docs/guide/request.md
new file mode 100644
index 00000000..033998ce
--- /dev/null
+++ b/site/src/content/docs/guide/request.md
@@ -0,0 +1,170 @@
+---
+title: Request
+description: Retrieve form, query, and path data from a request, and validate it.
+sidebar:
+ order: 7
+---
+
+A handler reads request data through the `echo.Context`. Echo can retrieve values
+individually by name, bind them into structs (see [Binding](/guide/binding/)), and
+hand off validation to a validator you register.
+
+## Retrieve data
+
+### Form data
+
+Retrieve a form field by name with `Context#FormValue(name string)`:
+
+```go
+e.POST("/form", func(c *echo.Context) error {
+ name := c.FormValue("name")
+ return c.String(http.StatusOK, name)
+})
+```
+
+For types other than `string`, use the generic `echo.FormValue[T]` function:
+
+```go
+age, err := echo.FormValue[int](c, "age")
+if err != nil {
+ return err
+}
+```
+
+Test with:
+
+```sh
+curl -X POST http://localhost:1323/form -d 'name=Joe&age=30'
+```
+
+To bind a custom data type, implement the `echo.BindUnmarshaler` interface:
+
+```go
+type Timestamp time.Time
+
+func (t *Timestamp) UnmarshalParam(src string) error {
+ ts, err := time.Parse(time.RFC3339, src)
+ if err != nil {
+ return err
+ }
+ *t = Timestamp(ts)
+ return nil
+}
+```
+
+### Query parameters
+
+Retrieve a query parameter by name with `Context#QueryParam(name string)`:
+
+```go
+func(c *echo.Context) error {
+ name := c.QueryParam("name")
+ return c.String(http.StatusOK, name)
+}
+```
+
+For types other than `string`, use the generic `echo.QueryParam[T]` function:
+
+```go
+age, err := echo.QueryParam[int](c, "age")
+if err != nil {
+ return err
+}
+```
+
+```sh
+curl -X GET "http://localhost:1323?name=Joe&age=30"
+```
+
+### Path parameters
+
+Retrieve a registered path parameter by name with `Context#Param(name string)`:
+
+```go
+e.GET("/users/:name", func(c *echo.Context) error {
+ name := c.Param("name")
+ return c.String(http.StatusOK, name)
+})
+```
+
+For types other than `string`, use the generic `echo.PathParam[T]` function:
+
+```go
+id, err := echo.PathParam[int](c, "id")
+if err != nil {
+ return err
+}
+```
+
+```sh
+curl http://localhost:1323/users/Joe
+curl http://localhost:1323/users/123
+```
+
+### Binding data
+
+Echo can also bind request data into native Go structs and variables. See
+[Binding](/guide/binding/).
+
+## Validate data
+
+Echo has no built-in data validation. You can register a custom validator via
+`Echo#Validator` and use a third-party library such as
+[go-playground/validator](https://github.com/go-playground/validator).
+
+The example below validates a bound struct:
+
+```go
+package main
+
+import (
+ "net/http"
+
+ "github.com/go-playground/validator/v10" // go get github.com/go-playground/validator/v10
+ "github.com/labstack/echo/v5"
+)
+
+type CustomValidator struct {
+ validator *validator.Validate
+}
+
+func (cv *CustomValidator) Validate(i any) error {
+ if err := cv.validator.Struct(i); err != nil {
+ // Optionally return the error to let each route control the status code.
+ return echo.ErrBadRequest.Wrap(err)
+ }
+ return nil
+}
+
+type User struct {
+ Name string `json:"name" validate:"required"`
+ Email string `json:"email" validate:"required,email"`
+}
+
+func main() {
+ e := echo.New()
+ e.Validator = &CustomValidator{validator: validator.New()}
+
+ e.POST("/users", func(c *echo.Context) error {
+ u := new(User)
+ if err := c.Bind(u); err != nil {
+ return err
+ }
+ if err := c.Validate(u); err != nil {
+ return err
+ }
+ return c.JSON(http.StatusOK, u)
+ })
+
+ if err := e.Start(":1323"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+```sh
+curl -X POST http://localhost:1323/users \
+ -H 'Content-Type: application/json' \
+ -d '{"name":"Joe","email":"joe@invalid-domain"}'
+{"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"}
+```
diff --git a/site/src/content/docs/guide/response.md b/site/src/content/docs/guide/response.md
new file mode 100644
index 00000000..04b64819
--- /dev/null
+++ b/site/src/content/docs/guide/response.md
@@ -0,0 +1,322 @@
+---
+title: Response
+description: Send strings, HTML, JSON, XML, files, streams, redirects, and response hooks.
+sidebar:
+ order: 8
+---
+
+A handler writes its response through the `echo.Context`. Each helper sets the
+appropriate `Content-Type` and status code for you.
+
+## Send string
+
+`Context#String(code int, s string)` sends a plain text response with a status code.
+
+```go
+func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Hello, World!")
+}
+```
+
+## Send HTML
+
+`Context#HTML(code int, html string)` sends a simple HTML response with a status
+code. To generate HTML dynamically, see [Templates](/guide/templates/).
+
+```go
+func(c *echo.Context) error {
+ return c.HTML(http.StatusOK, "Hello, World!")
+}
+```
+
+### Send HTML blob
+
+`Context#HTMLBlob(code int, b []byte)` sends an HTML blob with a status code. It is
+handy with a template engine that outputs `[]byte`.
+
+```go
+func handler(c *echo.Context) error {
+ blob := []byte("Hello, World!")
+ return c.HTMLBlob(http.StatusOK, blob)
+}
+```
+
+## Render template
+
+See [Templates](/guide/templates/).
+
+## Send JSON
+
+`Context#JSON(code int, i any)` encodes a Go value as JSON and sends it with a
+status code.
+
+```go
+type User struct {
+ Name string `json:"name" xml:"name"`
+ Email string `json:"email" xml:"email"`
+}
+
+func(c *echo.Context) error {
+ u := &User{
+ Name: "Jon",
+ Email: "jon@labstack.com",
+ }
+ return c.JSON(http.StatusOK, u)
+}
+```
+
+### Stream JSON
+
+`Context#JSON()` uses `json.Marshal` internally, which may be inefficient for large
+payloads. In that case, stream the JSON directly:
+
+```go
+func(c *echo.Context) error {
+ u := &User{
+ Name: "Jon",
+ Email: "jon@labstack.com",
+ }
+ c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
+ c.Response().WriteHeader(http.StatusOK)
+ return json.NewEncoder(c.Response()).Encode(u)
+}
+```
+
+### JSON pretty
+
+`Context#JSONPretty(code int, i any, indent string)` sends a pretty-printed JSON
+response. The indent can be spaces or tabs.
+
+```go
+func(c *echo.Context) error {
+ u := &User{
+ Name: "Jon",
+ Email: "jon@labstack.com",
+ }
+ return c.JSONPretty(http.StatusOK, u, " ")
+}
+```
+
+```json
+{
+ "email": "jon@labstack.com",
+ "name": "Jon"
+}
+```
+
+### JSON blob
+
+`Context#JSONBlob(code int, b []byte)` sends a pre-encoded JSON blob directly, for
+example from a database.
+
+```go
+func(c *echo.Context) error {
+ encodedJSON := []byte{} // Encoded JSON from an external source.
+ return c.JSONBlob(http.StatusOK, encodedJSON)
+}
+```
+
+## Send JSONP
+
+`Context#JSONP(code int, callback string, i any)` encodes a Go value as JSON and
+sends it as a JSONP payload wrapped in the given callback.
+
+```go
+func handler(c *echo.Context) error {
+ callback := c.QueryParam("callback")
+ return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"})
+}
+```
+
+See the [JSONP cookbook](/cookbook/jsonp/).
+
+## Send XML
+
+`Context#XML(code int, i any)` encodes a Go value as XML and sends it with a status
+code.
+
+```go
+func(c *echo.Context) error {
+ u := &User{
+ Name: "Jon",
+ Email: "jon@labstack.com",
+ }
+ return c.XML(http.StatusOK, u)
+}
+```
+
+### Stream XML
+
+`Context#XML` uses `xml.Marshal` internally, which may be inefficient for large
+payloads. In that case, stream the XML directly:
+
+```go
+func(c *echo.Context) error {
+ u := &User{
+ Name: "Jon",
+ Email: "jon@labstack.com",
+ }
+ c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
+ c.Response().WriteHeader(http.StatusOK)
+ return xml.NewEncoder(c.Response()).Encode(u)
+}
+```
+
+### XML pretty
+
+`Context#XMLPretty(code int, i any, indent string)` sends a pretty-printed XML
+response. The indent can be spaces or tabs.
+
+```go
+func(c *echo.Context) error {
+ u := &User{
+ Name: "Jon",
+ Email: "jon@labstack.com",
+ }
+ return c.XMLPretty(http.StatusOK, u, " ")
+}
+```
+
+```xml
+
+
+ Jon
+ jon@labstack.com
+
+```
+
+:::tip
+You can also make `Context#XML()` output pretty-printed XML by appending `pretty`
+to the request URL query string.
+
+```sh
+curl http://localhost:1323/users/1?pretty
+```
+:::
+
+### XML blob
+
+`Context#XMLBlob(code int, b []byte)` sends a pre-encoded XML blob directly, for
+example from a database.
+
+```go
+func(c *echo.Context) error {
+ encodedXML := []byte{} // Encoded XML from an external source.
+ return c.XMLBlob(http.StatusOK, encodedXML)
+}
+```
+
+## Send file
+
+`Context#File(file string)` sends the contents of a file as the response. It sets
+the correct content type and handles caching automatically.
+
+```go
+func(c *echo.Context) error {
+ return c.File("")
+}
+```
+
+## Send attachment
+
+`Context#Attachment(file, name string)` is like `File()` but sends the file with
+`Content-Disposition: attachment` and the given name.
+
+```go
+func(c *echo.Context) error {
+ return c.Attachment("", "")
+}
+```
+
+## Send inline
+
+`Context#Inline(file, name string)` is like `File()` but sends the file with
+`Content-Disposition: inline` and the given name.
+
+```go
+func(c *echo.Context) error {
+ return c.Inline("", "")
+}
+```
+
+## Send blob
+
+`Context#Blob(code int, contentType string, b []byte)` sends arbitrary data with a
+given content type and status code.
+
+```go
+func(c *echo.Context) error {
+ data := []byte(`0306703,0035866,NO_ACTION,06/19/2006
+0086003,"0005866",UPDATED,06/19/2006`)
+ return c.Blob(http.StatusOK, "text/csv", data)
+}
+```
+
+## Send stream
+
+`Context#Stream(code int, contentType string, r io.Reader)` sends an arbitrary data
+stream with a given content type, `io.Reader`, and status code.
+
+```go
+func(c *echo.Context) error {
+ f, err := os.Open("")
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ return c.Stream(http.StatusOK, "image/png", f)
+}
+```
+
+## Send no content
+
+`Context#NoContent(code int)` sends an empty body with a status code.
+
+```go
+func(c *echo.Context) error {
+ return c.NoContent(http.StatusOK)
+}
+```
+
+## Redirect request
+
+`Context#Redirect(code int, url string)` redirects the request to the given URL with
+a status code.
+
+```go
+func(c *echo.Context) error {
+ return c.Redirect(http.StatusMovedPermanently, "")
+}
+```
+
+## Hooks
+
+### Before response
+
+`Response#Before(func())` registers a function that runs just before the response is
+written.
+
+### After response
+
+`Response#After(func())` registers a function that runs just after the response is
+written. If the `Content-Length` is unknown, no after functions run.
+
+```go
+e.GET("/hooks", func(c *echo.Context) error {
+ resp, err := echo.UnwrapResponse(c.Response())
+ if err != nil {
+ return err
+ }
+ resp.Before(func() {
+ println("before response")
+ })
+ resp.After(func() {
+ println("after response")
+ })
+ return c.String(http.StatusOK, "Hello, World!")
+})
+```
+
+:::tip
+You can register multiple `Before` and `After` functions.
+:::
diff --git a/site/src/content/docs/guide/routing.md b/site/src/content/docs/guide/routing.md
new file mode 100644
index 00000000..cb3c28cd
--- /dev/null
+++ b/site/src/content/docs/guide/routing.md
@@ -0,0 +1,75 @@
+---
+title: Routing
+description: Match request URLs to handlers on Echo's zero-allocation radix tree.
+sidebar:
+ order: 3
+---
+
+Echo's optimized router matches request URLs to handlers using a radix tree with
+**zero dynamic memory allocation** and smart route prioritization.
+
+## Registering routes
+
+Use the HTTP-method helpers on the `Echo` instance. Each takes a path pattern and a
+`HandlerFunc` (`func(c *echo.Context) error`), with optional route-level middleware.
+
+```go
+e := echo.New()
+
+e.GET("/users/:id", getUser) // named parameter
+e.POST("/users", createUser)
+e.PUT("/users/:id", updateUser)
+e.DELETE("/users/:id", deleteUser)
+e.GET("/static/*", serveFiles) // wildcard
+```
+
+`Any` registers a handler for all supported methods, and `Match` for a specific set:
+
+```go
+e.Any("/ping", pong)
+e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm)
+```
+
+## Match types
+
+| Pattern | Type | Example match |
+| ------------------ | -------- | -------------------------- |
+| `/users/profile` | Static | `/users/profile` |
+| `/users/:id` | Param | `/users/42` |
+| `/static/*` | Wildcard | `/static/css/app.css` |
+
+:::note
+Priority is **static → param → wildcard**, so `/users/profile` always wins over
+`/users/:id`, which wins over `/users/*`.
+:::
+
+## Path parameters
+
+Read named parameters from the context with `c.Param()` (or `c.ParamOr()` for a default):
+
+```go
+func getUser(c *echo.Context) error {
+ id := c.Param("id")
+ return c.String(http.StatusOK, id)
+}
+```
+
+The wildcard segment is available as the `*` parameter:
+
+```go
+e.GET("/files/*", func(c *echo.Context) error {
+ return c.String(http.StatusOK, c.Param("*"))
+})
+```
+
+## Groups
+
+Group routes that share a prefix and middleware with `e.Group()`:
+
+```go
+admin := e.Group("/admin", middleware.BasicAuth(authFn))
+admin.GET("/metrics", metrics) // -> /admin/metrics
+admin.GET("/users", listUsers) // -> /admin/users
+```
+
+Groups can be nested to compose larger route trees.
diff --git a/site/src/content/docs/guide/static-files.md b/site/src/content/docs/guide/static-files.md
new file mode 100644
index 00000000..b2782be7
--- /dev/null
+++ b/site/src/content/docs/guide/static-files.md
@@ -0,0 +1,89 @@
+---
+title: Serving Static Files
+description: Serve images, JavaScript, CSS, fonts, and other assets with Echo.
+sidebar:
+ order: 9
+---
+
+Echo can serve static assets such as images, JavaScript, CSS, PDFs, and fonts from
+the filesystem or an embedded filesystem.
+
+## Default filesystem
+
+Echo uses `os.DirFS(".")` as its default filesystem, rooted at the current working
+directory. To change it, set the `Echo#Filesystem` field:
+
+```go
+e := echo.New()
+e.Filesystem = os.DirFS("assets")
+```
+
+## Using the Static middleware
+
+See [Static middleware](/middleware/static/).
+
+## Using Echo#Static()
+
+`Echo#Static(prefix, root string)` registers a route that serves static files under
+a path prefix from the given root directory.
+
+Serve any file from `assets` under `/static/*`. A request to `/static/js/main.js`
+serves `assets/js/main.js`:
+
+```go
+e := echo.New()
+e.Static("/static", "assets")
+```
+
+Serve any file from `assets` under `/*`. A request to `/js/main.js` serves
+`assets/js/main.js`:
+
+```go
+e := echo.New()
+e.Static("/", "assets")
+```
+
+## Using Echo#StaticFS()
+
+Static files can be served from any `fs.FS`, including an `embed.FS`. Use
+`echo.MustSubFS` so the served files are rooted at the correct subdirectory — an
+`embed.FS` includes its subdirectories as their own entries.
+
+```go
+//go:embed "assets/images"
+var images embed.FS
+
+func main() {
+ e := echo.New()
+
+ e.StaticFS("/images", echo.MustSubFS(images, "assets/images"))
+
+ sc := echo.StartConfig{Address: ":1323"}
+ if err := sc.Start(context.Background(), e); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Using Echo#File()
+
+`Echo#File(path, file string)` registers a route that serves a single static file.
+
+Serve an index page from `public/index.html`:
+
+```go
+e.File("/", "public/index.html")
+```
+
+Serve a favicon from `app/assets/favicon.ico`:
+
+```go
+e := echo.New()
+e.Filesystem = os.DirFS("/")
+e.File("/favicon.ico", "app/assets/favicon.ico") // The file path must not have a leading slash.
+```
+
+:::caution
+A leading `/` in the file path does not work with most `fs.FS` implementations. Use
+a relative path.
+:::
diff --git a/site/src/content/docs/guide/templates.md b/site/src/content/docs/guide/templates.md
new file mode 100644
index 00000000..4e43822c
--- /dev/null
+++ b/site/src/content/docs/guide/templates.md
@@ -0,0 +1,133 @@
+---
+title: Templates
+description: Render HTML templates with any engine by registering a renderer.
+sidebar:
+ order: 10
+---
+
+`Context#Render(code int, name string, data any) error` renders a template with data
+and sends a `text/html` response with a status code. Register a renderer by setting
+`Echo#Renderer`, which lets you use any template engine.
+
+## Rendering
+
+The example below uses Go's `html/template`.
+
+Use the default template renderer:
+
+```go
+e.Renderer = &echo.TemplateRenderer{
+ Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
+}
+```
+
+Or implement the `echo.Renderer` interface yourself:
+
+```go
+type Template struct {
+ templates *template.Template
+}
+
+func (t *Template) Render(c *echo.Context, w io.Writer, name string, data any) error {
+ return t.templates.ExecuteTemplate(w, name, data)
+}
+```
+
+1. Pre-compile the templates.
+
+ `public/views/hello.html`:
+
+ ```html
+ {{define "hello"}}Hello, {{.}}!{{end}}
+ ```
+
+ ```go
+ t := &Template{
+ templates: template.Must(template.ParseGlob("public/views/*.html")),
+ }
+ ```
+
+2. Register the renderer.
+
+ ```go
+ e := echo.New()
+ e.Renderer = t
+ e.GET("/hello", Hello)
+ ```
+
+3. Render a template inside the handler.
+
+ ```go
+ func Hello(c *echo.Context) error {
+ return c.Render(http.StatusOK, "hello", "World")
+ }
+ ```
+
+## Advanced: calling Echo from templates
+
+Sometimes it is useful to generate URIs from within a template by calling
+`Echo#Reverse`. Go's `html/template` is not ideally suited for this, but it can be
+done in two ways: by providing a common method on every object passed to templates,
+or by passing a `map[string]any` and augmenting it in the custom renderer. The
+latter is more flexible. Here is a complete example.
+
+`template.html`:
+
+```html
+
+
+
Hello {{index . "name"}}
+
+
{{ with $x := index . "reverse" }}
+ {{ call $x "foobar" }}
+ {{ end }}
+
+
+
+```
+
+`server.go`:
+
+```go
+package main
+
+import (
+ "html/template"
+ "io"
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+)
+
+// TemplateRenderer is a custom html/template renderer for Echo.
+type TemplateRenderer struct {
+ templates *template.Template
+}
+
+// Render renders a template document.
+func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error {
+ // Add global methods if the data is a map.
+ if viewContext, isMap := data.(map[string]any); isMap {
+ viewContext["reverse"] = c.RouteInfo().Reverse
+ }
+
+ return t.templates.ExecuteTemplate(w, name, data)
+}
+
+func main() {
+ e := echo.New()
+ e.Renderer = &TemplateRenderer{
+ templates: template.Must(template.ParseGlob("main/*.html")),
+ }
+
+ e.GET("/something/:name", func(c *echo.Context) error {
+ return c.Render(http.StatusOK, "template.html", map[string]any{
+ "name": "Dolly!",
+ })
+ })
+
+ if err := e.Start(":1323"); err != nil {
+ e.Logger.Error("shutting down the server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/guide/testing.md b/site/src/content/docs/guide/testing.md
new file mode 100644
index 00000000..2efd3260
--- /dev/null
+++ b/site/src/content/docs/guide/testing.md
@@ -0,0 +1,264 @@
+---
+title: Testing
+description: Test handlers and middleware with httptest and the echotest helpers.
+sidebar:
+ order: 13
+---
+
+Echo handlers and middleware are plain functions over an `echo.Context`, so they are
+straightforward to test with the standard `net/http/httptest` package. The
+`echotest` package provides helpers that cut down on boilerplate.
+
+## Testing a handler
+
+Consider two handlers:
+
+**CreateUser** — `POST /users`
+
+- Accepts a JSON payload.
+- Returns `201 Created` on success.
+- Returns `500 Internal Server Error` on error.
+
+**GetUser** — `GET /users/:email`
+
+- Returns `200 OK` on success.
+- Returns `404 Not Found` if the user does not exist, otherwise `500 Internal Server Error`.
+
+`handler.go`:
+
+```go
+package handler
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo/v5"
+)
+
+type (
+ User struct {
+ Name string `json:"name" form:"name"`
+ Email string `json:"email" form:"email"`
+ }
+ handler struct {
+ db map[string]*User
+ }
+)
+
+func (h *handler) createUser(c *echo.Context) error {
+ u := new(User)
+ if err := c.Bind(u); err != nil {
+ return err
+ }
+ return c.JSON(http.StatusCreated, u)
+}
+
+func (h *handler) getUser(c *echo.Context) error {
+ email := c.Param("email")
+ user := h.db[email]
+ if user == nil {
+ return echo.NewHTTPError(http.StatusNotFound, "user not found")
+ }
+ return c.JSON(http.StatusOK, user)
+}
+```
+
+`handler_test.go`:
+
+```go
+package handler
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/labstack/echo/v5"
+ "github.com/labstack/echo/v5/echotest"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ mockDB = map[string]*User{
+ "jon@labstack.com": {Name: "Jon Snow", Email: "jon@labstack.com"},
+ }
+ userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}`
+)
+
+func TestCreateUser(t *testing.T) {
+ // Setup
+ e := echo.New()
+ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+
+ rec := httptest.NewRecorder()
+ c := e.NewContext(req, rec)
+
+ h := &handler{mockDB}
+
+ // Assertions
+ if assert.NoError(t, h.createUser(c)) {
+ assert.Equal(t, http.StatusCreated, rec.Code)
+ assert.Equal(t, userJSON+"\n", rec.Body.String())
+ }
+}
+```
+
+### Using the echotest helpers
+
+`echotest.ContextConfig` builds a context (and recorder) from a declarative
+description of the request:
+
+```go
+// Same test as above, using echotest.
+func TestCreateUserWithEchoTest(t *testing.T) {
+ c, rec := echotest.ContextConfig{
+ Headers: map[string][]string{
+ echo.HeaderContentType: {echo.MIMEApplicationJSON},
+ },
+ JSONBody: []byte(userJSON),
+ }.ToContextRecorder(t)
+
+ h := &handler{mockDB}
+
+ // Assertions
+ if assert.NoError(t, h.createUser(c)) {
+ assert.Equal(t, http.StatusCreated, rec.Code)
+ assert.Equal(t, userJSON+"\n", rec.Body.String())
+ }
+}
+
+// Even shorter, using ServeWithHandler.
+func TestCreateUserWithServeHandler(t *testing.T) {
+ h := &handler{mockDB}
+
+ rec := echotest.ContextConfig{
+ Headers: map[string][]string{
+ echo.HeaderContentType: {echo.MIMEApplicationJSON},
+ },
+ JSONBody: []byte(userJSON),
+ }.ServeWithHandler(t, h.createUser)
+
+ assert.Equal(t, http.StatusCreated, rec.Code)
+ assert.Equal(t, userJSON+"\n", rec.Body.String())
+}
+
+func TestGetUser(t *testing.T) {
+ c, rec := echotest.ContextConfig{
+ PathValues: echo.PathValues{
+ {Name: "email", Value: "jon@labstack.com"},
+ },
+ Headers: map[string][]string{
+ echo.HeaderContentType: {echo.MIMEApplicationJSON},
+ },
+ }.ToContextRecorder(t)
+
+ h := &handler{mockDB}
+
+ // Assertions
+ if assert.NoError(t, h.getUser(c)) {
+ assert.Equal(t, http.StatusOK, rec.Code)
+ assert.Equal(t, userJSON+"\n", rec.Body.String())
+ }
+}
+```
+
+### Using a form payload
+
+```go
+// import "net/url"
+f := make(url.Values)
+f.Set("name", "Jon Snow")
+f.Set("email", "jon@labstack.com")
+req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
+req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
+```
+
+A multipart form payload with `echotest`:
+
+```go
+func TestContext_MultipartForm(t *testing.T) {
+ testConf := echotest.ContextConfig{
+ MultipartForm: &echotest.MultipartForm{
+ Fields: map[string]string{
+ "key": "value",
+ },
+ Files: []echotest.MultipartFormFile{
+ {
+ Fieldname: "file",
+ Filename: "test.json",
+ Content: echotest.LoadBytes(t, "testdata/test.json"),
+ },
+ },
+ },
+ }
+ c := testConf.ToContext(t)
+
+ assert.Equal(t, "value", c.FormValue("key"))
+ assert.Equal(t, http.MethodPost, c.Request().Method)
+ assert.Equal(t, true, strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "multipart/form-data; boundary="))
+
+ fv, err := c.FormFile("file")
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, "test.json", fv.Filename)
+}
+```
+
+### Setting path parameters
+
+```go
+c.SetPathValues(echo.PathValues{
+ {Name: "id", Value: "1"},
+ {Name: "email", Value: "jon@labstack.com"},
+})
+```
+
+### Setting query parameters
+
+```go
+// import "net/url"
+q := make(url.Values)
+q.Set("email", "jon@labstack.com")
+req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil)
+```
+
+## Testing middleware
+
+```go
+func TestMiddleware(t *testing.T) {
+ handler := func(c *echo.Context) error {
+ return c.JSON(http.StatusTeapot, fmt.Sprintf("email: %s", c.Param("email")))
+ }
+ middleware := func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+ c.Set("user_id", int64(1234))
+ return next(c)
+ }
+ }
+
+ c, rec := echotest.ContextConfig{
+ PathValues: echo.PathValues{{Name: "email", Value: "jon@labstack.com"}},
+ }.ToContextRecorder(t)
+
+ if err := middleware(handler)(c); err != nil {
+ t.Fatal(err)
+ }
+
+ // Check that the middleware set the value.
+ userID, err := echo.ContextGet[int64](c, "user_id")
+ assert.NoError(t, err)
+ assert.Equal(t, int64(1234), userID)
+
+ // Check that the handler returned the correct response.
+ assert.Equal(t, http.StatusTeapot, rec.Code)
+}
+```
+
+:::tip
+For more examples, see the [middleware test cases](https://github.com/labstack/echo/tree/master/middleware)
+in the Echo source.
+:::
diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx
new file mode 100644
index 00000000..cc9310ad
--- /dev/null
+++ b/site/src/content/docs/index.mdx
@@ -0,0 +1,101 @@
+---
+title: Echo
+description: High performance, extensible, minimalist Go web framework — a zero-allocation router, batteries-included middleware, and an expressive API.
+template: splash
+# Homepage gets a descriptive, keyword-rich instead of the bare
+# "Echo | Echo" Starlight would derive from title === site title.
+head:
+ - tag: title
+ content: Echo — High performance, minimalist Go web framework
+# Minimal hero so Starlight folds the page title into the (hidden) hero region
+# instead of rendering a separate title panel. Our real hero is .
+hero:
+ tagline: ' '
+tableOfContents: false
+---
+
+import HomeHero from '../../components/HomeHero.astro';
+import { starsLabel } from '../../data/github.ts';
+
+
+
+
+
{starsLabel}GitHub stars
+
0 allocsrouter, per request
+
25+built-in middlewares
+
MITopen source license
+
+
+
Why Echo
Everything you need. Nothing you don't.
+
+
+
Optimized Router
Radix-tree routing with zero dynamic allocation and smart route prioritization.
+
Batteries-included Middleware
CORS, JWT, rate-limit, gzip, recover, request logging — 25+ built in.
+
Data Binding
Bind JSON, XML, form, query & path params into typed structs, with validation.
+
Automatic TLS
HTTPS out of the box via Let's Encrypt, plus HTTP/2 support.
+
Extensible
Composable middleware and a clean, minimal interface for total control.
+
Templates
Plug in any Go template engine for fast, flexible HTML rendering.
+
+
+
Get Started
A running server in three steps.
+
+
+
01
Install
Add Echo to your module.
go get github.com/labstack/echo/v5
+
02
Write
Register a route.
e := echo.New()
+e.GET("/", hello)
+e.Start(":1323")
diff --git a/site/src/content/docs/middleware/basic-auth.md b/site/src/content/docs/middleware/basic-auth.md
new file mode 100644
index 00000000..cd93eebc
--- /dev/null
+++ b/site/src/content/docs/middleware/basic-auth.md
@@ -0,0 +1,74 @@
+---
+title: Basic Auth
+description: HTTP Basic authentication middleware that validates username and password credentials.
+sidebar:
+ order: 1
+---
+
+Basic Auth middleware provides HTTP basic authentication.
+
+- For valid credentials it calls the next handler.
+- For missing or invalid credentials, it sends a `401 Unauthorized` response.
+
+## Usage
+
+```go
+e.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (bool, error) {
+ // Use a constant time comparison to prevent timing attacks.
+ if subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 &&
+ subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 {
+ return true, nil
+ }
+ return false, nil
+}))
+```
+
+## Custom configuration
+
+```go
+e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{}))
+```
+
+## Configuration
+
+```go
+type BasicAuthConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Validator validates the credentials. If the request contains multiple basic
+ // auth headers, it is called once for each header until the first valid result.
+ // Required.
+ Validator BasicAuthValidator
+
+ // Realm is the realm attribute of the WWW-Authenticate header.
+ // Default value "Restricted".
+ Realm string
+
+ // AllowedCheckLimit sets how many headers are allowed to be checked. This is
+ // useful in environments such as corporate test setups with application proxies
+ // restricting access with their own auth scheme.
+ // Default value 1.
+ AllowedCheckLimit uint
+}
+```
+
+The `Validator` has the signature:
+
+```go
+type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error)
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset.
+BasicAuthConfig{
+ Skipper: DefaultSkipper,
+ Realm: "Restricted",
+}
+```
+
+:::caution[Security]
+Always compare credentials with `subtle.ConstantTimeCompare` to prevent timing attacks.
+:::
diff --git a/site/src/content/docs/middleware/body-dump.md b/site/src/content/docs/middleware/body-dump.md
new file mode 100644
index 00000000..129b2ccf
--- /dev/null
+++ b/site/src/content/docs/middleware/body-dump.md
@@ -0,0 +1,72 @@
+---
+title: Body Dump
+description: Capture request and response payloads and pass them to a handler for logging or debugging.
+sidebar:
+ order: 2
+---
+
+Body Dump middleware captures the request and response payloads and passes them to a
+registered handler. It is generally used for debugging or logging.
+
+:::caution
+Avoid Body Dump for large payloads such as file uploads or downloads. If you must use it
+on such routes, add an exception in the skipper function.
+:::
+
+## Usage
+
+```go
+e := echo.New()
+e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) {
+ // Handle the request and response bodies.
+}))
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{}))
+```
+
+## Configuration
+
+```go
+type BodyDumpConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Handler receives the request and response payloads and the handler error, if any.
+ // Required.
+ Handler BodyDumpHandler
+
+ // MaxRequestBytes limits how much of the request body to dump. If the request body
+ // exceeds this limit, only the first MaxRequestBytes are dumped and the handler
+ // receives truncated data.
+ // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended).
+ MaxRequestBytes int64
+
+ // MaxResponseBytes limits how much of the response body to dump. If the response body
+ // exceeds this limit, only the first MaxResponseBytes are dumped and the handler
+ // receives truncated data.
+ // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended).
+ MaxResponseBytes int64
+}
+```
+
+The `Handler` has the signature:
+
+```go
+type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error)
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset (Handler is required).
+BodyDumpConfig{
+ Skipper: DefaultSkipper,
+ MaxRequestBytes: 5 * MB,
+ MaxResponseBytes: 5 * MB,
+}
+```
diff --git a/site/src/content/docs/middleware/body-limit.md b/site/src/content/docs/middleware/body-limit.md
new file mode 100644
index 00000000..93eac3d0
--- /dev/null
+++ b/site/src/content/docs/middleware/body-limit.md
@@ -0,0 +1,48 @@
+---
+title: Body Limit
+description: Reject requests whose body exceeds a configured maximum size.
+sidebar:
+ order: 3
+---
+
+Body Limit middleware sets the maximum allowed size for a request body. If the size
+exceeds the configured limit, it sends a `413 Request Entity Too Large` response.
+
+The limit is enforced against both the `Content-Length` request header and the actual
+content read, which makes it resilient against spoofed headers. The limit is specified
+in bytes.
+
+## Usage
+
+```go
+e := echo.New()
+e.Use(middleware.BodyLimit(2_097_152)) // 2 MB
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{}))
+```
+
+## Configuration
+
+```go
+type BodyLimitConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // LimitBytes is the maximum allowed size in bytes for a request body.
+ LimitBytes int64
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset (Limit is required).
+BodyLimitConfig{
+ Skipper: DefaultSkipper,
+}
+```
diff --git a/site/src/content/docs/middleware/casbin-auth.md b/site/src/content/docs/middleware/casbin-auth.md
new file mode 100644
index 00000000..7012b66b
--- /dev/null
+++ b/site/src/content/docs/middleware/casbin-auth.md
@@ -0,0 +1,203 @@
+---
+title: Casbin Auth
+description: Authorize requests with the Casbin access control library using a small custom middleware.
+sidebar:
+ order: 4
+---
+
+[Casbin](https://github.com/casbin/casbin) is a powerful, efficient open-source access
+control library for Go. It supports enforcing authorization across many models:
+
+- ACL (Access Control List)
+- ACL with superuser
+- ACL without users — useful for systems without authentication or user log-ins
+- ACL without resources — target a type of resource (for example `write-article`, `read-log`) rather than an individual one
+- RBAC (Role-Based Access Control)
+- RBAC with resource roles — both users and resources can have roles
+- RBAC with domains/tenants — users can have different role sets per domain/tenant
+- ABAC (Attribute-Based Access Control)
+- RESTful
+- Deny-override — both allow and deny rules are supported, deny overrides allow
+
+See the [API overview](https://casbin.org/docs/api-overview) and the
+[Casbin documentation](https://casbin.org/docs/) for details.
+
+## Dependencies
+
+```bash
+go get github.com/casbin/casbin/v3
+```
+
+```go
+import (
+ "github.com/casbin/casbin/v3"
+)
+```
+
+## Implementation
+
+Echo does not ship a Casbin middleware; the integration is a small wrapper around the
+Casbin enforcer:
+
+```go
+// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/).
+func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+ username, err := userGetter(c)
+ if err != nil {
+ return echo.ErrUnauthorized.Wrap(err)
+ }
+ if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil {
+ return echo.ErrInternalServerError.Wrap(err)
+ } else if !pass {
+ return echo.NewHTTPError(http.StatusForbidden, "access denied")
+ }
+ return next(c)
+ }
+ }
+}
+```
+
+## Example
+
+Create a Casbin model file `auth_model.conf`:
+
+```ini
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
+```
+
+Create a Casbin policy file `auth_policy.csv`:
+
+```csv
+p, 1234567890, /dataset1/*, GET
+p, alice, /dataset1/*, GET
+p, alice, /dataset1/resource1, POST
+p, bob, /dataset2/resource1, *
+p, bob, /dataset2/resource2, GET
+p, bob, /dataset2/folder1/*, POST
+p, dataset1_admin, /dataset1/*, *
+g, cathy, dataset1_admin
+```
+
+Authentication and authorization are separate concerns. Authenticate the user with
+another middleware (such as JWT or Basic Auth), then supply a `userGetter` so Casbin
+can authorize the request.
+
+### With JWT
+
+```go
+e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
+jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
+ token, err := echo.ContextGet[*jwt.Token](c, "user")
+ if err != nil {
+ return "", err
+ }
+ return token.Claims.GetSubject()
+}
+e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
+```
+
+Try it with:
+
+```bash
+curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
+```
+
+### With Basic Auth
+
+```go
+// BasicAuth middleware does authentication
+e.Use(middleware.BasicAuth(func(c *echo.Context, user, password string) (bool, error) {
+ return subtle.ConstantTimeCompare([]byte(user), []byte("alice")) == 1 &&
+ subtle.ConstantTimeCompare([]byte(password), []byte("password")) == 1, nil
+}))
+basicAuthUser := func(c *echo.Context) (string, error) { // Basic auth user getter for Casbin authorization
+ username, _, _ := c.Request().BasicAuth() // password is verified by the BasicAuth middleware above
+ return username, nil
+}
+e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization
+```
+
+Try it with:
+
+```bash
+# should pass
+curl -v -u "alice:password" http://localhost:8080/dataset1/any
+# should fail
+curl -v -u "alice:password" http://localhost:8080/dataset2/resource2
+```
+
+### Full Casbin + JWT example
+
+```go
+package main
+
+import (
+ "log/slog"
+ "net/http"
+
+ "github.com/casbin/casbin/v3"
+ "github.com/golang-jwt/jwt/v5"
+ echojwt "github.com/labstack/echo-jwt/v5"
+ "github.com/labstack/echo/v5"
+)
+
+// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/).
+func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c *echo.Context) error {
+ username, err := userGetter(c)
+ if err != nil {
+ return echo.ErrUnauthorized.Wrap(err)
+ }
+ if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil {
+ return echo.ErrInternalServerError.Wrap(err)
+ } else if !pass {
+ return echo.NewHTTPError(http.StatusForbidden, "access denied")
+ }
+ return next(c)
+ }
+ }
+}
+
+func main() {
+ e := echo.New()
+
+ ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
+ if err != nil {
+ slog.Error("failed to initialize Casbin enforcer", "error", err)
+ }
+
+ e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
+ jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
+ token, err := echo.ContextGet[*jwt.Token](c, "user")
+ if err != nil {
+ return "", err
+ }
+ return token.Claims.GetSubject()
+ }
+ e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
+
+ e.GET("/*", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Hello, World!")
+ })
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/middleware/context-timeout.md b/site/src/content/docs/middleware/context-timeout.md
new file mode 100644
index 00000000..99524e9f
--- /dev/null
+++ b/site/src/content/docs/middleware/context-timeout.md
@@ -0,0 +1,47 @@
+---
+title: Context Timeout
+description: Apply a timeout to the request context so context-aware operations can return early.
+sidebar:
+ order: 5
+---
+
+Context Timeout middleware applies a timeout to the request context within a predefined
+period, so context-aware methods can return early once the deadline is exceeded.
+
+## Usage
+
+```go
+e.Use(middleware.ContextTimeout(60 * time.Second))
+```
+
+## Custom configuration
+
+```go
+e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{
+ Timeout: 60 * time.Second,
+}))
+```
+
+## Configuration
+
+```go
+type ContextTimeoutConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // ErrorHandler is a function invoked when an error arises during middleware execution.
+ ErrorHandler func(c *echo.Context, err error) error
+
+ // Timeout configures the timeout for the middleware.
+ Timeout time.Duration
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset (Timeout is required).
+ContextTimeoutConfig{
+ Skipper: DefaultSkipper,
+}
+```
diff --git a/site/src/content/docs/middleware/cors.md b/site/src/content/docs/middleware/cors.md
new file mode 100644
index 00000000..740ccfbe
--- /dev/null
+++ b/site/src/content/docs/middleware/cors.md
@@ -0,0 +1,118 @@
+---
+title: CORS
+description: Cross-Origin Resource Sharing middleware for secure cross-domain access control.
+sidebar:
+ order: 6
+---
+
+CORS middleware implements the [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol) specification. CORS gives
+web servers cross-domain access controls, which enable secure cross-domain data transfers.
+
+## Usage
+
+```go
+e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com"))
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
+ AllowOrigins: []string{"https://labstack.com", "https://labstack.net"},
+ AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
+}))
+```
+
+## Configuration
+
+```go
+type CORSConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // AllowOrigins determines the value of the Access-Control-Allow-Origin response
+ // header, defining the list of origins that may access the resource.
+ //
+ // An origin consists of: scheme + "://" + host + optional ":" + port.
+ // A wildcard may be used, but it must be set explicitly as []string{"*"}.
+ // Example: `https://example.com`, `http://example.com:8080`, `*`.
+ //
+ // Security: use extreme caution when handling the origin and carefully validate any
+ // logic. Attackers may register hostile domain names. See
+ // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
+ //
+ // Mandatory.
+ AllowOrigins []string
+
+ // UnsafeAllowOriginFunc is an optional custom function to validate the origin. It
+ // takes the origin and returns the allowed origin, whether it is allowed, and an
+ // error (returned immediately by the handler). If set, AllowOrigins is ignored.
+ //
+ // Security: use extreme caution when handling the origin. Attackers may register
+ // hostile (sub)domain names.
+ //
+ // Sub-domain check example:
+ // UnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) {
+ // if strings.HasSuffix(origin, ".example.com") {
+ // return origin, true, nil
+ // }
+ // return "", false, nil
+ // }
+ //
+ // Optional.
+ UnsafeAllowOriginFunc func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error)
+
+ // AllowMethods determines the value of the Access-Control-Allow-Methods response
+ // header, used in response to a preflight request.
+ //
+ // Optional. Defaults to GET, HEAD, PUT, PATCH, POST, DELETE. If left empty, the
+ // middleware fills the preflight Access-Control-Allow-Methods header from the
+ // `Allow` header that the router set into the context.
+ AllowMethods []string
+
+ // AllowHeaders determines the value of the Access-Control-Allow-Headers response
+ // header, indicating which HTTP headers can be used in the actual request.
+ //
+ // Optional. Defaults to an empty list.
+ AllowHeaders []string
+
+ // AllowCredentials determines the value of the Access-Control-Allow-Credentials
+ // response header, indicating whether the response can be exposed when the
+ // credentials mode is true.
+ //
+ // Optional. Default value false, in which case the header is not set.
+ //
+ // Security: avoid using AllowCredentials = true together with AllowOrigins = *.
+ AllowCredentials bool
+
+ // ExposeHeaders determines the value of Access-Control-Expose-Headers, the list of
+ // headers clients are allowed to access.
+ //
+ // Optional. Default value []string{}, in which case the header is not set.
+ ExposeHeaders []string
+
+ // MaxAge determines the value of the Access-Control-Max-Age response header, how long
+ // (in seconds) the results of a preflight request can be cached. The header is set
+ // only if MaxAge != 0; a negative value sends "0", instructing browsers not to cache.
+ //
+ // Optional. Default value 0 — the header is not sent.
+ MaxAge int
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset.
+CORSConfig{
+ Skipper: DefaultSkipper,
+ AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
+}
+```
+
+:::caution[Security]
+Never combine `AllowCredentials = true` with a wildcard `AllowOrigins`. When you need
+dynamic origin validation, use `UnsafeAllowOriginFunc` and validate carefully —
+attackers may register hostile (sub)domain names.
+:::
diff --git a/site/src/content/docs/middleware/csrf.md b/site/src/content/docs/middleware/csrf.md
new file mode 100644
index 00000000..0583c308
--- /dev/null
+++ b/site/src/content/docs/middleware/csrf.md
@@ -0,0 +1,177 @@
+---
+title: CSRF
+description: Cross-Site Request Forgery protection using Sec-Fetch-Site metadata and token validation.
+sidebar:
+ order: 7
+---
+
+Cross-Site Request Forgery (CSRF, sometimes pronounced "sea-surf", or XSRF) is a type of
+malicious exploit where unauthorized commands are transmitted from a user that a website
+trusts.
+
+## Usage
+
+```go
+e.Use(middleware.CSRF())
+```
+
+## How it works
+
+The CSRF middleware supports the
+[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site)
+header as a modern, defense-in-depth approach to
+[CSRF protection](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers),
+implementing the OWASP-recommended Fetch Metadata API alongside the traditional
+token-based mechanism.
+
+Modern browsers automatically send the `Sec-Fetch-Site` header with every request,
+indicating the relationship between the request origin and the target. The middleware
+uses this to make a security decision:
+
+- **`same-origin`** or **`none`** — allowed (exact origin match or direct user navigation)
+- **`same-site`** — falls back to token validation (for example, subdomain to main domain)
+- **`cross-site`** — blocked by default with a `403` error for unsafe methods (POST, PUT, DELETE, PATCH)
+
+For browsers that do not send this header (older browsers), the middleware seamlessly
+falls back to traditional token-based CSRF protection.
+
+Two options tune `Sec-Fetch-Site` behavior:
+
+- `TrustedOrigins []string` — allowlist specific origins for cross-site requests (useful for OAuth callbacks, webhooks)
+- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)` — custom logic for same-site/cross-site validation
+
+```go
+e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
+ // Allow OAuth callbacks from a trusted provider.
+ TrustedOrigins: []string{"https://oauth-provider.com"},
+
+ // Custom validation for same-site/cross-site requests.
+ AllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) {
+ // Your custom authorization logic here.
+ return validateCustomAuth(c), nil
+ // return true, err // blocks the request with an error
+ // return true, nil // allows the request through
+ // return false, nil // falls back to legacy token logic
+ },
+}))
+```
+
+## Token-based protection
+
+```go
+e := echo.New()
+e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
+ TokenLookup: "header:X-XSRF-TOKEN",
+}))
+```
+
+The example above extracts the CSRF token from the `X-XSRF-TOKEN` request header.
+
+Reading the token from a cookie instead:
+
+```go
+middleware.CSRFWithConfig(middleware.CSRFConfig{
+ TokenLookup: "cookie:_csrf",
+ CookiePath: "/",
+ CookieDomain: "example.com",
+ CookieSecure: true,
+ CookieHTTPOnly: true,
+ CookieSameSite: http.SameSiteStrictMode,
+})
+```
+
+## Accessing the CSRF token
+
+- **Server-side** — the token is available from the context under `ContextKey` and can be passed to the client via a template.
+- **Client-side** — the token can be read from the CSRF cookie.
+
+## Configuration
+
+```go
+type CSRFConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // TrustedOrigins permits any request with a `Sec-Fetch-Site` header whose `Origin`
+ // header exactly matches one of the listed values. Values should be formatted as
+ // the Origin header: "scheme://host[:port]".
+ TrustedOrigins []string
+
+ // AllowSecFetchSiteFunc allows custom behaviour for `Sec-Fetch-Site` requests that
+ // are about to fail with a CSRF error, to be allowed or replaced with a custom
+ // error. Applies to `same-site` and `cross-site` values.
+ AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)
+
+ // TokenLength is the length of the generated token.
+ // Optional. Default value 32.
+ TokenLength uint8
+
+ // TokenLookup is a string in the form ":" or
+ // ":,:" used to extract the token from the request.
+ // Optional. Default value "header:X-CSRF-Token".
+ // Possible values:
+ // - "header:" or "header::"
+ // - "query:"
+ // - "form:"
+ // Multiple sources example: "header:X-CSRF-Token,query:csrf".
+ TokenLookup string `yaml:"token_lookup"`
+
+ // Generator defines a function to generate the token.
+ // Optional. Defaults to randomString(TokenLength).
+ Generator func() string
+
+ // ContextKey is the key under which the generated CSRF token is stored in the context.
+ // Optional. Default value "csrf".
+ ContextKey string
+
+ // CookieName is the name of the CSRF cookie that stores the token.
+ // Optional. Default value "_csrf".
+ CookieName string
+
+ // CookieDomain is the domain of the CSRF cookie.
+ // Optional. Default value none.
+ CookieDomain string
+
+ // CookiePath is the path of the CSRF cookie.
+ // Optional. Default value none.
+ CookiePath string
+
+ // CookieMaxAge is the max age (in seconds) of the CSRF cookie.
+ // Optional. Default value 86400 (24h).
+ CookieMaxAge int
+
+ // CookieSecure indicates whether the CSRF cookie is secure.
+ // Optional. Default value false.
+ CookieSecure bool
+
+ // CookieHTTPOnly indicates whether the CSRF cookie is HTTP only.
+ // Optional. Default value false.
+ CookieHTTPOnly bool
+
+ // CookieSameSite indicates the SameSite mode of the CSRF cookie.
+ // Optional. Default value SameSiteDefaultMode.
+ CookieSameSite http.SameSite
+
+ // ErrorHandler defines a function that returns custom errors.
+ ErrorHandler func(c *echo.Context, err error) error
+}
+```
+
+### Default configuration
+
+```go
+var DefaultCSRFConfig = CSRFConfig{
+ Skipper: DefaultSkipper,
+ TokenLength: 32,
+ TokenLookup: "header:" + echo.HeaderXCSRFToken,
+ ContextKey: "csrf",
+ CookieName: "_csrf",
+ CookieMaxAge: 86400,
+ CookieSameSite: http.SameSiteDefaultMode,
+}
+```
+
+## Full example
+
+A complete, runnable example is available in the
+[echox cookbook](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go).
diff --git a/site/src/content/docs/middleware/decompress.md b/site/src/content/docs/middleware/decompress.md
new file mode 100644
index 00000000..4112fd09
--- /dev/null
+++ b/site/src/content/docs/middleware/decompress.md
@@ -0,0 +1,59 @@
+---
+title: Decompress
+description: Transparently decompress gzip-encoded request bodies.
+sidebar:
+ order: 8
+---
+
+Decompress middleware decompresses the HTTP request body when the `Content-Encoding`
+header is set to `gzip`.
+
+:::note
+The body is decompressed in memory and held there for the lifetime of the request (and
+until garbage collection).
+:::
+
+## Usage
+
+```go
+e.Use(middleware.Decompress())
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{
+ Skipper: middleware.DefaultSkipper,
+}))
+```
+
+## Configuration
+
+```go
+type DecompressConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // GzipDecompressPool provides the sync.Pool used to create and store gzip readers.
+ GzipDecompressPool Decompressor
+
+ // MaxDecompressedSize limits the maximum size of the decompressed request body in
+ // bytes. If the decompressed body exceeds this limit, the middleware returns an
+ // HTTP 413 error. This prevents zip-bomb attacks where a small compressed payload
+ // decompresses to a huge size.
+ // Default: 100 * MB (104,857,600 bytes). Set to -1 to disable limits (not recommended).
+ MaxDecompressedSize int64
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset.
+DecompressConfig{
+ Skipper: DefaultSkipper,
+ GzipDecompressPool: &DefaultGzipDecompressPool{},
+ MaxDecompressedSize: 100 * MB,
+}
+```
diff --git a/site/src/content/docs/middleware/gzip.md b/site/src/content/docs/middleware/gzip.md
new file mode 100644
index 00000000..5af5520a
--- /dev/null
+++ b/site/src/content/docs/middleware/gzip.md
@@ -0,0 +1,69 @@
+---
+title: Gzip
+description: Compress HTTP responses with the gzip compression scheme.
+sidebar:
+ order: 9
+---
+
+Gzip middleware compresses the HTTP response using the gzip compression scheme.
+
+## Usage
+
+```go
+e.Use(middleware.Gzip())
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
+ Level: 5,
+}))
+```
+
+:::tip
+Pass a skipper to disable gzip for certain URLs.
+:::
+
+```go
+e := echo.New()
+e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
+ Skipper: func(c *echo.Context) bool {
+ return strings.Contains(c.Path(), "metrics") // change "metrics" to your own path
+ },
+}))
+```
+
+## Configuration
+
+```go
+type GzipConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Level is the gzip compression level.
+ // Optional. Default value -1.
+ Level int
+
+ // MinLength is the length threshold before gzip compression is applied.
+ // Optional. Default value 0.
+ //
+ // Most of the time the default is fine. Compressing a short response might increase
+ // the transmitted data because of gzip's format overhead, and compression consumes
+ // CPU and time on both server and client. Depending on your use case such a
+ // threshold can be useful.
+ MinLength int
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset.
+GzipConfig{
+ Skipper: DefaultSkipper,
+ Level: -1,
+ MinLength: 0,
+}
+```
diff --git a/site/src/content/docs/middleware/jwt.md b/site/src/content/docs/middleware/jwt.md
new file mode 100644
index 00000000..de82fccc
--- /dev/null
+++ b/site/src/content/docs/middleware/jwt.md
@@ -0,0 +1,126 @@
+---
+title: JWT
+description: JSON Web Token authentication middleware provided by the echo-jwt module.
+sidebar:
+ order: 10
+---
+
+The JWT middleware provides JSON Web Token (JWT) authentication. It lives in a separate
+module: [github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt).
+
+Behavior:
+
+- For a valid token, it sets the user in the context and calls the next handler.
+- For an invalid token, it sends a `401 Unauthorized` response.
+- For a missing or invalid `Authorization` header, it sends a `400 Bad Request` response.
+
+## Dependencies
+
+```go
+import "github.com/labstack/echo-jwt/v5"
+```
+
+## Usage
+
+```go
+e.Use(echojwt.JWT([]byte("secret")))
+```
+
+## Custom configuration
+
+```go
+e.Use(echojwt.WithConfig(echojwt.Config{
+ SigningKey: []byte("secret"),
+}))
+```
+
+## Configuration
+
+```go
+type Config struct {
+ // Skipper defines a function to skip middleware.
+ Skipper middleware.Skipper
+
+ // BeforeFunc defines a function which is executed just before the middleware.
+ BeforeFunc middleware.BeforeFunc
+
+ // SuccessHandler defines a function executed for a valid token. If it returns an
+ // error, the middleware stops the handler chain and returns that error.
+ SuccessHandler func(c *echo.Context) error
+
+ // ErrorHandler defines a function executed when all lookups have been done and none
+ // passed the Validator. It runs with the last missing (ErrExtractionValueMissing)
+ // or invalid key, and may be used to define a custom JWT error.
+ //
+ // Note: when the error handler swallows the error (returns nil), the middleware
+ // continues the handler chain. This is useful when part of your site/api is public
+ // and offers extra features for authorized users; the handler can set a default
+ // public JWT token value in the request and continue.
+ ErrorHandler func(c *echo.Context, err error) error
+
+ // ContinueOnIgnoredError allows the next middleware/handler to be called when the
+ // ErrorHandler ignores the error (returns nil).
+ ContinueOnIgnoredError bool
+
+ // ContextKey is the key under which user information from the token is stored in the context.
+ // Optional. Default value "user".
+ ContextKey string
+
+ // SigningKey is the signing key used to validate the token. One of the three options
+ // to provide a token validation key. Order of precedence: user-defined KeyFunc,
+ // SigningKeys, then SigningKey.
+ // Required if neither a user-defined KeyFunc nor SigningKeys is provided.
+ SigningKey any
+
+ // SigningKeys is a map of signing keys to validate tokens using the kid field. One of
+ // the three options to provide a token validation key.
+ // Required if neither a user-defined KeyFunc nor SigningKey is provided.
+ SigningKeys map[string]any
+
+ // SigningMethod is the signing method used to check the token's signing algorithm.
+ // Not checked when a user-defined KeyFunc is provided.
+ // Optional. Default value HS256.
+ SigningMethod string
+
+ // KeyFunc supplies the public key for token validation. It must verify the signing
+ // algorithm and select the proper key. Useful when tokens are issued by an external
+ // party. When provided, SigningKey, SigningKeys and SigningMethod are ignored.
+ // One of the three options to provide a token validation key, and not used if a
+ // custom ParseTokenFunc is set.
+ KeyFunc jwt.Keyfunc
+
+ // TokenLookup is a string in the form ":" or
+ // ":,:" used to extract the token from the request.
+ // Optional. Default value "header:Authorization".
+ // Possible values:
+ // - "header:" or "header::"
+ // trims a static prefix from the extracted value. For JWT tokens with
+ // `Authorization: Bearer `, the prefix to cut is `Bearer ` (note the space).
+ // If the prefix is empty, the whole value is returned.
+ // - "query:"
+ // - "param:"
+ // - "cookie:"
+ // - "form:"
+ // Multiple sources example: "header:Authorization:Bearer ,cookie:myowncookie".
+ TokenLookup string
+
+ // TokenLookupFuncs is a list of user-defined functions that extract the JWT token
+ // from the context. One of two options to provide a token extractor. Order of
+ // precedence: TokenLookupFuncs, then TokenLookup. Both may be provided.
+ TokenLookupFuncs []middleware.ValuesExtractor
+
+ // ParseTokenFunc parses the token from the given auth string, returning an error when
+ // parsing fails or the token is invalid.
+ // Defaults to an implementation using github.com/golang-jwt/jwt.
+ ParseTokenFunc func(c *echo.Context, auth string) (any, error)
+
+ // NewClaimsFunc returns the extendable claims defining token content. Used by the
+ // default ParseTokenFunc; not used if a custom ParseTokenFunc is set.
+ // Optional. Defaults to a function returning jwt.MapClaims.
+ NewClaimsFunc func(c *echo.Context) jwt.Claims
+}
+```
+
+## Example
+
+See the [JWT cookbook](/cookbook/jwt/) for a complete example.
diff --git a/site/src/content/docs/middleware/key-auth.md b/site/src/content/docs/middleware/key-auth.md
new file mode 100644
index 00000000..be881c74
--- /dev/null
+++ b/site/src/content/docs/middleware/key-auth.md
@@ -0,0 +1,91 @@
+---
+title: Key Auth
+description: Key-based authentication middleware that validates an API key from header, query, form, or cookie.
+sidebar:
+ order: 11
+---
+
+Key Auth middleware provides key-based authentication.
+
+- For a valid key it calls the next handler.
+- For an invalid key, it sends a `401 Unauthorized` response.
+- For a missing key, it sends a `400 Bad Request` response.
+
+## Usage
+
+```go
+e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) {
+ return key == "valid-key", nil
+}))
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
+ KeyLookup: "query:api-key",
+ Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) {
+ return key == "valid-key", nil
+ },
+}))
+```
+
+## Configuration
+
+```go
+type KeyAuthConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // KeyLookup is a string in the form ":" or
+ // ":,:" used to extract the key from the request.
+ // Optional. Default value "header:Authorization:Bearer ".
+ // Possible values:
+ // - "header:" or "header::"
+ // trims a static prefix from the extracted value. For
+ // `Authorization: Basic `, the prefix to remove is `Basic `.
+ // - "query:"
+ // - "form:"
+ // - "cookie:"
+ // Multiple sources example: "header:Authorization,header:X-Api-Key".
+ KeyLookup string
+
+ // AllowedCheckLimit sets how many KeyLookup values are allowed to be checked. This is
+ // useful in environments such as corporate test setups with application proxies
+ // restricting access with their own auth scheme.
+ AllowedCheckLimit uint
+
+ // Validator validates the key.
+ // Required.
+ Validator KeyAuthValidator
+
+ // ErrorHandler defines a function executed when all lookups have been done and none
+ // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) or
+ // invalid key, and may be used to define a custom error.
+ //
+ // Note: when the error handler swallows the error (returns nil), the middleware
+ // continues the handler chain. This is useful when part of your site/api is public
+ // and offers extra features for authorized users.
+ ErrorHandler KeyAuthErrorHandler
+
+ // ContinueOnIgnoredError allows the next middleware/handler to be called when the
+ // ErrorHandler ignores the error (returns nil).
+ ContinueOnIgnoredError bool
+}
+```
+
+The `Validator` has the signature:
+
+```go
+type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error)
+```
+
+### Default configuration
+
+```go
+DefaultKeyAuthConfig = KeyAuthConfig{
+ Skipper: DefaultSkipper,
+ KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ",
+}
+```
diff --git a/site/src/content/docs/middleware/logger.md b/site/src/content/docs/middleware/logger.md
new file mode 100644
index 00000000..a3699705
--- /dev/null
+++ b/site/src/content/docs/middleware/logger.md
@@ -0,0 +1,236 @@
+---
+title: Request Logger
+description: Fully customizable request logging that integrates with structured logging libraries.
+sidebar:
+ order: 12
+---
+
+`RequestLogger` middleware logs information about each HTTP request. It lets you fully
+customize what is logged and how, making it well suited for use with third-party
+(structured logging) libraries.
+
+The values the logger can extract are controlled by the boolean and slice fields of
+`RequestLoggerConfig`. Enable a field (for example `LogStatus: true`) to have its value
+populated on the `RequestLoggerValues` passed to your `LogValuesFunc`.
+
+```go
+type RequestLoggerConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // BeforeNextFunc is called before the next middleware or handler in the chain.
+ BeforeNextFunc func(c *echo.Context)
+
+ // LogValuesFunc is called with the values extracted by the logger from the
+ // request/response.
+ // Mandatory.
+ LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error
+
+ // HandleError instructs the logger to call the global error handler when the next
+ // middleware/handler returns an error. A side effect is that the response is then
+ // committed and sent, so middlewares up the chain can no longer change the status
+ // code or body.
+ HandleError bool
+
+ // LogLatency records the duration of the rest of the handler chain (the next(c) call).
+ LogLatency bool
+ // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2).
+ LogProtocol bool
+ // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details.
+ LogRemoteIP bool
+ // LogHost extracts the request host value (for example example.com).
+ LogHost bool
+ // LogMethod extracts the request method (for example GET).
+ LogMethod bool
+ // LogURI extracts the request URI (for example /list?lang=en&page=1).
+ LogURI bool
+ // LogURIPath extracts the request URI path part (for example /list).
+ LogURIPath bool
+ // LogRoutePath extracts the route path the request matched (for example /user/:id).
+ LogRoutePath bool
+ // LogRequestID extracts the request ID from the X-Request-ID request header, or the
+ // response if the request did not have a value.
+ LogRequestID bool
+ // LogReferer extracts the request referer value.
+ LogReferer bool
+ // LogUserAgent extracts the request user agent value.
+ LogUserAgent bool
+ // LogStatus extracts the response status code. If the chain returns an echo.HTTPError,
+ // the status code is taken from it.
+ LogStatus bool
+ // LogError extracts the error returned from the handler chain.
+ LogError bool
+ // LogContentLength extracts the Content-Length header value. Note: this can differ
+ // from the actual request body size as it may be spoofed.
+ LogContentLength bool
+ // LogResponseSize extracts the response content length. Note: when used with Gzip
+ // middleware this value may not always be correct.
+ LogResponseSize bool
+ // LogHeaders extracts the given list of request headers. A slice of values is logged
+ // per header since a request can contain more than one. Names are canonicalized with
+ // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding").
+ LogHeaders []string
+ // LogQueryParams extracts the given list of query parameters from the request URI. A
+ // slice of values is logged per name since a request can repeat a parameter.
+ LogQueryParams []string
+ // LogFormValues extracts the given list of form values from the request body and URI.
+ // A slice of values is logged per name since a request can repeat a value.
+ LogFormValues []string
+}
+```
+
+## Examples
+
+### fmt.Printf
+
+```go
+skipper := func(c *echo.Context) bool {
+ // Skip the health check endpoint.
+ return c.Request().URL.Path == "/health"
+}
+e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+ LogStatus: true,
+ LogURI: true,
+ Skipper: skipper,
+ BeforeNextFunc: func(c *echo.Context) {
+ c.Set("customValueFromContext", 42)
+ },
+ LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
+ value, _ := c.Get("customValueFromContext").(int)
+ fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value)
+ return nil
+ },
+}))
+```
+
+Sample output:
+
+```text
+REQUEST: uri: /hello, status: 200, custom-value: 42
+```
+
+### slog ([log/slog](https://pkg.go.dev/log/slog))
+
+```go
+logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
+e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+ LogStatus: true,
+ LogURI: true,
+ LogError: true,
+ HandleError: true, // forwards the error to the global error handler so it can pick the status code
+ LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
+ if v.Error == nil {
+ logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
+ slog.String("uri", v.URI),
+ slog.Int("status", v.Status),
+ )
+ } else {
+ logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
+ slog.String("uri", v.URI),
+ slog.Int("status", v.Status),
+ slog.String("err", v.Error.Error()),
+ )
+ }
+ return nil
+ },
+}))
+```
+
+Sample output:
+
+```text
+{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200}
+```
+
+### Zerolog ([rs/zerolog](https://github.com/rs/zerolog))
+
+```go
+logger := zerolog.New(os.Stdout)
+e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+ LogURI: true,
+ LogStatus: true,
+ LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
+ logger.Info().
+ Str("URI", v.URI).
+ Int("status", v.Status).
+ Msg("request")
+ return nil
+ },
+}))
+```
+
+Sample output:
+
+```text
+{"level":"info","URI":"/hello","status":200,"message":"request"}
+```
+
+### Zap ([uber-go/zap](https://github.com/uber-go/zap))
+
+```go
+logger, _ := zap.NewProduction()
+e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+ LogURI: true,
+ LogStatus: true,
+ LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
+ logger.Info("request",
+ zap.String("URI", v.URI),
+ zap.Int("status", v.Status),
+ )
+ return nil
+ },
+}))
+```
+
+Sample output:
+
+```text
+{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200}
+```
+
+### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus))
+
+```go
+log := logrus.New()
+e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+ LogURI: true,
+ LogStatus: true,
+ LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error {
+ log.WithFields(logrus.Fields{
+ "URI": values.URI,
+ "status": values.Status,
+ }).Info("request")
+ return nil
+ },
+}))
+```
+
+Sample output:
+
+```text
+time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200
+```
+
+## Troubleshooting
+
+### panic: missing LogValuesFunc callback function for request logger middleware
+
+This panic occurs when the mandatory `LogValuesFunc` callback is left unset. Define a
+function matching the `LogValuesFunc` signature and assign it in the configuration:
+
+```go
+func logValues(c *echo.Context, v middleware.RequestLoggerValues) error {
+ fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI)
+ return nil
+}
+
+e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+ LogValuesFunc: logValues,
+}))
+```
+
+### Parameters in logs are empty
+
+If values such as `v.URI` and `v.Status` are empty inside `LogValuesFunc`, check that the
+corresponding extraction flags (`LogStatus`, `LogURI`, and so on) are set to `true` in
+the configuration. Each value is only populated when its flag is enabled.
diff --git a/site/src/content/docs/middleware/method-override.md b/site/src/content/docs/middleware/method-override.md
new file mode 100644
index 00000000..e4cb01d1
--- /dev/null
+++ b/site/src/content/docs/middleware/method-override.md
@@ -0,0 +1,52 @@
+---
+title: Method Override
+description: Override the HTTP method of a POST request via header, form, or query value.
+sidebar:
+ order: 13
+---
+
+Method Override middleware reads the overridden method from the request and uses it
+instead of the original method.
+
+:::note
+For security reasons, only the `POST` method can be overridden.
+:::
+
+## Usage
+
+```go
+e.Pre(middleware.MethodOverride())
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{
+ Getter: middleware.MethodFromForm("_method"),
+}))
+```
+
+The method can be sourced with `MethodFromHeader`, `MethodFromForm`, or `MethodFromQuery`.
+
+## Configuration
+
+```go
+type MethodOverrideConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Getter is a function that gets the overridden method from the request.
+ // Optional. Default value MethodFromHeader(echo.HeaderXHTTPMethodOverride).
+ Getter MethodOverrideGetter
+}
+```
+
+### Default configuration
+
+```go
+DefaultMethodOverrideConfig = MethodOverrideConfig{
+ Skipper: DefaultSkipper,
+ Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride),
+}
+```
diff --git a/site/src/content/docs/middleware/open-telemetry.md b/site/src/content/docs/middleware/open-telemetry.md
new file mode 100644
index 00000000..1f97e2b2
--- /dev/null
+++ b/site/src/content/docs/middleware/open-telemetry.md
@@ -0,0 +1,81 @@
+---
+title: OpenTelemetry
+description: OpenTelemetry instrumentation for HTTP requests in Echo.
+sidebar:
+ order: 14
+---
+
+[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) is a middleware that
+provides OpenTelemetry instrumentation for HTTP requests.
+
+OpenTelemetry is a set of open-source tools that provide instrumentation for cloud-native
+applications.
+
+- [OpenTelemetry Exporters](https://opentelemetry.io/docs/languages/go/exporters/)
+- [OpenTelemetry HTTP spec](https://opentelemetry.io/docs/specs/semconv/http/)
+- [HTTP metrics spec](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/)
+
+## Usage
+
+Add the OpenTelemetry middleware dependency with Go modules:
+
+```bash
+go get github.com/labstack/echo-opentelemetry
+```
+
+Import the middleware and the OpenTelemetry trace API:
+
+```go
+import (
+ echootel "github.com/labstack/echo-opentelemetry"
+ "go.opentelemetry.io/otel/trace"
+)
+```
+
+Register it with full configuration:
+
+```go
+e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{
+ ServerName: "my-server",
+ TracerProvider: tp,
+
+ //Skipper: nil,
+ //OnNextError: nil,
+ //OnExtractionError: nil,
+ //MeterProvider: nil,
+ //Propagators: nil,
+ //SpanStartOptions: nil,
+ //SpanStartAttributes: nil,
+ //SpanEndAttributes: nil,
+ //MetricAttributes: nil,
+ //Metrics: nil,
+}))
+```
+
+For configuration options, see the [`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28) struct.
+
+Add the middleware in simplified form by providing only the server name:
+
+```go
+e.Use(echootel.NewMiddleware("app.example.com"))
+```
+
+Add the middleware with configuration options:
+
+```go
+e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{
+ TracerProvider: tp,
+}))
+```
+
+Retrieve the tracer from the Echo context:
+
+```go
+tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey)
+```
+
+## Example
+
+The [example](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) exports
+metrics and spans to stdout, but you can use any exporter (OTLP, etc.). See the
+[OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters) documentation.
diff --git a/site/src/content/docs/middleware/prometheus.md b/site/src/content/docs/middleware/prometheus.md
new file mode 100644
index 00000000..3a70a999
--- /dev/null
+++ b/site/src/content/docs/middleware/prometheus.md
@@ -0,0 +1,284 @@
+---
+title: Prometheus
+description: Generate Prometheus metrics for HTTP requests in Echo.
+sidebar:
+ order: 15
+---
+
+[Echo Prometheus](https://github.com/labstack/echo-prometheus) middleware generates Prometheus
+metrics for HTTP requests.
+
+## Usage
+
+Add the required module:
+
+```bash
+go get github.com/labstack/echo-prometheus
+```
+
+Add the Prometheus middleware and a route to serve the gathered metrics:
+
+```go
+e := echo.New()
+e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics
+e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics
+```
+
+## Examples
+
+Serve metrics from the same server that gathers them:
+
+```go
+package main
+
+import (
+ "net/http"
+
+ echoprometheus "github.com/labstack/echo-prometheus"
+ "github.com/labstack/echo/v5"
+)
+
+func main() {
+ e := echo.New()
+
+ e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics
+ e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics
+
+ e.GET("/hello", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "hello")
+ })
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+Serve metrics on a separate port:
+
+```go
+func main() {
+ e := echo.New() // this Echo instance will serve routes on port 8080
+ e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics
+
+ go func() {
+ metrics := echo.New() // this Echo will run on separate port 8081
+ metrics.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics
+ if err := metrics.Start(":8081"); err != nil {
+ e.Logger.Error("failed to start metrics server", "error", err)
+ }
+ }()
+
+ e.GET("/hello", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "hello")
+ })
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+Sample output (for the first example):
+
+```bash
+curl http://localhost:8080/metrics
+
+# HELP echo_request_duration_seconds The HTTP request latencies in seconds.
+# TYPE echo_request_duration_seconds summary
+echo_request_duration_seconds_sum 0.41086482
+echo_request_duration_seconds_count 1
+# HELP echo_request_size_bytes The HTTP request sizes in bytes.
+# TYPE echo_request_size_bytes summary
+echo_request_size_bytes_sum 56
+echo_request_size_bytes_count 1
+# HELP echo_requests_total How many HTTP requests processed, partitioned by status code and HTTP method.
+# TYPE echo_requests_total counter
+echo_requests_total{code="200",host="localhost:8080",method="GET",url="/"} 1
+# HELP echo_response_size_bytes The HTTP response sizes in bytes.
+# TYPE echo_response_size_bytes summary
+echo_response_size_bytes_sum 61
+echo_response_size_bytes_count 1
+...
+```
+
+## Custom configuration
+
+### Serving custom Prometheus metrics
+
+Use custom metrics with the Prometheus default registry:
+
+```go
+package main
+
+import (
+ "log"
+
+ echoprometheus "github.com/labstack/echo-prometheus"
+ "github.com/labstack/echo/v5"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+func main() {
+ e := echo.New() // this Echo instance will serve routes on port 8080
+
+ customCounter := prometheus.NewCounter( // create a new counter metric
+ prometheus.CounterOpts{
+ Name: "custom_requests_total",
+ Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
+ },
+ )
+ if err := prometheus.Register(customCounter); err != nil { // register the counter with the default registry
+ log.Fatal(err)
+ }
+
+ e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
+ AfterNext: func(c *echo.Context, err error) {
+ customCounter.Inc() // increment the counter after every request
+ },
+ }))
+ e.GET("/metrics", echoprometheus.NewHandler()) // register a route to serve gathered metrics
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+Or create your own registry and register custom metrics with it:
+
+```go
+package main
+
+import (
+ "log"
+
+ echoprometheus "github.com/labstack/echo-prometheus"
+ "github.com/labstack/echo/v5"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+func main() {
+ e := echo.New() // this Echo instance will serve routes on port 8080
+
+ customRegistry := prometheus.NewRegistry() // create a custom registry for your custom metrics
+ customCounter := prometheus.NewCounter( // create a new counter metric
+ prometheus.CounterOpts{
+ Name: "custom_requests_total",
+ Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
+ },
+ )
+ if err := customRegistry.Register(customCounter); err != nil { // register the counter with the custom registry
+ log.Fatal(err)
+ }
+
+ e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
+ AfterNext: func(c *echo.Context, err error) {
+ customCounter.Inc() // increment the counter after every request
+ },
+ Registerer: customRegistry, // use the custom registry instead of the default Prometheus registry
+ }))
+ e.GET("/metrics", echoprometheus.NewHandlerWithConfig(echoprometheus.HandlerConfig{Gatherer: customRegistry})) // serve metrics from the custom registry
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+### Skipping URLs
+
+A skipper can be passed to avoid generating metrics for certain URLs:
+
+```go
+package main
+
+import (
+ "net/http"
+ "strings"
+
+ echoprometheus "github.com/labstack/echo-prometheus"
+ "github.com/labstack/echo/v5"
+)
+
+func main() {
+ e := echo.New() // this Echo instance will serve routes on port 8080
+
+ mwConfig := echoprometheus.MiddlewareConfig{
+ Skipper: func(c *echo.Context) bool {
+ return strings.HasPrefix(c.Path(), "/testurl")
+ }, // does not gather metrics on routes starting with `/testurl`
+ }
+ e.Use(echoprometheus.NewMiddlewareWithConfig(mwConfig)) // adds middleware to gather metrics
+
+ e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics
+
+ e.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "Hello, World!")
+ })
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Complex scenarios
+
+Modify the default `echoprometheus` metric definitions:
+
+```go
+package main
+
+import (
+ "net/http"
+
+ echoprometheus "github.com/labstack/echo-prometheus"
+ "github.com/labstack/echo/v5"
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+func main() {
+ e := echo.New() // this Echo instance will serve routes on port 8080
+
+ e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
+ // Labels of default metrics can be modified or added with the `LabelFuncs` function.
+ LabelFuncs: map[string]echoprometheus.LabelValueFunc{
+ "scheme": func(c *echo.Context, err error) string { // additional custom label
+ return c.Scheme()
+ },
+ "host": func(c *echo.Context, err error) string { // overrides the default 'host' label value
+ return "y_" + c.Request().Host
+ },
+ },
+ // The `echoprometheus` middleware registers the following metrics by default:
+ // - Histogram: request_duration_seconds
+ // - Histogram: response_size_bytes
+ // - Histogram: request_size_bytes
+ // - Counter: requests_total
+ // which can be modified with the `HistogramOptsFunc` and `CounterOptsFunc` functions.
+ HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts {
+ if opts.Name == "request_duration_seconds" {
+ opts.Buckets = []float64{1000.0, 10_000.0, 100_000.0, 1_000_000.0} // 1KB, 10KB, 100KB, 1MB
+ }
+ return opts
+ },
+ CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts {
+ if opts.Name == "requests_total" {
+ opts.ConstLabels = prometheus.Labels{"my_const": "123"}
+ }
+ return opts
+ },
+ })) // adds middleware to gather metrics
+
+ e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics
+
+ e.GET("/hello", func(c *echo.Context) error {
+ return c.String(http.StatusOK, "hello")
+ })
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
diff --git a/site/src/content/docs/middleware/proxy.md b/site/src/content/docs/middleware/proxy.md
new file mode 100644
index 00000000..f33c1756
--- /dev/null
+++ b/site/src/content/docs/middleware/proxy.md
@@ -0,0 +1,134 @@
+---
+title: Proxy
+description: HTTP and WebSocket reverse proxy middleware with load balancing.
+sidebar:
+ order: 16
+---
+
+Proxy provides an HTTP/WebSocket reverse proxy middleware. It forwards a request to an
+upstream server using a configured load balancing technique.
+
+## Usage
+
+```go
+url1, err := url.Parse("http://localhost:8081")
+if err != nil {
+ e.Logger.Error("failed to parse url", "error", err)
+}
+url2, err := url.Parse("http://localhost:8082")
+if err != nil {
+ e.Logger.Error("failed to parse url", "error", err)
+}
+e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{
+ {
+ URL: url1,
+ },
+ {
+ URL: url2,
+ },
+})))
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{}))
+```
+
+## Configuration
+
+```go
+type ProxyConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Balancer defines a load balancing technique.
+ // Required.
+ Balancer ProxyBalancer
+
+ // RetryCount defines the number of times a failed proxied request should be retried
+ // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried.
+ RetryCount int
+
+ // RetryFilter defines a function used to determine if a failed request to a
+ // ProxyTarget should be retried. The RetryFilter will only be called when the number
+ // of previous retries is less than RetryCount. If the function returns true, the
+ // request will be retried. The provided error indicates the reason for the request
+ // failure. When the ProxyTarget is unavailable, the error will be an instance of
+ // echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error
+ // will indicate an internal error in the Proxy middleware. When a RetryFilter is not
+ // specified, all requests that fail with http.StatusBadGateway will be retried. A custom
+ // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is
+ // only called when the request to the target fails, or an internal error in the Proxy
+ // middleware has occurred. Successful requests that return a non-200 response code cannot
+ // be retried.
+ RetryFilter func(c *echo.Context, e error) bool
+
+ // ErrorHandler defines a function which can be used to return custom errors from
+ // the Proxy middleware. ErrorHandler is only invoked when there has been
+ // either an internal error in the Proxy middleware or the ProxyTarget is
+ // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked
+ // when a ProxyTarget returns a non-200 response. In these cases, the response
+ // is already written so errors cannot be modified. ErrorHandler is only
+ // invoked after all retry attempts have been exhausted.
+ ErrorHandler func(c *echo.Context, err error) error
+
+ // Rewrite defines URL path rewrite rules. The values captured in asterisk can be
+ // retrieved by index e.g. $1, $2 and so on.
+ // Examples:
+ // "/old": "/new",
+ // "/api/*": "/$1",
+ // "/js/*": "/public/javascripts/$1",
+ // "/users/*/orders/*": "/user/$1/order/$2",
+ Rewrite map[string]string
+
+ // RegexRewrite defines rewrite rules using regexp.Regexp with captures.
+ // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.
+ // Example:
+ // "^/old/[0.9]+/": "/new",
+ // "^/api/.+?/(.*)": "/v2/$1",
+ RegexRewrite map[*regexp.Regexp]string
+
+ // Context key to store selected ProxyTarget into context.
+ // Optional. Default value "target".
+ ContextKey string
+
+ // To customize the transport to remote.
+ // Examples: If custom TLS certificates are required.
+ Transport http.RoundTripper
+
+ // ModifyResponse defines function to modify response from ProxyTarget.
+ ModifyResponse func(*http.Response) error
+}
+```
+
+### Default configuration
+
+| Name | Value |
+| ---------- | -------------- |
+| Skipper | DefaultSkipper |
+| ContextKey | `target` |
+
+### Regex-based rules
+
+For advanced rewriting of proxy requests, rules may also be defined using regular
+expressions. Normal capture groups can be defined using `()` and referenced by index
+(`$1`, `$2`, ...) in the rewritten path.
+
+`RegexRewrite` and normal `Rewrite` rules can be combined.
+
+```go
+e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{
+ Balancer: rrb,
+ Rewrite: map[string]string{
+ "^/v1/*": "/v2/$1",
+ },
+ RegexRewrite: map[*regexp.Regexp]string{
+ regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1",
+ regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1",
+ },
+}))
+```
+
+See the [reverse proxy](/cookbook/reverse-proxy/) cookbook for a complete example.
diff --git a/site/src/content/docs/middleware/rate-limiter.md b/site/src/content/docs/middleware/rate-limiter.md
new file mode 100644
index 00000000..0c8dd335
--- /dev/null
+++ b/site/src/content/docs/middleware/rate-limiter.md
@@ -0,0 +1,109 @@
+---
+title: Rate Limiter
+description: Limit the number of requests from a particular IP or identifier within a time period.
+sidebar:
+ order: 17
+---
+
+`RateLimiter` provides a rate limiter middleware that limits the number of requests sent to
+the server from a particular IP or identifier within a time period.
+
+By default, an in-memory store keeps track of requests. The default in-memory implementation
+is focused on correctness and may not be the best option for a high number of concurrent
+requests or a large number of distinct identifiers (>16k).
+
+## Usage
+
+To add a rate limit to your application, add the `RateLimiter` middleware. The example below
+limits the application to 20 requests/sec using the default in-memory store:
+
+```go
+e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0)))
+```
+
+:::note
+If the provided rate is a float number, `Burst` is treated as the rounded-down value of the rate.
+:::
+
+## Custom configuration
+
+```go
+config := middleware.RateLimiterConfig{
+ Skipper: middleware.DefaultSkipper,
+ Store: middleware.NewRateLimiterMemoryStoreWithConfig(
+ middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute},
+ ),
+ IdentifierExtractor: func(c *echo.Context) (string, error) {
+ id := c.RealIP()
+ return id, nil
+ },
+ ErrorHandler: func(c *echo.Context, err error) error {
+ return c.JSON(http.StatusForbidden, nil)
+ },
+ DenyHandler: func(c *echo.Context, identifier string, err error) error {
+ return c.JSON(http.StatusTooManyRequests, nil)
+ },
+}
+
+e.Use(middleware.RateLimiterWithConfig(config))
+```
+
+### Errors
+
+```go
+var (
+ // ErrRateLimitExceeded denotes an error raised when the rate limit is exceeded.
+ ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
+ // ErrExtractorError denotes an error raised when the extractor function is unsuccessful.
+ ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier")
+)
+```
+
+:::tip
+To implement your own store, satisfy the `RateLimiterStore` interface and pass it to
+`RateLimiterConfig`.
+:::
+
+## Configuration
+
+```go
+type RateLimiterConfig struct {
+ Skipper Skipper
+ BeforeFunc BeforeFunc
+ // IdentifierExtractor uses echo.Context to extract the identifier for a visitor.
+ IdentifierExtractor Extractor
+ // Store defines a store for the rate limiter.
+ Store RateLimiterStore
+ // ErrorHandler provides a handler to be called when IdentifierExtractor returns a non-nil error.
+ ErrorHandler func(c *echo.Context, err error) error
+ // DenyHandler provides a handler to be called when RateLimiter denies access.
+ DenyHandler func(c *echo.Context, identifier string, err error) error
+}
+```
+
+### Default configuration
+
+```go
+// DefaultRateLimiterConfig defines default values for RateLimiterConfig.
+var DefaultRateLimiterConfig = RateLimiterConfig{
+ Skipper: DefaultSkipper,
+ IdentifierExtractor: func(c *echo.Context) (string, error) {
+ id := c.RealIP()
+ return id, nil
+ },
+ ErrorHandler: func(c *echo.Context, err error) error {
+ return &echo.HTTPError{
+ Code: ErrExtractorError.Code,
+ Message: ErrExtractorError.Message,
+ Internal: err,
+ }
+ },
+ DenyHandler: func(c *echo.Context, identifier string, err error) error {
+ return &echo.HTTPError{
+ Code: ErrRateLimitExceeded.Code,
+ Message: ErrRateLimitExceeded.Message,
+ Internal: err,
+ }
+ },
+}
+```
diff --git a/site/src/content/docs/middleware/recover.md b/site/src/content/docs/middleware/recover.md
new file mode 100644
index 00000000..0146171e
--- /dev/null
+++ b/site/src/content/docs/middleware/recover.md
@@ -0,0 +1,61 @@
+---
+title: Recover
+description: Recover from panics anywhere in the chain and delegate to the centralized error handler.
+sidebar:
+ order: 18
+---
+
+Recover middleware recovers from panics anywhere in the chain, prints the stack trace, and
+passes control to the centralized
+[HTTPErrorHandler](/guide/customization/#http-error-handler).
+
+## Usage
+
+```go
+e.Use(middleware.Recover())
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
+ StackSize: 1 << 10, // 1 KB
+}))
+```
+
+The example above uses a `StackSize` of 1 KB and default values for `DisableStackAll` and
+`DisablePrintStack`.
+
+## Configuration
+
+```go
+type RecoverConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Size of the stack to be printed.
+ // Optional. Default value 4KB.
+ StackSize int
+
+ // DisableStackAll disables formatting stack traces of all other goroutines
+ // into the buffer after the trace for the current goroutine.
+ // Optional. Default value false.
+ DisableStackAll bool
+
+ // DisablePrintStack disables printing the stack trace.
+ // Optional. Default value false.
+ DisablePrintStack bool
+}
+```
+
+### Default configuration
+
+```go
+var DefaultRecoverConfig = RecoverConfig{
+ Skipper: DefaultSkipper,
+ StackSize: 4 << 10, // 4 KB
+ DisableStackAll: false,
+ DisablePrintStack: false,
+}
+```
diff --git a/site/src/content/docs/middleware/redirect.md b/site/src/content/docs/middleware/redirect.md
new file mode 100644
index 00000000..b071f0ec
--- /dev/null
+++ b/site/src/content/docs/middleware/redirect.md
@@ -0,0 +1,101 @@
+---
+title: Redirect
+description: Redirect requests between HTTP/HTTPS and www/non-www variants.
+sidebar:
+ order: 19
+---
+
+## HTTPS Redirect
+
+HTTPS redirect middleware redirects HTTP requests to HTTPS. For example,
+`http://labstack.com` is redirected to `https://labstack.com`.
+
+### Usage
+
+```go
+e := echo.New()
+e.Pre(middleware.HTTPSRedirect())
+```
+
+## HTTPS WWW Redirect
+
+HTTPS WWW redirect redirects HTTP requests to www HTTPS. For example,
+`http://labstack.com` is redirected to `https://www.labstack.com`.
+
+### Usage
+
+```go
+e := echo.New()
+e.Pre(middleware.HTTPSWWWRedirect())
+```
+
+## HTTPS NonWWW Redirect
+
+HTTPS NonWWW redirect redirects HTTP requests to non-www HTTPS. For example,
+`http://www.labstack.com` is redirected to `https://labstack.com`.
+
+### Usage
+
+```go
+e := echo.New()
+e.Pre(middleware.HTTPSNonWWWRedirect())
+```
+
+## WWW Redirect
+
+WWW redirect redirects non-www requests to www. For example, `http://labstack.com` is
+redirected to `http://www.labstack.com`.
+
+### Usage
+
+```go
+e := echo.New()
+e.Pre(middleware.WWWRedirect())
+```
+
+## NonWWW Redirect
+
+NonWWW redirect redirects www requests to non-www. For example, `http://www.labstack.com` is
+redirected to `http://labstack.com`.
+
+### Usage
+
+```go
+e := echo.New()
+e.Pre(middleware.NonWWWRedirect())
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{
+ Code: http.StatusTemporaryRedirect,
+}))
+```
+
+The example above redirects HTTP requests to HTTPS with status code
+`307 - StatusTemporaryRedirect`.
+
+## Configuration
+
+```go
+type RedirectConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Status code to be used when redirecting the request.
+ // Optional. Default value http.StatusMovedPermanently.
+ Code int
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset.
+RedirectConfig{
+ Skipper: DefaultSkipper,
+ Code: http.StatusMovedPermanently,
+}
+```
diff --git a/site/src/content/docs/middleware/request-id.md b/site/src/content/docs/middleware/request-id.md
new file mode 100644
index 00000000..692f85d9
--- /dev/null
+++ b/site/src/content/docs/middleware/request-id.md
@@ -0,0 +1,89 @@
+---
+title: Request ID
+description: Generate a unique ID for each request.
+sidebar:
+ order: 20
+---
+
+Request ID middleware generates a unique ID for a request.
+
+## Usage
+
+```go
+e.Use(middleware.RequestID())
+```
+
+Example:
+
+```go
+func main() {
+ e := echo.New()
+
+ e.Use(middleware.RequestID())
+
+ e.GET("/", func(c *echo.Context) error {
+ return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID))
+ })
+
+ if err := e.Start(":8080"); err != nil {
+ e.Logger.Error("failed to start server", "error", err)
+ }
+}
+```
+
+## Custom configuration
+
+```go
+e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{
+ Generator: func() string {
+ return customGenerator()
+ },
+}))
+```
+
+## Configuration
+
+```go
+type RequestIDConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Generator defines a function to generate an ID.
+ // Optional. Default value random.String(32).
+ Generator func() string
+
+ // RequestIDHandler defines a function which is executed for a request id.
+ RequestIDHandler func(c *echo.Context, requestID string)
+
+ // TargetHeader defines what header to look for to populate the id.
+ // Optional. Default value is `X-Request-Id`.
+ TargetHeader string
+}
+```
+
+### Default configuration
+
+```go
+// Effective defaults applied when fields are left unset.
+RequestIDConfig{
+ Skipper: DefaultSkipper,
+ Generator: generator, // random 32-character string
+ TargetHeader: echo.HeaderXRequestID,
+}
+```
+
+## Set ID
+
+You can set the ID from the requester with the `X-Request-ID` header.
+
+### Request
+
+```sh
+curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param"
+```
+
+### Log
+
+```js
+{"time":"2017-11-13T20:26:28.6438003+01:00","id":"3","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/?my=param","my":"param","status":200, "latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":13}
+```
diff --git a/site/src/content/docs/middleware/rewrite.md b/site/src/content/docs/middleware/rewrite.md
new file mode 100644
index 00000000..fd89bfaa
--- /dev/null
+++ b/site/src/content/docs/middleware/rewrite.md
@@ -0,0 +1,87 @@
+---
+title: Rewrite
+description: Rewrite the URL path based on configured rules.
+sidebar:
+ order: 21
+---
+
+Rewrite middleware rewrites the URL path based on the provided rules. It is helpful for
+backward compatibility or for creating cleaner and more descriptive links.
+
+## Usage
+
+```go
+e.Pre(middleware.Rewrite(map[string]string{
+ "/old": "/new",
+ "/api/*": "/$1",
+ "/js/*": "/public/javascripts/$1",
+ "/users/*/orders/*": "/user/$1/order/$2",
+}))
+```
+
+The values captured in asterisks can be retrieved by index, e.g. `$1`, `$2`, and so on. Each
+asterisk is non-greedy (translated to a capture group `(.*?)`); when using multiple asterisks,
+a trailing `*` matches the rest of the path.
+
+:::caution
+Rewrite middleware should be registered via `Echo#Pre()` so it runs before the router.
+:::
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{}))
+```
+
+## Configuration
+
+```go
+type RewriteConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // Rules defines the URL path rewrite rules. The values captured in asterisk can be
+ // retrieved by index e.g. $1, $2 and so on.
+ // Example:
+ // "/old": "/new",
+ // "/api/*": "/$1",
+ // "/js/*": "/public/javascripts/$1",
+ // "/users/*/orders/*": "/user/$1/order/$2",
+ // Required.
+ Rules map[string]string
+
+ // RegexRules defines the URL path rewrite rules using regexp.Regexp with captures.
+ // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.
+ // Example:
+ // "^/old/[0.9]+/": "/new",
+ // "^/api/.+?/(.*)": "/v2/$1",
+ RegexRules map[*regexp.Regexp]string
+}
+```
+
+Default configuration:
+
+| Name | Value |
+| ------- | -------------- |
+| Skipper | DefaultSkipper |
+
+### Regex-based rules
+
+For advanced rewriting of paths, rules may also be defined using regular expressions. Normal
+capture groups can be defined using `()` and referenced by index (`$1`, `$2`, ...) in the
+rewritten path.
+
+`RegexRules` and normal `Rules` can be combined.
+
+```go
+e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{
+ Rules: map[string]string{
+ "^/v1/*": "/v2/$1",
+ },
+ RegexRules: map[*regexp.Regexp]string{
+ regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1",
+ regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1",
+ },
+}))
+```
diff --git a/site/src/content/docs/middleware/secure.md b/site/src/content/docs/middleware/secure.md
new file mode 100644
index 00000000..d7a3d3b5
--- /dev/null
+++ b/site/src/content/docs/middleware/secure.md
@@ -0,0 +1,113 @@
+---
+title: Secure
+description: Protect against XSS, content sniffing, clickjacking, and other injection attacks.
+sidebar:
+ order: 22
+---
+
+Secure middleware provides protection against cross-site scripting (XSS), content type
+sniffing, clickjacking, insecure connections, and other code injection attacks.
+
+## Usage
+
+```go
+e.Use(middleware.Secure())
+```
+
+## Custom configuration
+
+```go
+e := echo.New()
+e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
+ XSSProtection: "",
+ ContentTypeNosniff: "",
+ XFrameOptions: "",
+ HSTSMaxAge: 3600,
+ ContentSecurityPolicy: "default-src 'self'",
+}))
+```
+
+:::note
+Passing an empty `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions`, or
+`ContentSecurityPolicy` disables that protection.
+:::
+
+## Configuration
+
+```go
+type SecureConfig struct {
+ // Skipper defines a function to skip middleware.
+ Skipper Skipper
+
+ // XSSProtection provides protection against cross-site scripting attack (XSS)
+ // by setting the `X-XSS-Protection` header.
+ // Optional. Default value "1; mode=block".
+ XSSProtection string
+
+ // ContentTypeNosniff provides protection against overriding Content-Type
+ // header by setting the `X-Content-Type-Options` header.
+ // Optional. Default value "nosniff".
+ ContentTypeNosniff string
+
+ // XFrameOptions can be used to indicate whether or not a browser should
+ // be allowed to render a page in a ,