From f0e7bee362c7ab42a78d3fc5ac65aea1ca73b151 Mon Sep 17 00:00:00 2001 From: toim Date: Fri, 9 Jan 2026 17:40:14 +0200 Subject: [PATCH 1/4] cookbook reworked --- cookbook/auto-tls/server.go | 45 ++-- cookbook/cors/origin-func/server.go | 39 ++-- cookbook/cors/origin-list/server.go | 22 +- cookbook/crud/server.go | 22 +- cookbook/embed-resources/.gitignore | 2 - cookbook/embed-resources/app/index.html | 11 - cookbook/embed-resources/app/main.js | 1 - cookbook/embed-resources/server.go | 21 -- cookbook/embed/server.go | 9 +- cookbook/file-download/attachment/server.go | 17 +- cookbook/file-download/inline/server.go | 17 +- cookbook/file-download/server.go | 17 +- cookbook/file-upload/multiple/server.go | 14 +- cookbook/file-upload/single/server.go | 14 +- cookbook/google-app-engine/Dockerfile | 7 - cookbook/google-app-engine/app-engine.go | 17 -- cookbook/google-app-engine/app-engine.yaml | 36 ---- cookbook/google-app-engine/app-managed.go | 25 --- cookbook/google-app-engine/app-managed.yaml | 37 ---- cookbook/google-app-engine/app-standalone.go | 24 --- cookbook/google-app-engine/app.go | 4 - cookbook/google-app-engine/public/favicon.ico | Bin 1150 -> 0 bytes cookbook/google-app-engine/public/index.html | 15 -- .../google-app-engine/public/scripts/main.js | 1 - .../google-app-engine/templates/welcome.html | 1 - cookbook/google-app-engine/users.go | 54 ----- cookbook/google-app-engine/welcome.go | 31 --- cookbook/graceful-shutdown/server.go | 41 +++- cookbook/hello-world/server.go | 14 +- cookbook/http2-server-push/server.go | 18 +- cookbook/http2/server.go | 10 +- cookbook/jsonp/server.go | 14 +- cookbook/jwt/custom-claims/server.go | 32 +-- cookbook/jwt/user-defined-keyfunc/server.go | 23 +- cookbook/load-balancing/upstream/server.go | 17 +- cookbook/middleware/server.go | 48 +++-- cookbook/reverse-proxy/server.go | 30 ++- cookbook/reverse-proxy/upstream/server.go | 27 ++- cookbook/sse/broadcast/server.go | 18 +- cookbook/sse/simple/server.go | 13 +- cookbook/streaming-response/server.go | 22 +- cookbook/subdomain/server.go | 51 ++--- cookbook/timeout/server.go | 24 ++- cookbook/twitter/handler/handler.go | 16 -- cookbook/twitter/handler/post.go | 73 ------- cookbook/twitter/handler/user.go | 97 --------- cookbook/twitter/model/post.go | 14 -- cookbook/twitter/model/user.go | 15 -- cookbook/twitter/server.go | 53 ----- cookbook/websocket/gorilla/server.go | 22 +- cookbook/websocket/net/server.go | 27 ++- go.mod | 34 +-- go.sum | 104 ++------- website/docs/cookbook/embed-resources.md | 5 - website/docs/cookbook/google-app-engine.md | 127 ----------- website/docs/cookbook/twitter.md | 198 ------------------ website/docs/guide/quick-start.md | 7 +- website/docs/guide/request.md | 5 +- website/docs/guide/routing.md | 8 +- website/docs/middleware/jaeger.md | 20 +- website/docs/middleware/logger.md | 6 +- website/docs/middleware/request-id.md | 5 +- 62 files changed, 475 insertions(+), 1266 deletions(-) delete mode 100644 cookbook/embed-resources/.gitignore delete mode 100644 cookbook/embed-resources/app/index.html delete mode 100644 cookbook/embed-resources/app/main.js delete mode 100644 cookbook/embed-resources/server.go delete mode 100644 cookbook/google-app-engine/Dockerfile delete mode 100644 cookbook/google-app-engine/app-engine.go delete mode 100644 cookbook/google-app-engine/app-engine.yaml delete mode 100644 cookbook/google-app-engine/app-managed.go delete mode 100644 cookbook/google-app-engine/app-managed.yaml delete mode 100644 cookbook/google-app-engine/app-standalone.go delete mode 100644 cookbook/google-app-engine/app.go delete mode 100644 cookbook/google-app-engine/public/favicon.ico delete mode 100644 cookbook/google-app-engine/public/index.html delete mode 100644 cookbook/google-app-engine/public/scripts/main.js delete mode 100644 cookbook/google-app-engine/templates/welcome.html delete mode 100644 cookbook/google-app-engine/users.go delete mode 100644 cookbook/google-app-engine/welcome.go delete mode 100644 cookbook/twitter/handler/handler.go delete mode 100644 cookbook/twitter/handler/post.go delete mode 100644 cookbook/twitter/handler/user.go delete mode 100644 cookbook/twitter/model/post.go delete mode 100644 cookbook/twitter/model/user.go delete mode 100644 cookbook/twitter/server.go delete mode 100644 website/docs/cookbook/google-app-engine.md delete mode 100644 website/docs/cookbook/twitter.md diff --git a/cookbook/auto-tls/server.go b/cookbook/auto-tls/server.go index 830f6d01..d17d0132 100644 --- a/cookbook/auto-tls/server.go +++ b/cookbook/auto-tls/server.go @@ -1,37 +1,56 @@ package main import ( + "context" "crypto/tls" - "golang.org/x/crypto/acme" + "errors" + "log/slog" "net/http" + "os" + + "golang.org/x/crypto/acme" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "golang.org/x/crypto/acme/autocert" ) func main() { e := echo.New() - // e.AutoTLSManager.HostPolicy = autocert.HostWhitelist("") - // Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) - e.AutoTLSManager.Cache = autocert.DirCache("/var/www/.cache") + e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) + e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - e.GET("/", func(c echo.Context) error { + e.Use(middleware.RequestLogger()) + + e.GET("/", func(c *echo.Context) error { return c.HTML(http.StatusOK, `

Welcome to Echo!

TLS certificates automatically installed from Let's Encrypt :)

`) }) - e.Logger.Fatal(e.StartAutoTLS(":443")) + m := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist("example.com", "www.example.com"), + // Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) + Cache: autocert.DirCache("/var/www/.cache"), + // Email: "[email protected]", // optional but recommended + } + + sc := echo.StartConfig{ + Address: ":443", + TLSConfig: m.TLSConfig(), + } + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } func customHTTPServer() { e := echo.New() e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - e.GET("/", func(c echo.Context) error { + e.Use(middleware.RequestLogger()) + e.GET("/", func(c *echo.Context) error { return c.HTML(http.StatusOK, `

Welcome to Echo!

TLS certificates automatically installed from Let's Encrypt :)

@@ -54,7 +73,7 @@ func customHTTPServer() { }, //ReadTimeout: 30 * time.Second, // use custom timeouts } - if err := s.ListenAndServeTLS("", ""); err != http.ErrServerClosed { - e.Logger.Fatal(err) + if err := s.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { + e.Logger.Error("failed to start server", "error", err) } } diff --git a/cookbook/cors/origin-func/server.go b/cookbook/cors/origin-func/server.go index 7098ebb1..c5759a05 100644 --- a/cookbook/cors/origin-func/server.go +++ b/cookbook/cors/origin-func/server.go @@ -1,43 +1,56 @@ package main import ( + "context" "net/http" - "regexp" + "strings" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) var ( users = []string{"Joe", "Veer", "Zion"} ) -func getUsers(c echo.Context) error { +func getUsers(c *echo.Context) error { return c.JSON(http.StatusOK, users) } -// allowOrigin takes the origin as an argument and returns true if the origin -// is allowed or false otherwise. -func allowOrigin(origin string) (bool, error) { - // In this example we use a regular expression but we can imagine various +// allowOrigin takes the origin as an argument and returns: +// - origin to add to the response Access-Control-Allow-Origin header +// - whether the request is allowed or not +// - an optional error. this will stop handler chain execution and return an error response. +// +// return origin, true, err // blocks request with error +// return origin, true, nil // allows CSRF request through +// return origin, false, nil // falls back to legacy token logic +func allowOrigin(c *echo.Context, origin string) (string, bool, error) { + // In this example we use a naive suffix check but we can imagine various // kind of custom logic. For example, an external datasource could be used // to maintain the list of allowed origins. - return regexp.MatchString(`^https:\/\/labstack\.(net|com)$`, origin) + if strings.HasSuffix(origin, ".example.com") { + return origin, true, nil + } + return "", false, nil } func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // CORS restricted with a custom function to allow origins // and with the GET, PUT, POST or DELETE methods allowed. e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOriginFunc: allowOrigin, - AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, + UnsafeAllowOriginFunc: allowOrigin, + AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, })) e.GET("/api/users", getUsers) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/cors/origin-list/server.go b/cookbook/cors/origin-list/server.go index 4f008d1f..fb4dcdaa 100644 --- a/cookbook/cors/origin-list/server.go +++ b/cookbook/cors/origin-list/server.go @@ -1,38 +1,38 @@ package main import ( + "context" "net/http" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) var ( users = []string{"Joe", "Veer", "Zion"} ) -func getUsers(c echo.Context) error { +func getUsers(c *echo.Context) error { return c.JSON(http.StatusOK, users) } func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // CORS default // Allows requests from any origin wth GET, HEAD, PUT, POST or DELETE method. - // e.Use(middleware.CORS()) + // e.Use(middleware.CORS("*")) // CORS restricted // Allows requests from any `https://labstack.com` or `https://labstack.net` origin - // wth GET, PUT, POST or DELETE method. - e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, - AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, - })) + e.Use(middleware.CORS("https://labstack.com", "https://labstack.net")) e.GET("/api/users", getUsers) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/crud/server.go b/cookbook/crud/server.go index ced0898d..0647427c 100644 --- a/cookbook/crud/server.go +++ b/cookbook/crud/server.go @@ -1,12 +1,13 @@ package main import ( + "context" "net/http" "strconv" "sync" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) type ( @@ -26,7 +27,7 @@ var ( // Handlers //---------- -func createUser(c echo.Context) error { +func createUser(c *echo.Context) error { lock.Lock() defer lock.Unlock() u := &user{ @@ -40,14 +41,14 @@ func createUser(c echo.Context) error { return c.JSON(http.StatusCreated, u) } -func getUser(c echo.Context) error { +func getUser(c *echo.Context) error { lock.Lock() defer lock.Unlock() id, _ := strconv.Atoi(c.Param("id")) return c.JSON(http.StatusOK, users[id]) } -func updateUser(c echo.Context) error { +func updateUser(c *echo.Context) error { lock.Lock() defer lock.Unlock() u := new(user) @@ -59,7 +60,7 @@ func updateUser(c echo.Context) error { return c.JSON(http.StatusOK, users[id]) } -func deleteUser(c echo.Context) error { +func deleteUser(c *echo.Context) error { lock.Lock() defer lock.Unlock() id, _ := strconv.Atoi(c.Param("id")) @@ -67,7 +68,7 @@ func deleteUser(c echo.Context) error { return c.NoContent(http.StatusNoContent) } -func getAllUsers(c echo.Context) error { +func getAllUsers(c *echo.Context) error { lock.Lock() defer lock.Unlock() return c.JSON(http.StatusOK, users) @@ -77,7 +78,7 @@ func main() { e := echo.New() // Middleware - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // Routes @@ -88,5 +89,8 @@ func main() { e.DELETE("/users/:id", deleteUser) // Start server - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/embed-resources/.gitignore b/cookbook/embed-resources/.gitignore deleted file mode 100644 index 9524d94f..00000000 --- a/cookbook/embed-resources/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -rice -app.rice-box.go diff --git a/cookbook/embed-resources/app/index.html b/cookbook/embed-resources/app/index.html deleted file mode 100644 index 66aac446..00000000 --- a/cookbook/embed-resources/app/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - go.rice Example - - - -

go.rice Example

- - diff --git a/cookbook/embed-resources/app/main.js b/cookbook/embed-resources/app/main.js deleted file mode 100644 index f888dc5c..00000000 --- a/cookbook/embed-resources/app/main.js +++ /dev/null @@ -1 +0,0 @@ -alert("main.js"); diff --git a/cookbook/embed-resources/server.go b/cookbook/embed-resources/server.go deleted file mode 100644 index 1fed5d07..00000000 --- a/cookbook/embed-resources/server.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/GeertJohan/go.rice" - "github.com/labstack/echo/v4" -) - -func main() { - e := echo.New() - // the file server for rice. "app" is the folder where the files come from. - assetHandler := http.FileServer(rice.MustFindBox("app").HTTPBox()) - // serves the index.html from rice - e.GET("/", echo.WrapHandler(assetHandler)) - - // servers other static files - e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) - - e.Logger.Fatal(e.Start(":1323")) -} diff --git a/cookbook/embed/server.go b/cookbook/embed/server.go index c49542ce..1a1a3e1d 100644 --- a/cookbook/embed/server.go +++ b/cookbook/embed/server.go @@ -1,13 +1,14 @@ package main import ( + "context" "embed" "io/fs" "log" "net/http" "os" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) //go:embed app @@ -34,5 +35,9 @@ func main() { assetHandler := http.FileServer(getFileSystem(useOS)) e.GET("/", echo.WrapHandler(assetHandler)) e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) - e.Logger.Fatal(e.Start(":1323")) + + 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/cookbook/file-download/attachment/server.go b/cookbook/file-download/attachment/server.go index 06863034..a880a5d2 100644 --- a/cookbook/file-download/attachment/server.go +++ b/cookbook/file-download/attachment/server.go @@ -1,22 +1,27 @@ package main import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.File("index.html") }) - e.GET("/attachment", func(c echo.Context) error { + e.GET("/attachment", func(c *echo.Context) error { return c.Attachment("attachment.txt", "attachment.txt") }) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/file-download/inline/server.go b/cookbook/file-download/inline/server.go index dcce34a0..866b03f9 100644 --- a/cookbook/file-download/inline/server.go +++ b/cookbook/file-download/inline/server.go @@ -1,22 +1,27 @@ package main import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.File("index.html") }) - e.GET("/inline", func(c echo.Context) error { + e.GET("/inline", func(c *echo.Context) error { return c.Inline("inline.txt", "inline.txt") }) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/file-download/server.go b/cookbook/file-download/server.go index be701ceb..3f72176e 100644 --- a/cookbook/file-download/server.go +++ b/cookbook/file-download/server.go @@ -1,22 +1,27 @@ package main import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.File("index.html") }) - e.GET("/file", func(c echo.Context) error { + e.GET("/file", func(c *echo.Context) error { return c.File("echo.svg") }) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/file-upload/multiple/server.go b/cookbook/file-upload/multiple/server.go index e2fb455e..1fc577ed 100644 --- a/cookbook/file-upload/multiple/server.go +++ b/cookbook/file-upload/multiple/server.go @@ -1,16 +1,17 @@ package main import ( + "context" "fmt" "io" "net/http" "os" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) -func upload(c echo.Context) error { +func upload(c *echo.Context) error { // Read form fields name := c.FormValue("name") email := c.FormValue("email") @@ -54,11 +55,14 @@ func upload(c echo.Context) error { func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) e.Static("/", "public") e.POST("/upload", upload) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/file-upload/single/server.go b/cookbook/file-upload/single/server.go index 562e4736..8759af14 100644 --- a/cookbook/file-upload/single/server.go +++ b/cookbook/file-upload/single/server.go @@ -1,16 +1,17 @@ package main import ( + "context" "fmt" "io" "net/http" "os" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) -func upload(c echo.Context) error { +func upload(c *echo.Context) error { // Read form fields name := c.FormValue("name") email := c.FormValue("email") @@ -48,11 +49,14 @@ func upload(c echo.Context) error { func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) e.Static("/", "public") e.POST("/upload", upload) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/google-app-engine/Dockerfile b/cookbook/google-app-engine/Dockerfile deleted file mode 100644 index 5d1c13e5..00000000 --- a/cookbook/google-app-engine/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -# Dockerfile extending the generic Go image with application files for a -# single application. -FROM gcr.io/google_appengine/golang - -COPY . /go/src/app -RUN go-wrapper download -RUN go-wrapper install -tags appenginevm \ No newline at end of file diff --git a/cookbook/google-app-engine/app-engine.go b/cookbook/google-app-engine/app-engine.go deleted file mode 100644 index f0214446..00000000 --- a/cookbook/google-app-engine/app-engine.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build appengine - -package main - -import ( - "net/http" - - "github.com/labstack/echo/v4" -) - -func createMux() *echo.Echo { - e := echo.New() - // note: we don't need to provide the middleware or static handlers, that's taken care of by the platform - // app engine has it's own "main" wrapper - we just need to hook echo into the default handler - http.Handle("/", e) - return e -} diff --git a/cookbook/google-app-engine/app-engine.yaml b/cookbook/google-app-engine/app-engine.yaml deleted file mode 100644 index e8f5bf05..00000000 --- a/cookbook/google-app-engine/app-engine.yaml +++ /dev/null @@ -1,36 +0,0 @@ -application: my-application-id # defined when you create your app using google dev console -module: default # see https://cloud.google.com/appengine/docs/go/ -version: alpha # you can run multiple versions of an app and A/B test -runtime: go # see https://cloud.google.com/appengine/docs/go/ -api_version: go1 # used when appengine supports different go versions - -default_expiration: "1d" # for CDN serving of static files (use url versioning if long!) - -handlers: -# all the static files that we normally serve ourselves are defined here and Google will handle -# serving them for us from it's own CDN / edge locations. For all the configuration options see: -# https://cloud.google.com/appengine/docs/go/config/appconfig#Go_app_yaml_Static_file_handlers -- url: / - mime_type: text/html - static_files: public/index.html - upload: public/index.html - -- url: /favicon.ico - mime_type: image/x-icon - static_files: public/favicon.ico - upload: public/favicon.ico - -- url: /scripts - mime_type: text/javascript - static_dir: public/scripts - -# static files normally don't touch the server that the app runs on but server-side template files -# needs to be readable by the app. The application_readable option makes sure they are available as -# part of the app deployment onto the instance. -- url: /templates - static_dir: /templates - application_readable: true - -# finally, we route all other requests to our application. The script name just means "the go app" -- url: /.* - script: _go_app \ No newline at end of file diff --git a/cookbook/google-app-engine/app-managed.go b/cookbook/google-app-engine/app-managed.go deleted file mode 100644 index 30635270..00000000 --- a/cookbook/google-app-engine/app-managed.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build appenginevm - -package main - -import ( - "net/http" - - "github.com/labstack/echo/v4" - "google.golang.org/appengine" -) - -func createMux() *echo.Echo { - e := echo.New() - // note: we don't need to provide the middleware or static handlers - // for the appengine vm version - that's taken care of by the platform - return e -} - -func main() { - // the appengine package provides a convenient method to handle the health-check requests - // and also run the app on the correct port. We just need to add Echo to the default handler - e := echo.New(":8080") - http.Handle("/", e) - appengine.Main() -} diff --git a/cookbook/google-app-engine/app-managed.yaml b/cookbook/google-app-engine/app-managed.yaml deleted file mode 100644 index d5da4cd9..00000000 --- a/cookbook/google-app-engine/app-managed.yaml +++ /dev/null @@ -1,37 +0,0 @@ -application: my-application-id # defined when you create your app using google dev console -module: default # see https://cloud.google.com/appengine/docs/go/ -version: alpha # you can run multiple versions of an app and A/B test -runtime: go # see https://cloud.google.com/appengine/docs/go/ -api_version: go1 # used when appengine supports different go versions -vm: true # for managed VMs only, remove for appengine classic - -default_expiration: "1d" # for CDN serving of static files (use url versioning if long!) - -handlers: -# all the static files that we normally serve ourselves are defined here and Google will handle -# serving them for us from it's own CDN / edge locations. For all the configuration options see: -# https://cloud.google.com/appengine/docs/go/config/appconfig#Go_app_yaml_Static_file_handlers -- url: / - mime_type: text/html - static_files: public/index.html - upload: public/index.html - -- url: /favicon.ico - mime_type: image/x-icon - static_files: public/favicon.ico - upload: public/favicon.ico - -- url: /scripts - mime_type: text/javascript - static_dir: public/scripts - -# static files normally don't touch the server that the app runs on but server-side template files -# needs to be readable by the app. The application_readable option makes sure they are available as -# part of the app deployment onto the instance. -- url: /templates - static_dir: /templates - application_readable: true - -# finally, we route all other requests to our application. The script name just means "the go app" -- url: /.* - script: _go_app \ No newline at end of file diff --git a/cookbook/google-app-engine/app-standalone.go b/cookbook/google-app-engine/app-standalone.go deleted file mode 100644 index 6b3da640..00000000 --- a/cookbook/google-app-engine/app-standalone.go +++ /dev/null @@ -1,24 +0,0 @@ -// +build !appengine,!appenginevm - -package main - -import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) - -func createMux() *echo.Echo { - e := echo.New() - - e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - e.Use(middleware.Gzip()) - - e.Static("/", "public") - - return e -} - -func main() { - e.Logger.Fatal(e.Start(":8080")) -} diff --git a/cookbook/google-app-engine/app.go b/cookbook/google-app-engine/app.go deleted file mode 100644 index 8d4d97a2..00000000 --- a/cookbook/google-app-engine/app.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -// reference our echo instance and create it early -var e = createMux() diff --git a/cookbook/google-app-engine/public/favicon.ico b/cookbook/google-app-engine/public/favicon.ico deleted file mode 100644 index d939ddca12aa14a2fa691e082b14436f18719f6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYI*xgi+L$0S%eIXJ|4d3e=$ zc=$9q*f})W*w{3AxVg1?dHFTCxVU71TG%nQ!9^s*C1e}r6xHtm@dI&j$+JSjqKD-a z)E)x)52d8#mN7Cgr~_3q!*xIzLZXr&7N@L&MvuI*?teKY?f(+eioe7p3-i zipeOb?_p!-lme;)8iNY#y}}th{6lyR9m5aFStkFNx6S&mWS;@V>Hn3T^ZqMa;8qR43_WoB7nfzbbr}e*9NZ)^>jyEcB5D~<+3`sc1>sGYKwp`v5j zgo^Gt|D*C+{(Hxj{P#>K{U4Ck^gq98>i?R)d1sp^&J}2!I2S|z%ALmmsy{F`jln-ARk5h5aYt9* zj16{yc^j0iVpbHkPBBa^s?yiCa9}le@x`haBp8{T%@C22>snl1`#mEv>3=|I+E;tu z_=n{M)i2vSC%z1cPj!k)%D|~VFfN0^CoYq}ptP - - - - - Echo - - - - - -

Echo!

- - - diff --git a/cookbook/google-app-engine/public/scripts/main.js b/cookbook/google-app-engine/public/scripts/main.js deleted file mode 100644 index 62a4c8f1..00000000 --- a/cookbook/google-app-engine/public/scripts/main.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Echo!"); diff --git a/cookbook/google-app-engine/templates/welcome.html b/cookbook/google-app-engine/templates/welcome.html deleted file mode 100644 index 5dc667c3..00000000 --- a/cookbook/google-app-engine/templates/welcome.html +++ /dev/null @@ -1 +0,0 @@ -{{define "welcome"}}Hello, {{.}}!{{end}} diff --git a/cookbook/google-app-engine/users.go b/cookbook/google-app-engine/users.go deleted file mode 100644 index 2684336a..00000000 --- a/cookbook/google-app-engine/users.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) - -type ( - user struct { - ID string `json:"id"` - Name string `json:"name"` - } -) - -var ( - users map[string]user -) - -func init() { - users = map[string]user{ - "1": user{ - ID: "1", - Name: "Wreck-It Ralph", - }, - } - - // hook into the echo instance to create an endpoint group - // and add specific middleware to it plus handlers - g := e.Group("/users") - g.Use(middleware.CORS()) - - g.POST("", createUser) - g.GET("", getUsers) - g.GET("/:id", getUser) -} - -func createUser(c echo.Context) error { - u := new(user) - if err := c.Bind(u); err != nil { - return err - } - users[u.ID] = *u - return c.JSON(http.StatusCreated, u) -} - -func getUsers(c echo.Context) error { - return c.JSON(http.StatusOK, users) -} - -func getUser(c echo.Context) error { - return c.JSON(http.StatusOK, users[c.Param("id")]) -} diff --git a/cookbook/google-app-engine/welcome.go b/cookbook/google-app-engine/welcome.go deleted file mode 100644 index 4378ee10..00000000 --- a/cookbook/google-app-engine/welcome.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "html/template" - "io" - "net/http" - - "github.com/labstack/echo/v4" -) - -type ( - Template struct { - templates *template.Template - } -) - -func init() { - t := &Template{ - templates: template.Must(template.ParseFiles("templates/welcome.html")), - } - e.Renderer = t - e.GET("/welcome", welcome) -} - -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -func welcome(c echo.Context) error { - return c.Render(http.StatusOK, "welcome", "Joe") -} diff --git a/cookbook/graceful-shutdown/server.go b/cookbook/graceful-shutdown/server.go index 69240d74..3a9ce167 100644 --- a/cookbook/graceful-shutdown/server.go +++ b/cookbook/graceful-shutdown/server.go @@ -2,38 +2,61 @@ package main import ( "context" + "errors" "net/http" "os" "os/signal" + "syscall" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/gommon/log" + "github.com/labstack/echo/v5" ) func main() { // Setup e := echo.New() - e.Logger.SetLevel(log.INFO) - e.GET("/", func(c echo.Context) error { + 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) + 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) + } +} + +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 := e.Start(":1323"); err != nil && err != http.ErrServerClosed { - e.Logger.Fatal("shutting down the server") + 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 := e.Shutdown(ctx); err != nil { - e.Logger.Fatal(err) + if err := s.Shutdown(ctx); err != nil { + e.Logger.Error("failed to stop server", "error", err) } } diff --git a/cookbook/hello-world/server.go b/cookbook/hello-world/server.go index a68c4dbe..3903bf1c 100644 --- a/cookbook/hello-world/server.go +++ b/cookbook/hello-world/server.go @@ -1,10 +1,11 @@ package main import ( + "context" "net/http" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { @@ -12,14 +13,17 @@ func main() { e := echo.New() // Middleware - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // Route => handler - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!\n") }) // Start server - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/http2-server-push/server.go b/cookbook/http2-server-push/server.go index db889bb7..581ca0c0 100644 --- a/cookbook/http2-server-push/server.go +++ b/cookbook/http2-server-push/server.go @@ -1,17 +1,21 @@ package main import ( + "context" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) func main() { e := echo.New() e.Static("/", "static") - e.GET("/", func(c echo.Context) (err error) { - pusher, ok := c.Response().Writer.(http.Pusher) - if ok { + 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 } @@ -24,5 +28,9 @@ func main() { } return c.File("index.html") }) - e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem")) + + 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/cookbook/http2/server.go b/cookbook/http2/server.go index a170d8a7..e8bde1b1 100644 --- a/cookbook/http2/server.go +++ b/cookbook/http2/server.go @@ -1,15 +1,16 @@ package main import ( + "context" "fmt" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) func main() { e := echo.New() - e.GET("/request", func(c echo.Context) error { + e.GET("/request", func(c *echo.Context) error { req := c.Request() format := ` @@ -22,5 +23,8 @@ func main() { ` return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) }) - e.Logger.Fatal(e.StartTLS(":1323", "cert.pem", "key.pem")) + 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/cookbook/jsonp/server.go b/cookbook/jsonp/server.go index 0ca29499..9a4caacc 100644 --- a/cookbook/jsonp/server.go +++ b/cookbook/jsonp/server.go @@ -1,23 +1,24 @@ package main import ( + "context" "math/rand" "net/http" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) e.Static("/", "public") // JSONP - e.GET("/jsonp", func(c echo.Context) error { + e.GET("/jsonp", func(c *echo.Context) error { callback := c.QueryParam("callback") var content struct { Response string `json:"response"` @@ -31,5 +32,8 @@ func main() { }) // Start server - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/jwt/custom-claims/server.go b/cookbook/jwt/custom-claims/server.go index c0b8adeb..3238cffa 100644 --- a/cookbook/jwt/custom-claims/server.go +++ b/cookbook/jwt/custom-claims/server.go @@ -1,12 +1,14 @@ package main import ( - "github.com/golang-jwt/jwt/v5" - echojwt "github.com/labstack/echo-jwt/v4" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "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. @@ -17,7 +19,7 @@ type jwtCustomClaims struct { jwt.RegisteredClaims } -func login(c echo.Context) error { +func login(c *echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") @@ -44,17 +46,20 @@ func login(c echo.Context) error { return err } - return c.JSON(http.StatusOK, echo.Map{ + return c.JSON(http.StatusOK, map[string]string{ "token": t, }) } -func accessible(c echo.Context) error { +func accessible(c *echo.Context) error { return c.String(http.StatusOK, "Accessible") } -func restricted(c echo.Context) error { - user := c.Get("user").(*jwt.Token) +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.(*jwtCustomClaims) name := claims.Name return c.String(http.StatusOK, "Welcome "+name+"!") @@ -64,7 +69,7 @@ func main() { e := echo.New() // Middleware - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // Login route @@ -78,7 +83,7 @@ func main() { // Configure middleware with the custom claims type config := echojwt.Config{ - NewClaimsFunc: func(c echo.Context) jwt.Claims { + NewClaimsFunc: func(c *echo.Context) jwt.Claims { return new(jwtCustomClaims) }, SigningKey: []byte("secret"), @@ -86,5 +91,8 @@ func main() { r.Use(echojwt.WithConfig(config)) r.GET("", restricted) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/jwt/user-defined-keyfunc/server.go b/cookbook/jwt/user-defined-keyfunc/server.go index 48751dff..61b1c074 100644 --- a/cookbook/jwt/user-defined-keyfunc/server.go +++ b/cookbook/jwt/user-defined-keyfunc/server.go @@ -4,12 +4,13 @@ import ( "context" "errors" "fmt" - echojwt "github.com/labstack/echo-jwt/v4" "net/http" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "github.com/lestrrat-go/jwx/v3/jwk" ) @@ -40,12 +41,15 @@ func getKey(token *jwt.Token) (interface{}, error) { return key.PublicKey() } -func accessible(c echo.Context) error { +func accessible(c *echo.Context) error { return c.String(http.StatusOK, "Accessible") } -func restricted(c echo.Context) error { - user := c.Get("user").(*jwt.Token) +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+"!") @@ -55,7 +59,7 @@ func main() { e := echo.New() // Middleware - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // Unauthenticated route @@ -71,5 +75,8 @@ func main() { r.GET("", restricted) } - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/load-balancing/upstream/server.go b/cookbook/load-balancing/upstream/server.go index fed38708..aa7d9496 100644 --- a/cookbook/load-balancing/upstream/server.go +++ b/cookbook/load-balancing/upstream/server.go @@ -1,12 +1,13 @@ package main import ( + "context" "fmt" "net/http" "os" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) var index = ` @@ -34,11 +35,17 @@ var index = ` func main() { name := os.Args[1] port := os.Args[2] + e := echo.New() e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - e.GET("/", func(c echo.Context) error { + e.Use(middleware.RequestLogger()) + + e.GET("/", func(c *echo.Context) error { return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) }) - e.Logger.Fatal(e.Start(port)) + + sc := echo.StartConfig{Address: port} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } diff --git a/cookbook/middleware/server.go b/cookbook/middleware/server.go index f68c0471..9241dfb7 100644 --- a/cookbook/middleware/server.go +++ b/cookbook/middleware/server.go @@ -1,19 +1,20 @@ package main import ( + "context" + "errors" "net/http" - "strconv" "sync" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type ( Stats struct { Uptime time.Time `json:"uptime"` RequestCount uint64 `json:"requestCount"` - Statuses map[string]int `json:"statuses"` + Statuses map[int]uint64 `json:"statuses"` mutex sync.RWMutex } ) @@ -21,27 +22,40 @@ type ( func NewStats() *Stats { return &Stats{ Uptime: time.Now(), - Statuses: map[string]int{}, + Statuses: map[int]uint64{}, } } // Process is the middleware function. func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if err := next(c); err != nil { - c.Error(err) + 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++ - status := strconv.Itoa(c.Response().Status) s.Statuses[status]++ - return nil + + return err } } // Handle is the endpoint to get stats. -func (s *Stats) Handle(c echo.Context) error { +func (s *Stats) Handle(c *echo.Context) error { s.mutex.RLock() defer s.mutex.RUnlock() return c.JSON(http.StatusOK, s) @@ -49,8 +63,8 @@ func (s *Stats) Handle(c echo.Context) error { // 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/3.0") + return func(c *echo.Context) error { + c.Response().Header().Set(echo.HeaderServer, "Echo/5.0") return next(c) } } @@ -58,9 +72,6 @@ func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc { func main() { e := echo.New() - // Debug mode - e.Debug = true - //------------------- // Custom middleware //------------------- @@ -73,10 +84,13 @@ func main() { e.Use(ServerHeader) // Handler - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) // Start server - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/reverse-proxy/server.go b/cookbook/reverse-proxy/server.go index a8404406..f0024f92 100644 --- a/cookbook/reverse-proxy/server.go +++ b/cookbook/reverse-proxy/server.go @@ -1,35 +1,29 @@ package main import ( + "context" "net/url" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "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.Use(middleware.Logger()) // Setup proxy - url1, err := url.Parse("http://localhost:8081") - if err != nil { - e.Logger.Fatal(err) - } - url2, err := url.Parse("http://localhost:8082") - if err != nil { - e.Logger.Fatal(err) - } + url1, _ := url.Parse("http://localhost:8081") + url2, _ := url.Parse("http://localhost:8082") targets := []*middleware.ProxyTarget{ - { - URL: url1, - }, - { - URL: url2, - }, + {URL: url1}, + {URL: url2}, } e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/reverse-proxy/upstream/server.go b/cookbook/reverse-proxy/upstream/server.go index 977f26fb..42035f81 100644 --- a/cookbook/reverse-proxy/upstream/server.go +++ b/cookbook/reverse-proxy/upstream/server.go @@ -1,13 +1,14 @@ package main import ( + "context" "fmt" "net/http" "os" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "golang.org/x/net/websocket" ) @@ -48,27 +49,37 @@ func main() { name := os.Args[1] port := os.Args[2] e := echo.New() + + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - e.GET("/", func(c echo.Context) error { + + 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 { + 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(err) + e.Logger.Error("failed to send message", "error", err) + } + select { + case <-ws.Request().Context().Done(): + return + case <-time.After(1 * time.Second): + continue } - time.Sleep(1 * time.Second) } }).ServeHTTP(c.Response(), c.Request()) return nil }) - e.Logger.Fatal(e.Start(port)) + sc := echo.StartConfig{Address: port} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } diff --git a/cookbook/sse/broadcast/server.go b/cookbook/sse/broadcast/server.go index 94d52d7d..e83da9df 100644 --- a/cookbook/sse/broadcast/server.go +++ b/cookbook/sse/broadcast/server.go @@ -2,12 +2,12 @@ package main import ( "errors" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/r3labs/sse/v2" - "log" "net/http" "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/r3labs/sse/v2" ) func main() { @@ -31,17 +31,17 @@ func main() { } }(server) - e.Use(middleware.Logger()) + 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 - log.Printf("The client is connected: %v\n", c.RealIP()) + 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 - log.Printf("The client is disconnected: %v\n", c.RealIP()) + e.Logger.Info("Client disconnected", "ip", c.RealIP()) return }() @@ -50,6 +50,6 @@ func main() { }) if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + e.Logger.Error("shutting down the server", "error", err) } } diff --git a/cookbook/sse/simple/server.go b/cookbook/sse/simple/server.go index efa5937b..f51cac38 100644 --- a/cookbook/sse/simple/server.go +++ b/cookbook/sse/simple/server.go @@ -2,21 +2,22 @@ package main import ( "errors" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "log" "net/http" "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { e := echo.New() - e.Use(middleware.Logger()) + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) e.File("/", "./index.html") - e.GET("/sse", func(c echo.Context) error { + e.GET("/sse", func(c *echo.Context) error { log.Printf("SSE client connected, ip: %v", c.RealIP()) w := c.Response() @@ -38,7 +39,9 @@ func main() { if err := event.MarshalTo(w); err != nil { return err } - w.Flush() + if err := http.NewResponseController(w).Flush(); err != nil { + return err + } } } }) diff --git a/cookbook/streaming-response/server.go b/cookbook/streaming-response/server.go index add65f2f..7687dde8 100644 --- a/cookbook/streaming-response/server.go +++ b/cookbook/streaming-response/server.go @@ -1,11 +1,12 @@ package main import ( + "context" "encoding/json" "net/http" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type ( @@ -28,7 +29,7 @@ var ( func main() { e := echo.New() - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) c.Response().WriteHeader(http.StatusOK) @@ -37,10 +38,21 @@ func main() { if err := enc.Encode(l); err != nil { return err } - c.Response().Flush() - time.Sleep(1 * time.Second) + 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 }) - e.Logger.Fatal(e.Start(":1323")) + + 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/cookbook/subdomain/server.go b/cookbook/subdomain/server.go index 3c9bb85a..2708e214 100644 --- a/cookbook/subdomain/server.go +++ b/cookbook/subdomain/server.go @@ -1,33 +1,28 @@ package main import ( + "context" "net/http" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) - -type ( - Host struct { - Echo *echo.Echo - } + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { // Hosts - hosts := map[string]*Host{} + vHosts := make(map[string]*echo.Echo) //----- // API //----- api := echo.New() - api.Use(middleware.Logger()) + api.Use(middleware.RequestLogger()) api.Use(middleware.Recover()) - hosts["api.localhost:1323"] = &Host{api} + vHosts["api.localhost:1323"] = api - api.GET("/", func(c echo.Context) error { + api.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "API") }) @@ -36,12 +31,12 @@ func main() { //------ blog := echo.New() - blog.Use(middleware.Logger()) + blog.Use(middleware.RequestLogger()) blog.Use(middleware.Recover()) - hosts["blog.localhost:1323"] = &Host{blog} + vHosts["blog.localhost:1323"] = blog - blog.GET("/", func(c echo.Context) error { + blog.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Blog") }) @@ -50,29 +45,19 @@ func main() { //--------- site := echo.New() - site.Use(middleware.Logger()) + site.Use(middleware.RequestLogger()) site.Use(middleware.Recover()) - hosts["localhost:1323"] = &Host{site} + vHosts["localhost:1323"] = site - site.GET("/", func(c echo.Context) error { + site.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Website") }) - // Server - e := echo.New() - e.Any("/*", func(c echo.Context) (err error) { - req := c.Request() - res := c.Response() - host := hosts[req.Host] + e := echo.NewVirtualHostHandler(vHosts) - if host == nil { - err = echo.ErrNotFound - } else { - host.Echo.ServeHTTP(res, req) - } - - return - }) - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/timeout/server.go b/cookbook/timeout/server.go index fd7c9476..83947879 100644 --- a/cookbook/timeout/server.go +++ b/cookbook/timeout/server.go @@ -1,11 +1,12 @@ package main import ( + "context" "net/http" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func main() { @@ -13,16 +14,21 @@ func main() { e := echo.New() // Middleware - e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ - Timeout: 5 * time.Second, - })) + e.Use(middleware.ContextTimeout(5 * time.Second)) // Route => handler - e.GET("/", func(c echo.Context) error { - time.Sleep(10 * time.Second) - return c.String(http.StatusOK, "Hello, World!\n") + 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 - e.Logger.Fatal(e.Start(":1323")) + 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/cookbook/twitter/handler/handler.go b/cookbook/twitter/handler/handler.go deleted file mode 100644 index dc730c8e..00000000 --- a/cookbook/twitter/handler/handler.go +++ /dev/null @@ -1,16 +0,0 @@ -package handler - -import ( - "gopkg.in/mgo.v2" -) - -type ( - Handler struct { - DB *mgo.Session - } -) - -const ( - // Key (Should come from somewhere else). - Key = "secret" -) diff --git a/cookbook/twitter/handler/post.go b/cookbook/twitter/handler/post.go deleted file mode 100644 index 57509e64..00000000 --- a/cookbook/twitter/handler/post.go +++ /dev/null @@ -1,73 +0,0 @@ -package handler - -import ( - "net/http" - "strconv" - - "github.com/labstack/echo/v4" - "github.com/labstack/echox/cookbook/twitter/model" - "gopkg.in/mgo.v2" - "gopkg.in/mgo.v2/bson" -) - -func (h *Handler) CreatePost(c echo.Context) (err error) { - u := &model.User{ - ID: bson.ObjectIdHex(userIDFromToken(c)), - } - p := &model.Post{ - ID: bson.NewObjectId(), - From: u.ID.Hex(), - } - if err = c.Bind(p); err != nil { - return - } - - // Validation - if p.To == "" || p.Message == "" { - return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid to or message fields"} - } - - // Find user from database - db := h.DB.Clone() - defer db.Close() - if err = db.DB("twitter").C("users").FindId(u.ID).One(u); err != nil { - if err == mgo.ErrNotFound { - return echo.ErrNotFound - } - return - } - - // Save post in database - if err = db.DB("twitter").C("posts").Insert(p); err != nil { - return - } - return c.JSON(http.StatusCreated, p) -} - -func (h *Handler) FetchPost(c echo.Context) (err error) { - userID := userIDFromToken(c) - page, _ := strconv.Atoi(c.QueryParam("page")) - limit, _ := strconv.Atoi(c.QueryParam("limit")) - - // Defaults - if page == 0 { - page = 1 - } - if limit == 0 { - limit = 100 - } - - // Retrieve posts from database - posts := []*model.Post{} - db := h.DB.Clone() - if err = db.DB("twitter").C("posts"). - Find(bson.M{"to": userID}). - Skip((page - 1) * limit). - Limit(limit). - All(&posts); err != nil { - return - } - defer db.Close() - - return c.JSON(http.StatusOK, posts) -} diff --git a/cookbook/twitter/handler/user.go b/cookbook/twitter/handler/user.go deleted file mode 100644 index 8e5f9b2c..00000000 --- a/cookbook/twitter/handler/user.go +++ /dev/null @@ -1,97 +0,0 @@ -package handler - -import ( - "github.com/golang-jwt/jwt/v5" - "net/http" - "time" - - "github.com/labstack/echo/v4" - "github.com/labstack/echox/cookbook/twitter/model" - "gopkg.in/mgo.v2" - "gopkg.in/mgo.v2/bson" -) - -func (h *Handler) Signup(c echo.Context) (err error) { - // Bind - u := &model.User{ID: bson.NewObjectId()} - if err = c.Bind(u); err != nil { - return - } - - // Validate - if u.Email == "" || u.Password == "" { - return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid email or password"} - } - - // Save user - db := h.DB.Clone() - defer db.Close() - if err = db.DB("twitter").C("users").Insert(u); err != nil { - return - } - - return c.JSON(http.StatusCreated, u) -} - -func (h *Handler) Login(c echo.Context) (err error) { - // Bind - u := new(model.User) - if err = c.Bind(u); err != nil { - return - } - - // Find user - db := h.DB.Clone() - defer db.Close() - if err = db.DB("twitter").C("users"). - Find(bson.M{"email": u.Email, "password": u.Password}).One(u); err != nil { - if err == mgo.ErrNotFound { - return &echo.HTTPError{Code: http.StatusUnauthorized, Message: "invalid email or password"} - } - return - } - - //----- - // JWT - //----- - - // Create token - token := jwt.New(jwt.SigningMethodHS256) - - // Set claims - claims := token.Claims.(jwt.MapClaims) - claims["id"] = u.ID - claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - - // Generate encoded token and send it as response - u.Token, err = token.SignedString([]byte(Key)) - if err != nil { - return err - } - - u.Password = "" // Don't send password - return c.JSON(http.StatusOK, u) -} - -func (h *Handler) Follow(c echo.Context) (err error) { - userID := userIDFromToken(c) - id := c.Param("id") - - // Add a follower to user - db := h.DB.Clone() - defer db.Close() - if err = db.DB("twitter").C("users"). - UpdateId(bson.ObjectIdHex(id), bson.M{"$addToSet": bson.M{"followers": userID}}); err != nil { - if err == mgo.ErrNotFound { - return echo.ErrNotFound - } - } - - return -} - -func userIDFromToken(c echo.Context) string { - user := c.Get("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - return claims["id"].(string) -} diff --git a/cookbook/twitter/model/post.go b/cookbook/twitter/model/post.go deleted file mode 100644 index 5b5333d6..00000000 --- a/cookbook/twitter/model/post.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -import ( - "gopkg.in/mgo.v2/bson" -) - -type ( - Post struct { - ID bson.ObjectId `json:"id" bson:"_id,omitempty"` - To string `json:"to" bson:"to"` - From string `json:"from" bson:"from"` - Message string `json:"message" bson:"message"` - } -) diff --git a/cookbook/twitter/model/user.go b/cookbook/twitter/model/user.go deleted file mode 100644 index 62a4b11a..00000000 --- a/cookbook/twitter/model/user.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -import ( - "gopkg.in/mgo.v2/bson" -) - -type ( - User struct { - ID bson.ObjectId `json:"id" bson:"_id,omitempty"` - Email string `json:"email" bson:"email"` - Password string `json:"password,omitempty" bson:"password"` - Token string `json:"token,omitempty" bson:"-"` - Followers []string `json:"followers,omitempty" bson:"followers,omitempty"` - } -) diff --git a/cookbook/twitter/server.go b/cookbook/twitter/server.go deleted file mode 100644 index e5fad7c5..00000000 --- a/cookbook/twitter/server.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - echojwt "github.com/labstack/echo-jwt/v4" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/echox/cookbook/twitter/handler" - "github.com/labstack/gommon/log" - "gopkg.in/mgo.v2" -) - -func main() { - e := echo.New() - e.Logger.SetLevel(log.ERROR) - e.Use(middleware.Logger()) - e.Use(echojwt.WithConfig(echojwt.Config{ - SigningKey: []byte(handler.Key), - Skipper: func(c echo.Context) bool { - // Skip authentication for signup and login requests - if c.Path() == "/login" || c.Path() == "/signup" { - return true - } - return false - }, - })) - - // Database connection - db, err := mgo.Dial("localhost") - if err != nil { - e.Logger.Fatal(err) - } - - // Create indices - if err = db.Copy().DB("twitter").C("users").EnsureIndex(mgo.Index{ - Key: []string{"email"}, - Unique: true, - }); err != nil { - log.Fatal(err) - } - - // Initialize handler - h := &handler.Handler{DB: db} - - // Routes - e.POST("/signup", h.Signup) - e.POST("/login", h.Login) - e.POST("/follow/:id", h.Follow) - e.POST("/posts", h.CreatePost) - e.GET("/feed", h.FetchPost) - - // Start server - e.Logger.Fatal(e.Start(":1323")) -} diff --git a/cookbook/websocket/gorilla/server.go b/cookbook/websocket/gorilla/server.go index cb71401b..845d3e26 100644 --- a/cookbook/websocket/gorilla/server.go +++ b/cookbook/websocket/gorilla/server.go @@ -1,18 +1,19 @@ package main import ( + "context" "fmt" "github.com/gorilla/websocket" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) var ( upgrader = websocket.Upgrader{} ) -func hello(c echo.Context) error { +func hello(c *echo.Context) error { ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) if err != nil { return err @@ -23,13 +24,13 @@ func hello(c echo.Context) error { // Write err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!")) if err != nil { - c.Logger().Error(err) + c.Logger().Error("failed to write WS message", "error", err) } // Read _, msg, err := ws.ReadMessage() if err != nil { - c.Logger().Error(err) + c.Logger().Error("failed to read WS message", "error", err) } fmt.Printf("%s\n", msg) } @@ -37,9 +38,16 @@ func hello(c echo.Context) error { func main() { e := echo.New() - e.Use(middleware.Logger()) + + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) + e.Static("/", "../public") + e.GET("/ws", hello) - e.Logger.Fatal(e.Start(":1323")) + + 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/cookbook/websocket/net/server.go b/cookbook/websocket/net/server.go index 9d478a53..02141114 100644 --- a/cookbook/websocket/net/server.go +++ b/cookbook/websocket/net/server.go @@ -1,28 +1,27 @@ package main import ( + "context" "fmt" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "golang.org/x/net/websocket" ) -func hello(c echo.Context) error { +func hello(c *echo.Context) error { websocket.Handler(func(ws *websocket.Conn) { defer ws.Close() for { // Write - err := websocket.Message.Send(ws, "Hello, Client!") - if err != nil { - c.Logger().Error(err) + if err := websocket.Message.Send(ws, "Hello, Client!"); err != nil { + c.Logger().Error("failed to write WS message", "error", err) } // Read msg := "" - err = websocket.Message.Receive(ws, &msg) - if err != nil { - c.Logger().Error(err) + if err := websocket.Message.Receive(ws, &msg); err != nil { + c.Logger().Error("failed to write WS message", "error", err) } fmt.Printf("%s\n", msg) } @@ -32,9 +31,15 @@ func hello(c echo.Context) error { func main() { e := echo.New() - e.Use(middleware.Logger()) + + e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) + e.Static("/", "../public") e.GET("/ws", hello) - e.Logger.Fatal(e.Start(":1323")) + + 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/go.mod b/go.mod index 821931b6..5335b35c 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,28 @@ module github.com/labstack/echox -go 1.24.0 - -toolchain go1.24.1 +go 1.25.0 require ( - github.com/GeertJohan/go.rice v1.0.3 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/gorilla/websocket v1.5.3 - github.com/labstack/echo-jwt/v4 v4.3.1 - github.com/labstack/echo/v4 v4.13.4 - github.com/labstack/gommon v0.4.2 + github.com/labstack/echo-jwt/v5 v5.0.0-20260101195926-7cdd901b4337 + github.com/labstack/echo/v5 v5.0.0-20260106091252-d6cb58b5c24e github.com/lestrrat-go/jwx/v3 v3.0.12 github.com/r3labs/sse/v2 v2.10.0 - golang.org/x/crypto v0.43.0 - golang.org/x/net v0.46.0 - google.golang.org/appengine v1.6.8 - gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 + golang.org/x/crypto v0.46.0 + golang.org/x/net v0.48.0 ) require ( - github.com/daaku/go.zipexe v1.0.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/segmentio/asm v1.2.1 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 50e3ed7d..cb5b74ed 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,3 @@ -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI= -github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk= -github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -14,29 +7,12 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo-jwt/v4 v4.3.1 h1:d8+/qf8nx7RxeL46LtoIwHJsH2PNN8xXCQ/jDianycE= -github.com/labstack/echo-jwt/v4 v4.3.1/go.mod h1:yJi83kN8S/5vePVPd+7ID75P4PqPNVRs2HVeuvYJH00= -github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= -github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/labstack/echo-jwt/v5 v5.0.0-20260101195926-7cdd901b4337 h1:+9keiGOPRLJvrh+hp+4hApQK/seq3iVr0hVdRweUUYI= +github.com/labstack/echo-jwt/v5 v5.0.0-20260101195926-7cdd901b4337/go.mod h1:7zV1rqeuZ57XS4nwL/ymw4tywVUzg5SEovZandLv9nI= +github.com/labstack/echo/v5 v5.0.0-20260106091252-d6cb58b5c24e h1:QFu8S1iZijCXEUcP8u/h+JtZYlekbt0VIndrKbT/VAo= +github.com/labstack/echo/v5 v5.0.0-20260106091252-d6cb58b5c24e/go.mod h1:5La3y+CVfH4IzVRlQA2LmK+clJEniclmzbqSyZIhsP4= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= @@ -45,94 +21,42 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7 github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI= -github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk= +github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs= +github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/website/docs/cookbook/embed-resources.md b/website/docs/cookbook/embed-resources.md index 81edf6c4..3af43873 100644 --- a/website/docs/cookbook/embed-resources.md +++ b/website/docs/cookbook/embed-resources.md @@ -10,8 +10,3 @@ description: Embed resources recipe https://github.com/labstack/echox/blob/master/cookbook/embed/server.go ``` -## With go.rice - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/embed-resources/server.go -``` diff --git a/website/docs/cookbook/google-app-engine.md b/website/docs/cookbook/google-app-engine.md deleted file mode 100644 index 81d810d0..00000000 --- a/website/docs/cookbook/google-app-engine.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -description: Google App Engine recipe ---- - -# Google App Engine - -Google App Engine (GAE) provides a range of hosting options from pure PaaS (App Engine Classic) -through Managed VMs to fully self-managed or container-driven Compute Engine instances. Echo -works great with all of these but requires a few changes to the usual examples to run on the -AppEngine Classic and Managed VM options. With a small amount of effort though it's possible -to produce a codebase that will run on these and also non-managed platforms automatically. - -We'll walk through the changes needed to support each option. - -## Standalone - -Wait? What? I thought this was about AppEngine! Bear with me - the easiest way to show the changes -required is to start with a setup for standalone and work from there plus there's no reason we -wouldn't want to retain the ability to run our app anywhere, right? - -We take advantage of the go [build constraints or tags](http://golang.org/pkg/go/build/) to change -how we create and run the Echo server for each platform while keeping the rest of the application -(e.g. handler wireup) the same across all of them. - -First, we have the normal setup based on the examples but we split it into two files - `app.go` will -be common to all variations and holds the Echo instance variable. We initialise it from a function -and because it is a `var` this will happen _before_ any `init()` functions run - a feature that we'll -use to connect our handlers later. - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/google-app-engine/app.go -``` - -A separate source file contains the function to create the Echo instance and add the static -file handlers and middleware. Note the build tag on the first line which says to use this when _not_ -building with appengine or appenginevm tags (which those platforms automatically add for us). We also -have the `main()` function to start serving our app as normal. This should all be very familiar. - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/google-app-engine/app-standalone.go -``` - -The handler-wireup that would normally also be a part of this Echo setup moves to separate files which -take advantage of the ability to have multiple `init()` functions which run _after_ the `e` Echo var is -initialized but _before_ the `main()` function is executed. These allow additional handlers to attach -themselves to the instance - I've found the `Group` feature naturally fits into this pattern with a file -per REST endpoint, often with a higher-level `api` group created that they attach to instead of the root -Echo instance directly (so things like CORS middleware can be added at this higher common-level). - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/google-app-engine/users.go -``` - -If we run our app it should execute as it did before when everything was in one file although we have -at least gained the ability to organize our handlers a little more cleanly. - -## AppEngine Classic and Managed VM(s) - -So far we've seen how to split apart the Echo creation and setup but still have the same app that -still only runs standalone. Now we'll see how those changes allow us to add support for AppEngine -hosting. - -Refer to the [AppEngine site](https://cloud.google.com/appengine/docs/go/) for full configuration -and deployment information. - -### Configuration file - -Both of these are Platform as as Service options running on either sandboxed micro-containers -or managed Compute Engine instances. Both require an `app.yaml` file to describe the app to -the service. While the app _could_ still serve all it's static files itself, one of the benefits -of the platform is having Google's infrastructure handle that for us so it can be offloaded and -the app only has to deal with dynamic requests. The platform also handles logging and http gzip -compression so these can be removed from the codebase as well. - -The yaml file also contains other options to control instance size and auto-scaling so for true -deployment freedom you would likely have separate `app-classic.yaml` and `app-vm.yaml` files and -this can help when making the transition from AppEngine Classic to Managed VMs. - -```yaml reference -https://github.com/labstack/echox/blob/master/cookbook/google-app-engine/app-engine.yaml -``` - -### Router configuration - -We'll now use the [build constraints](http://golang.org/pkg/go/build/) again like we did when creating -our `app-standalone.go` instance but this time with the opposite tags to use this file _if_ the build has -the appengine or appenginevm tags (added automatically when deploying to these platforms). - -This allows us to replace the `createMux()` function to create our Echo server _without_ any of the -static file handling and logging + gzip middleware which is no longer required. Also worth nothing is -that GAE classic provides a wrapper to handle serving the app so instead of a `main()` function where -we run the server, we instead wire up the router to the default `http.Handler` instead. - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/google-app-engine/app-engine.go -``` - -Managed VMs are slightly different. They are expected to respond to requests on port 8080 as well -as special health-check requests used by the service to detect if an instance is still running in -order to provide automated failover and instance replacement. The `google.golang.org/appengine` -package provides this for us so we have a slightly different version for Managed VMs: - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/google-app-engine/app-managed.go -``` - -So now we have three different configurations. We can build and run our app as normal so it can -be executed locally, on a full Compute Engine instance or any other traditional hosting provider -(including EC2, Docker etc...). This build will ignore the code in appengine and appenginevm tagged -files and the `app.yaml` file is meaningless to anything other than the AppEngine platform. - -We can also run locally using the [Google AppEngine SDK for Go](https://cloud.google.com/appengine/downloads) -either emulating [AppEngine Classic](https://cloud.google.com/appengine/docs/go/tools/devserver): - - goapp serve - -Or [Managed VM(s)](https://cloud.google.com/appengine/docs/managed-vms/sdk#run-local): - - gcloud config set project [your project id] - gcloud preview app run . - -And of course we can deploy our app to both of these platforms for easy and inexpensive auto-scaling joy. - -Depending on what your app actually does it's possible you may need to make other changes to allow -switching between AppEngine provided service such as Datastore and alternative storage implementations -such as MongoDB. A combination of go interfaces and build constraints can make this fairly straightforward -but is outside the scope of this example. diff --git a/website/docs/cookbook/twitter.md b/website/docs/cookbook/twitter.md deleted file mode 100644 index b8cebca0..00000000 --- a/website/docs/cookbook/twitter.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -description: Twitter like API recipe ---- - -# Twitter Like API - -This recipe demonstrates how to create a Twitter like REST API using MongoDB (Database), -JWT (API security) and JSON (Data exchange). - -## Models - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/twitter/model/user.go -``` - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/twitter/model/post.go -``` - -## Handlers - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/twitter/handler/handler.go -``` - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/twitter/handler/user.go -``` - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/twitter/handler/post.go -``` - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/twitter/server.go -``` - -## API - -### Signup - -User signup - -- Retrieve user credentials from the body and validate against database. -- For invalid email or password, send `400 - Bad Request` response. -- For valid email and password, save user in database and send `201 - Created` response. - -#### Request - -```sh -curl \ - -X POST \ - http://localhost:1323/signup \ - -H "Content-Type: application/json" \ - -d '{"email":"jon@labstack.com","password":"shhh!"}' -``` - -#### Response - -`201 - Created` - -```js -{ - "id": "58465b4ea6fe886d3215c6df", - "email": "jon@labstack.com", - "password": "shhh!" -} -``` - -### Login - -User login - -- Retrieve user credentials from the body and validate against database. -- For invalid credentials, send `401 - Unauthorized` response. -- For valid credentials, send `200 - OK` response: - - Generate JWT for the user and send it as response. - - Each subsequent request must include JWT in the `Authorization` header. - -`POST` `/login` - -#### Request - -```sh -curl \ - -X POST \ - http://localhost:1323/login \ - -H "Content-Type: application/json" \ - -d '{"email":"jon@labstack.com","password":"shhh!"}' -``` - -#### Response - -`200 - OK` - -```js -{ - "id": "58465b4ea6fe886d3215c6df", - "email": "jon@labstack.com", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0" -} -``` - -:::tip - -Client should store the token, for browsers, you may use local storage. - -::: - -### Follow - -Follow a user - -- For invalid token, send `400 - Bad Request` response. -- For valid token: - - If user is not found, send `404 - Not Found` response. - - Add a follower to the specified user in the path parameter and send `200 - OK` response. - -`POST` `/follow/:id` - -#### Request - -```sh -curl \ - -X POST \ - http://localhost:1323/follow/58465b4ea6fe886d3215c6df \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0" -``` - -#### Response - -`200 - OK` - -### Post - -Post a message to specified user - -- For invalid request payload, send `400 - Bad Request` response. -- If user is not found, send `404 - Not Found` response. -- Otherwise save post in the database and return it via `201 - Created` response. - -`POST` `/posts` - -#### Request - -```sh -curl \ - -X POST \ - http://localhost:1323/posts \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0" \ - -H "Content-Type: application/json" \ - -d '{"to":"58465b4ea6fe886d3215c6df","message":"hello"}' -``` - -#### Response - -`201 - Created` - -```js -{ - "id": "584661b9a6fe8871a3804cba", - "to": "58465b4ea6fe886d3215c6df", - "from": "58465b4ea6fe886d3215c6df", - "message": "hello" -} -``` - -### Feed - -List most recent messages based on optional `page` and `limit` query parameters - -`GET` `/feed?page=1&limit=5` - -#### Request - -```sh -curl \ - -X GET \ - http://localhost:1323/feed \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0" -``` - -#### Response - -`200 - OK` - -```js -[ - { - "id": "584661b9a6fe8871a3804cba", - "to": "58465b4ea6fe886d3215c6df", - "from": "58465b4ea6fe886d3215c6df", - "message": "hello" - } -] -``` diff --git a/website/docs/guide/quick-start.md b/website/docs/guide/quick-start.md index 7f7a9715..2a1fc33f 100644 --- a/website/docs/guide/quick-start.md +++ b/website/docs/guide/quick-start.md @@ -42,7 +42,10 @@ func main() { e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) - e.Logger.Fatal(e.Start(":1323")) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } ``` @@ -215,7 +218,7 @@ e.Static("/static", "static") ```go // Root level middleware -e.Use(middleware.Logger()) +e.Use(middleware.RequestLogger()) e.Use(middleware.Recover()) // Group level middleware diff --git a/website/docs/guide/request.md b/website/docs/guide/request.md index fecd40bd..a6ce2429 100644 --- a/website/docs/guide/request.md +++ b/website/docs/guide/request.md @@ -126,7 +126,10 @@ func main() { } return c.JSON(http.StatusOK, u) }) - e.Logger.Fatal(e.Start(":1323")) + 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/website/docs/guide/routing.md b/website/docs/guide/routing.md index 0a468dc3..080b8e71 100644 --- a/website/docs/guide/routing.md +++ b/website/docs/guide/routing.md @@ -264,7 +264,7 @@ admin.POST("/users", createUser) ```go // API v1 group with logging and CORS apiV1 := e.Group("/api/v1") -apiV1.Use(middleware.Logger()) +apiV1.Use(middleware.RequestLogger()) apiV1.Use(middleware.CORS()) apiV1.GET("/users", getUsers) @@ -272,7 +272,7 @@ apiV1.POST("/users", createUser) // API v2 group with different middleware apiV2 := e.Group("/api/v2") -apiV2.Use(middleware.Logger()) +apiV2.Use(middleware.RequestLogger()) apiV2.Use(middleware.CORS()) apiV2.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20))) @@ -297,7 +297,7 @@ adminUsers.POST("/", createUser) api := e.Group("/api") // Add middleware later -api.Use(middleware.Logger()) +api.Use(middleware.RequestLogger()) api.Use(middleware.Recover()) // Add routes @@ -319,7 +319,7 @@ func setupRoutes(e *echo.Echo) { // API group with common middleware api := e.Group("/api") - api.Use(middleware.Logger()) + api.Use(middleware.RequestLogger()) api.Use(middleware.Recover()) api.Use(middleware.CORS()) diff --git a/website/docs/middleware/jaeger.md b/website/docs/middleware/jaeger.md index 48eda52f..14195d83 100644 --- a/website/docs/middleware/jaeger.md +++ b/website/docs/middleware/jaeger.md @@ -26,7 +26,10 @@ func main() { c := jaegertracing.New(e, nil) defer c.Close() - e.Logger.Fatal(e.Start(":1323")) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } ``` @@ -99,7 +102,10 @@ func main() { c := jaegertracing.New(e, urlSkipper) defer c.Close() - e.Logger.Fatal(e.Start(":1323")) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } ``` @@ -128,7 +134,10 @@ func main() { jaegertracing.TraceFunction(c, slowFunc, "Test String") return c.String(http.StatusOK, "Hello, World!") }) - e.Logger.Fatal(e.Start(":1323")) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } // A function to be wrapped. No need to change it's arguments due to tracing @@ -167,7 +176,10 @@ func main() { time.Sleep(100 * time.Millisecond) return c.String(http.StatusOK, "Hello, World!") }) - e.Logger.Fatal(e.Start(":1323")) + 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/website/docs/middleware/logger.md b/website/docs/middleware/logger.md index 9a805b47..92161747 100644 --- a/website/docs/middleware/logger.md +++ b/website/docs/middleware/logger.md @@ -16,7 +16,7 @@ Echo has 2 different logger middlewares: ## Usage ```go -e.Use(middleware.Logger()) +e.Use(middleware.RequestLogger()) ``` *Sample output* @@ -30,7 +30,7 @@ e.Use(middleware.Logger()) ### Usage ```go -e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ Format: "method=${method}, uri=${uri}, status=${status}\n", })) ``` @@ -315,7 +315,7 @@ func logValues(c echo.Context, v middleware.RequestLoggerValues) error { return nil } -e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogValuesFunc: logValues, })) ``` diff --git a/website/docs/middleware/request-id.md b/website/docs/middleware/request-id.md index f5407c63..fb896b9d 100644 --- a/website/docs/middleware/request-id.md +++ b/website/docs/middleware/request-id.md @@ -22,7 +22,10 @@ e.Use(middleware.RequestID()) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID)) }) - e.Logger.Fatal(e.Start(":1323")) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } ``` ## Custom Configuration From b7c2b09be3f687f8015c2ac4b26193f32f9df505 Mon Sep 17 00:00:00 2001 From: toim Date: Sun, 11 Jan 2026 20:37:00 +0200 Subject: [PATCH 2/4] guides reworked for v5 --- cookbook/jwt/user-defined-keyfunc/server.go | 2 +- website/docs/cookbook/http2-server-push.md | 2 +- website/docs/cookbook/http2.md | 2 +- website/docs/guide/binding.md | 61 +++--- website/docs/guide/context.md | 107 ---------- website/docs/guide/cookies.md | 6 +- website/docs/guide/customization.md | 112 ++--------- website/docs/guide/error-handling.md | 54 +++-- website/docs/guide/quick-start.md | 48 +++-- website/docs/guide/request.md | 113 ++++++----- website/docs/guide/response.md | 82 ++++---- website/docs/guide/routing.md | 212 ++++++++++---------- website/docs/guide/start-server.md | 83 +++++--- website/docs/guide/static-files.md | 21 ++ website/docs/guide/templates.md | 83 ++++---- website/docs/guide/testing.md | 141 ++++++++++++- website/docs/middleware/basic-auth.md | 2 +- website/docs/middleware/body-dump.md | 2 +- website/docs/middleware/gzip.md | 2 +- website/docs/middleware/jaeger.md | 6 +- website/docs/middleware/jwt.md | 12 +- website/docs/middleware/key-auth.md | 4 +- website/docs/middleware/logger.md | 20 +- website/docs/middleware/prometheus.md | 18 +- website/docs/middleware/recover.md | 2 +- website/docs/middleware/request-id.md | 2 +- website/docs/middleware/session.md | 4 +- website/docs/middleware/static.md | 2 +- 28 files changed, 616 insertions(+), 589 deletions(-) delete mode 100644 website/docs/guide/context.md diff --git a/cookbook/jwt/user-defined-keyfunc/server.go b/cookbook/jwt/user-defined-keyfunc/server.go index 61b1c074..cad8d3a6 100644 --- a/cookbook/jwt/user-defined-keyfunc/server.go +++ b/cookbook/jwt/user-defined-keyfunc/server.go @@ -14,7 +14,7 @@ import ( "github.com/lestrrat-go/jwx/v3/jwk" ) -func getKey(token *jwt.Token) (interface{}, error) { +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 diff --git a/website/docs/cookbook/http2-server-push.md b/website/docs/cookbook/http2-server-push.md index 5903db75..1ad65e98 100644 --- a/website/docs/cookbook/http2-server-push.md +++ b/website/docs/cookbook/http2-server-push.md @@ -23,7 +23,7 @@ e.Static("/", "static") ### 2) Create a handler to serve index.html and push it's dependencies ```go -e.GET("/", func(c echo.Context) (err error) { +e.GET("/", func(c *echo.Context) (err error) { pusher, ok := c.Response().Writer.(http.Pusher) if ok { if err = pusher.Push("/app.css", nil); err != nil { diff --git a/website/docs/cookbook/http2.md b/website/docs/cookbook/http2.md index e8bf57a4..cbb7efee 100644 --- a/website/docs/cookbook/http2.md +++ b/website/docs/cookbook/http2.md @@ -22,7 +22,7 @@ a certificate from [CA](https://en.wikipedia.org/wiki/Certificate_authority). ## 2) Create a handler which simply outputs the request information to the client ```go -e.GET("/request", func(c echo.Context) error { +e.GET("/request", func(c *echo.Context) error { req := c.Request() format := ` diff --git a/website/docs/guide/binding.md b/website/docs/guide/binding.md index 6707b6a9..dac817ac 100644 --- a/website/docs/guide/binding.md +++ b/website/docs/guide/binding.md @@ -17,7 +17,7 @@ Echo provides different ways to perform binding, each described in the sections ## Struct Tag Binding -With struct binding you define a Go struct with tags specifying the data source and corresponding key. In your request handler you simply call `Context#Bind(i interface{})` with a pointer to your struct. The tags tell the binder everything it needs to know to load data from the request. +With struct binding you define a Go struct with tags specifying the data source and corresponding key. In your request handler you simply call `Context#Bind(i any)` with a pointer to your struct. The tags tell the binder everything it needs to know to load data from the request. In this example a struct type `User` tells the binder to bind the query string parameter `id` to its string field `ID`: @@ -78,22 +78,22 @@ It is also possible to bind data directly from a specific source: Request body: ```go -err := (&DefaultBinder{}).BindBody(c, &payload) +err := echo.BindBody(c, &payload)) ``` Query parameters: ```go -err := (&DefaultBinder{}).BindQueryParams(c, &payload) +err := echo.BindQueryParams(c, &payload) ``` Path parameters: ```go -err := (&DefaultBinder{}).BindPathParams(c, &payload) +err := echo.BindPathValues(c, &payload) ``` Header parameters: ```go -err := (&DefaultBinder{}).BindHeaders(c, &payload) +err := echo.BindHeaders(c, &payload) ``` Note that headers is not one of the included sources with `Context#Bind`. The only way to bind header data is by calling `BindHeaders` directly. @@ -124,22 +124,22 @@ type User struct { And a handler at the POST `/users` route binds request data to the struct: ```go -e.POST("/users", func(c echo.Context) (err error) { - u := new(UserDTO) - if err := c.Bind(u); err != nil { - return c.String(http.StatusBadRequest, "bad request") - } - - // Load into separate struct for security - user := User{ - Name: u.Name, - Email: u.Email, - IsAdmin: false // avoids exposing field that should not be bound - } - - executeSomeBusinessLogic(user) - - return c.JSON(http.StatusOK, u) +e.POST("/users", func(c *echo.Context) (err error) { + u := new(UserDTO) + if err := c.Bind(u); err != nil { + return c.String(http.StatusBadRequest, "bad request") + } + + // Load into a separate struct for security + user := User{ + Name: u.Name, + Email: u.Email, + IsAdmin: false // avoids exposing field that should not be bound + } + + executeSomeBusinessLogic(user) + + return c.JSON(http.StatusOK, u) }) ``` @@ -172,7 +172,7 @@ Echo provides an interface to bind explicit data types from a specified source. The following methods provide a handful of methods for binding to Go data type. These binders offer a fluent syntax and can be chained to configure & execute binding, and handle errors. * `echo.QueryParamsBinder(c)` - binds query parameters (source URL) -* `echo.PathParamsBinder(c)` - binds path parameters (source URL) +* `echo.PathValuesBinder(c)` - binds path parameters (source URL) * `echo.FormFieldBinder(c)` - binds form fields (source URL + body). See also [Request.ParseForm](https://golang.org/pkg/net/http/#Request.ParseForm). ### Error Handling @@ -242,14 +242,13 @@ A custom binder can be registered using `Echo#Binder`. ```go type CustomBinder struct {} -func (cb *CustomBinder) Bind(i interface{}, c echo.Context) (err error) { - // You may use default binder - db := new(echo.DefaultBinder) - if err := db.Bind(i, c); err != echo.ErrUnsupportedMediaType { - return - } - - // Define your custom implementation here - return +func (cb *CustomBinder) Bind(c *echo.Context, i any) (error) { + // You may use default binder + db := new(echo.DefaultBinder) + if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType { + return err + } + // Define your custom implementation here + return nil } ``` diff --git a/website/docs/guide/context.md b/website/docs/guide/context.md deleted file mode 100644 index a0bfd0eb..00000000 --- a/website/docs/guide/context.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -description: Context in Echo -slug: /context -sidebar_position: 4 ---- - -# Context - -`echo.Context` represents the context of the current HTTP request. It holds request and -response reference, path, path parameters, data, registered handler and APIs to read -request and write response. As Context is an interface, it is easy to extend it with -custom APIs. - -## Extending - -**Define a custom context** - -```go -type CustomContext struct { - echo.Context -} - -func (c *CustomContext) Foo() { - println("foo") -} - -func (c *CustomContext) Bar() { - println("bar") -} -``` - -**Create a middleware to extend default context** - -```go -e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - cc := &CustomContext{c} - return next(cc) - } -}) -``` - -:::caution - -This middleware should be registered before any other middleware. - -::: - -:::caution - -Custom context cannot be defined in a middleware before the router ran (Pre) - -::: - -**Use in handler** - -```go -e.GET("/", func(c echo.Context) error { - cc := c.(*CustomContext) - cc.Foo() - cc.Bar() - return cc.String(200, "OK") -}) -``` - -## Concurrency - -:::caution - -`Context` must not be accessed out of the goroutine handling the request. There are two reasons: - -1. `Context` has functions that are dangerous to execute from multiple goroutines. Therefore, only one goroutine should access it. -2. Echo uses a pool to create `Context`'s. When the request handling finishes, Echo returns the `Context` to the pool. - -See issue [1908](https://github.com/labstack/echo/issues/1908) for a "cautionary tale" caused by this reason. Concurrency is complicated. Beware of this pitfall when working with goroutines. - -::: - -### Solution - -Use a channel - -```go -func(c echo.Context) error { - ca := make(chan string, 1) // To prevent this channel from blocking, size is set to 1. - r := c.Request() - method := r.Method - - go func() { - // This function must not touch the Context. - - fmt.Printf("Method: %s\n", method) - - // Do some long running operations... - - ca <- "Hey!" - }() - - select { - case result := <-ca: - return c.String(http.StatusOK, "Result: "+result) - case <-c.Request().Context().Done(): // Check context. - // If it reaches here, this means that context was canceled (a timeout was reached, etc.). - return nil - } -} -``` \ No newline at end of file diff --git a/website/docs/guide/cookies.md b/website/docs/guide/cookies.md index b44bec8b..ea7ea0af 100644 --- a/website/docs/guide/cookies.md +++ b/website/docs/guide/cookies.md @@ -31,7 +31,7 @@ Echo uses go standard `http.Cookie` object to add/retrieve cookies from the cont ## Create a Cookie ```go -func writeCookie(c echo.Context) error { +func writeCookie(c *echo.Context) error { cookie := new(http.Cookie) cookie.Name = "username" cookie.Value = "jon" @@ -48,7 +48,7 @@ func writeCookie(c echo.Context) error { ## Read a Cookie ```go -func readCookie(c echo.Context) error { +func readCookie(c *echo.Context) error { cookie, err := c.Cookie("username") if err != nil { return err @@ -65,7 +65,7 @@ func readCookie(c echo.Context) error { ## Read all the Cookies ```go -func readAllCookies(c echo.Context) error { +func readAllCookies(c *echo.Context) error { for _, cookie := range c.Cookies() { fmt.Println(cookie.Name) fmt.Println(cookie.Value) diff --git a/website/docs/guide/customization.md b/website/docs/guide/customization.md index ce79432a..221f339a 100644 --- a/website/docs/guide/customization.md +++ b/website/docs/guide/customization.md @@ -6,108 +6,20 @@ sidebar_position: 2 # Customization -## Debug - -`Echo#Debug` can be used to enable / disable debug mode. Debug mode sets the log level -to `DEBUG`. - ## Logging -The default format for logging is JSON, which can be changed by modifying the header. - -### Log Header - -`Echo#Logger.SetHeader(string)` can be used to set the header for -the logger. Default value: - -```js -{"time":"${time_rfc3339_nano}","level":"${level}","prefix":"${prefix}","file":"${short_file}","line":"${line}"} -``` - -*Example* -```go -import "github.com/labstack/gommon/log" - -/* ... */ +`Echo#Logger` - the default format for logging is JSON, which writes to `os.Stdout` by default. -if l, ok := e.Logger.(*log.Logger); ok { - l.SetHeader("${time_rfc3339} ${level}") -} -``` - -```sh -2018-05-08T20:30:06-07:00 INFO info -``` - -#### Available Tags - -- `time_rfc3339` -- `time_rfc3339_nano` -- `level` -- `prefix` -- `long_file` -- `short_file` -- `line` - -### Log Output - -`Echo#Logger.SetOutput(io.Writer)` can be used to set the output destination for -the logger. Default value is `os.Stdout` - -To completely disable logs use `Echo#Logger.SetOutput(io.Discard)` or `Echo#Logger.SetLevel(log.OFF)` - -### Log Level - -`Echo#Logger.SetLevel(log.Lvl)` can be used to set the log level for the logger. -Default value is `ERROR`. Possible values: - -- `DEBUG` -- `INFO` -- `WARN` -- `ERROR` -- `OFF` ### Custom Logger -Logging is implemented using `echo.Logger` interface which allows you to register +Logging is implemented using `slog.Logger` interface which allows you to register a custom logger using `Echo#Logger`. -## Startup Banner - -`Echo#HideBanner` can be used to hide the startup banner. - -## Listener Port - -`Echo#HidePort` can be used to hide the listener port message. - -## Custom Listener - -`Echo#*Listener` can be used to run a custom listener. - -*Example* - ```go -l, err := net.Listen("tcp", ":1323") -if err != nil { - e.Logger.Fatal(err) -} -e.Listener = l -e.Logger.Fatal(e.Start("")) +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) ``` -## Disable HTTP/2 - -`Echo#DisableHTTP2` can be used to disable HTTP/2 protocol. - -## Read Timeout - -`Echo#*Server#ReadTimeout` can be used to set the maximum duration before timing out read -of the request. - -## Write Timeout - -`Echo#*Server#WriteTimeout` can be used to set the maximum duration before timing out write -of the response. ## Validator @@ -116,26 +28,44 @@ on request payload. [Learn more](./request.md#validate-data) + ## Custom Binder `Echo#Binder` can be used to register a custom binder for binding request payload. [Learn more](./binding#custom-binding) + ## Custom JSON Serializer `Echo#JSONSerializer` can be used to register a custom JSON serializer. Have a look at `DefaultJSONSerializer` on [json.go](https://github.com/labstack/echo/blob/master/json.go). + ## Renderer `Echo#Renderer` can be used to register a renderer for template rendering. [Learn more](./templates.md) + ## HTTP Error Handler `Echo#HTTPErrorHandler` can be used to register a custom http error handler. + +## HTTP Error Handler + +`Echo#OnAddRoute` can be used to register a callback function that is invoked when a new route is added to the router. + [Learn more](./error-handling.md) + + +## IP Extractor for finding real IP address + +`Echo#IPExtractor` is used to retrieve IP address reliably/securely, you must let your application be aware of the entire +architecture of your infrastructure. In Echo, this can be done by configuring `Echo#IPExtractor` appropriately. + +[Learn more](./ip-address.md) + diff --git a/website/docs/guide/error-handling.md b/website/docs/guide/error-handling.md index 91e3eb02..6fa746cd 100644 --- a/website/docs/guide/error-handling.md +++ b/website/docs/guide/error-handling.md @@ -17,7 +17,7 @@ For example, when basic auth middleware finds invalid credentials it returns ```go e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { // Extract the credentials from HTTP request header and perform a security // check @@ -58,6 +58,26 @@ take action accordingly e.g. send notification email or log error to a centraliz system. You can also send customized response to the client e.g. error page or just a JSON response. +To check if the response has been already sent to the client ("commited") you can use `echo.UnwrapResponse()`, +```go +if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + if resp.Committed { + return // response has been already sent to the client by handler or some middleware + } +} +``` + +To find error in an error chain that implements `echo.HTTPStatusCoder`, +```go +code := http.StatusInternalServerError +var sc echo.HTTPStatusCoder +if errors.As(err, &sc) { // find error in an error chain that implements HTTPStatusCoder + if tmp := sc.StatusCode(); tmp != 0 { + code = tmp + } +} +``` + ### Error Pages The following custom HTTP error handler shows how to display error pages for different @@ -65,20 +85,30 @@ type of errors and logs the error. The name of the error page should be like `Hello, World!") } ``` @@ -43,7 +43,7 @@ code. You may find it handy using with a template engine which outputs `[]byte`. ## Send JSON -`Context#JSON(code int, i interface{})` can be used to encode a provided Go type into +`Context#JSON(code int, i any)` can be used to encode a provided Go type into JSON and send it as response with status code. *Example* @@ -56,7 +56,7 @@ type User struct { } // Handler -func(c echo.Context) error { +func(c *echo.Context) error { u := &User{ Name: "Jon", Email: "jon@labstack.com", @@ -73,7 +73,7 @@ in that case you can directly stream JSON. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { u := &User{ Name: "Jon", Email: "jon@labstack.com", @@ -86,13 +86,13 @@ func(c echo.Context) error { ### JSON Pretty -`Context#JSONPretty(code int, i interface{}, indent string)` can be used to a send +`Context#JSONPretty(code int, i any, indent string)` can be used to a send a JSON response which is pretty printed based on indent, which could be spaces or tabs. Example below sends a pretty print JSON indented with spaces: ```go -func(c echo.Context) error { +func(c *echo.Context) error { u := &User{ Name: "Jon", Email: "joe@labstack.com", @@ -107,18 +107,6 @@ func(c echo.Context) error { "name": "Jon" } ``` -:::tip - -You can also use `Context#JSON()` to output a pretty printed JSON (indented with spaces) -by appending `pretty` in the request URL query string. - -::: - -*Example* - -```sh -curl http://localhost:1323/users/1?pretty -``` ### JSON Blob @@ -128,7 +116,7 @@ from external source, for example, database. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { encodedJSON := []byte{} // Encoded JSON from external source return c.JSONBlob(http.StatusOK, encodedJSON) } @@ -136,7 +124,7 @@ func(c echo.Context) error { ## Send JSONP -`Context#JSONP(code int, callback string, i interface{})` can be used to encode a provided +`Context#JSONP(code int, callback string, i any)` can be used to encode a provided Go type into JSON and send it as JSONP payload constructed using a callback, with status code. @@ -144,13 +132,13 @@ status code. ## Send XML -`Context#XML(code int, i interface{})` can be used to encode a provided Go type into +`Context#XML(code int, i any)` can be used to encode a provided Go type into XML and send it as response with status code. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { u := &User{ Name: "Jon", Email: "jon@labstack.com", @@ -167,7 +155,7 @@ in that case you can directly stream XML. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { u := &User{ Name: "Jon", Email: "jon@labstack.com", @@ -180,13 +168,13 @@ func(c echo.Context) error { ### XML Pretty -`Context#XMLPretty(code int, i interface{}, indent string)` can be used to a send +`Context#XMLPretty(code int, i any, indent string)` can be used to a send an XML response which is pretty printed based on indent, which could be spaces or tabs. Example below sends a pretty print XML indented with spaces: ```go -func(c echo.Context) error { +func(c *echo.Context) error { u := &User{ Name: "Jon", Email: "joe@labstack.com", @@ -222,7 +210,7 @@ from external source, for example, database. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { encodedXML := []byte{} // Encoded XML from external source return c.XMLBlob(http.StatusOK, encodedXML) } @@ -236,7 +224,7 @@ It automatically sets the correct content type and handles caching gracefully. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { return c.File("") } ``` @@ -249,7 +237,7 @@ used to send file as `Content-Disposition: attachment` with provided name. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { return c.Attachment("", "") } ``` @@ -262,7 +250,7 @@ used to send file as `Content-Disposition: inline` with provided name. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { return c.Inline("") } ``` @@ -275,7 +263,7 @@ data response with provided content type and status code. *Example* ```go -func(c echo.Context) (err error) { +func(c *echo.Context) (err error) { data := []byte(`0306703,0035866,NO_ACTION,06/19/2006 0086003,"0005866",UPDATED,06/19/2006`) return c.Blob(http.StatusOK, "text/csv", data) @@ -291,7 +279,7 @@ code. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { f, err := os.Open("") if err != nil { return err @@ -308,7 +296,7 @@ func(c echo.Context) error { *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { return c.NoContent(http.StatusOK) } ``` @@ -321,7 +309,7 @@ a provided URL with status code. *Example* ```go -func(c echo.Context) error { +func(c *echo.Context) error { return c.Redirect(http.StatusMovedPermanently, "") } ``` @@ -330,26 +318,30 @@ func(c echo.Context) error { ### Before Response -`Context#Response#Before(func())` can be used to register a function which is called just before the response is written. +`Response#Before(func())` can be used to register a function which is called just before the response is written. ### After Response -`Context#Response#After(func())` can be used to register a function which is called just +`Response#After(func())` can be used to register a function which is called just after the response is written. If the "Content-Length" is unknown, none of the after function is executed. *Example* ```go -func(c echo.Context) error { - c.Response().Before(func() { - println("before response") - }) - c.Response().After(func() { - println("after response") - }) - return c.NoContent(http.StatusNoContent) -} +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 diff --git a/website/docs/guide/routing.md b/website/docs/guide/routing.md index 080b8e71..876a2d0f 100644 --- a/website/docs/guide/routing.md +++ b/website/docs/guide/routing.md @@ -16,7 +16,7 @@ handler which sends `Hello, World!` HTTP response. ```go // Handler -func hello(c echo.Context) error { +func hello(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!") } @@ -27,19 +27,23 @@ e.GET("/hello", hello) ## HTTP Methods Echo supports all standard HTTP methods. Each method follows the signature: -`e.METHOD(path string, h HandlerFunc, middleware ...Middleware) *Route` +`e.METHOD(path string, h HandlerFunc, middleware ...Middleware) echo.RouteInfo` ### Available HTTP Methods -| Method | Signature | Description | -|--------|-----------|-------------| -| `GET` | `e.GET(path, handler, ...middleware)` | Retrieve data | -| `POST` | `e.POST(path, handler, ...middleware)` | Create new resource | -| `PUT` | `e.PUT(path, handler, ...middleware)` | Update entire resource | -| `PATCH` | `e.PATCH(path, handler, ...middleware)` | Partial update of resource | -| `DELETE` | `e.DELETE(path, handler, ...middleware)` | Remove resource | -| `HEAD` | `e.HEAD(path, handler, ...middleware)` | Get headers only | -| `OPTIONS` | `e.OPTIONS(path, handler, ...middleware)` | Get allowed methods | +| Method | Signature | Description | +|--------|-----------|----------------------------------------------------------------| +| `GET` | `e.GET(path, handler, ...middleware)` | Retrieve data | +| `POST` | `e.POST(path, handler, ...middleware)` | Create new resource | +| `PUT` | `e.PUT(path, handler, ...middleware)` | Update entire resource | +| `PATCH` | `e.PATCH(path, handler, ...middleware)` | Partial update of resource | +| `DELETE` | `e.DELETE(path, handler, ...middleware)` | Remove resource | +| `HEAD` | `e.HEAD(path, handler, ...middleware)` | Get headers only | +| `OPTIONS` | `e.OPTIONS(path, handler, ...middleware)` | Get allowed methods | +| `CONNECT` | `e.CONNECT(path, handler, ...middleware)` | Connect method | +| `TRACE` | `e.TRACE(path, handler, ...middleware)` | Trace method | +| `RouteNotFound` | `e.TRACE(path, handler, ...middleware)` | In case router did not found matching route (404) | +| `Any` | `e.TRACE(path, handler, ...middleware)` | Matches any request method. Lower priority than other handlers | ### Method Parameters @@ -49,15 +53,16 @@ Echo supports all standard HTTP methods. Each method follows the signature: ### Handler Function Signature -Echo defines handler functions as `func(echo.Context) error` where: +Echo defines handler functions as `func(c *echo.Context) error` where: -- **`echo.Context`**: Provides access to: +- **`*echo.Context`**: Provides access to: - Request and response objects - Path parameters (`c.Param("id")`) - Query parameters (`c.QueryParam("name")`) - Form data (`c.FormValue("field")`) - JSON binding (`c.Bind(&struct{})`) - Response helpers (`c.JSON()`, `c.String()`, etc.) + - etc ### Example Usage @@ -94,7 +99,7 @@ If you want to register it for some methods use `Echo.Match(methods []string, pa Registers a handler for all HTTP methods on a given path: ```go -e.Any("/api/*", func(c echo.Context) error { +e.Any("/api/*", func(c *echo.Context) error { return c.String(http.StatusOK, "Handles all methods") }) ``` @@ -121,20 +126,30 @@ Path parameters are defined using `:paramName` syntax and can be accessed in han ```go // Single parameter -e.GET("/users/:id", func(c echo.Context) error { +e.GET("/users/:id", func(c *echo.Context) error { id := c.Param("id") return c.String(http.StatusOK, "User ID: " + id) }) +// genetic type function to convert string to int +e.GET("/users/:id", func(c *echo.Context) error { + ID, err := echo.PathParam[int](c, "id") + //ID, err := echo.PathParamOr[int](c, "id", -1) // default value -1 + if err != nil { + return err + } + return c.String(http.StatusOK, fmt.Sprintf("User ID: %d", ID)) +}) + // Multiple parameters -e.GET("/users/:id/posts/:postId", func(c echo.Context) error { +e.GET("/users/:id/posts/:postId", func(c *echo.Context) error { userId := c.Param("id") postId := c.Param("postId") return c.String(http.StatusOK, "User: " + userId + ", Post: " + postId) }) // Optional parameters (using query parameters instead) -e.GET("/users", func(c echo.Context) error { +e.GET("/users", func(c *echo.Context) error { id := c.QueryParam("id") // Optional return c.String(http.StatusOK, "User ID: " + id) }) @@ -145,10 +160,15 @@ e.GET("/users", func(c echo.Context) error { Query parameters are accessed using `c.QueryParam("name")` and are optional by nature: ```go -e.GET("/search", func(c echo.Context) error { - query := c.QueryParam("q") - limit := c.QueryParam("limit") - return c.String(http.StatusOK, "Search: " + query + ", Limit: " + limit) +e.GET("/search", func(c *echo.Context) error { + query := c.QueryParam("q") + // typed generic function to get value other type than string + //limit, err := echo.QueryParam[int](c, "limit") + limit, err := echo.QueryParamOr[int](c, "limit", 100) // default value -1 + if err != nil { + return err + } + return c.String(http.StatusOK, fmt.Sprintf("Search query: %s, Limit: %d", query, limit)) }) ``` @@ -162,10 +182,14 @@ e.GET("/search", func(c echo.Context) error { For POST/PUT requests with form data, use `c.FormValue("field")`: ```go -e.POST("/users", func(c echo.Context) error { - name := c.FormValue("name") - email := c.FormValue("email") - return c.String(http.StatusOK, "Name: " + name + ", Email: " + email) +e.POST("/users", func(c *echo.Context) error { + name := c.FormValue("name") + email := c.FormValue("email") + age, err := echo.FormValueOr[int](c, "age", -1) // default value -1 + if err != nil { + return err + } + return c.String(http.StatusOK, fmt.Sprintf("Name: %s, Email: %s, Age: %d", name, email, age)) }) ``` @@ -195,15 +219,15 @@ There can be only one effective match-any parameter in route. When route is adde ### Example ```go -e.GET("/users/:id", func(c echo.Context) error { +e.GET("/users/:id", func(c *echo.Context) error { return c.String(http.StatusOK, "/users/:id") }) -e.GET("/users/new", func(c echo.Context) error { +e.GET("/users/new", func(c *echo.Context) error { return c.String(http.StatusOK, "/users/new") }) -e.GET("/users/1/files/*", func(c echo.Context) error { +e.GET("/users/1/files/*", func(c *echo.Context) error { return c.String(http.StatusOK, "/users/1/files/*") }) ``` @@ -246,11 +270,12 @@ api.GET("/users/:id", getUser) ```go // Admin group with authentication admin := e.Group("/admin") -admin.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { - if username == "joe" && password == "secret" { - return true, nil - } - return false, nil +admin.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (bool, error) { + // Use constant-time comparison to prevent timing attacks + userMatch := subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 + passMatch := subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 + + return userMatch && passMatch, nil })) // All routes under /admin/* will require authentication @@ -265,7 +290,7 @@ admin.POST("/users", createUser) // API v1 group with logging and CORS apiV1 := e.Group("/api/v1") apiV1.Use(middleware.RequestLogger()) -apiV1.Use(middleware.CORS()) +apiV1.Use(middleware.CORS("https://api.example.com")) apiV1.GET("/users", getUsers) apiV1.POST("/users", createUser) @@ -273,7 +298,7 @@ apiV1.POST("/users", createUser) // API v2 group with different middleware apiV2 := e.Group("/api/v2") apiV2.Use(middleware.RequestLogger()) -apiV2.Use(middleware.CORS()) +apiV2.Use(middleware.CORS("https://api.example.com")) apiV2.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20))) apiV2.GET("/users", getUsersV2) @@ -321,7 +346,7 @@ func setupRoutes(e *echo.Echo) { api := e.Group("/api") api.Use(middleware.RequestLogger()) api.Use(middleware.Recover()) - api.Use(middleware.CORS()) + api.Use(middleware.CORS("https://api.example.com")) // Public API routes api.GET("/health", healthCheck) @@ -361,30 +386,15 @@ Each of the registration methods returns a `Route` object, which can be used to ### Basic Route Naming ```go -// Method 1: Assign name after registration -route := e.POST("/users", createUser) -route.Name = "create-user" - -// Method 2: Inline syntax -e.GET("/users/:id", getUser).Name = "get-user" -e.PUT("/users/:id", updateUser).Name = "update-user" -e.DELETE("/users/:id", deleteUser).Name = "delete-user" -``` - -### Advanced Route Naming - -```go -// Multiple parameters -e.GET("/users/:id/posts/:postId", getPost).Name = "get-user-post" - -// Nested routes -e.GET("/api/v1/users/:id", getUser).Name = "api-get-user" -e.GET("/api/v2/users/:id", getUserV2).Name = "api-get-user-v2" - -// Group routes with names -admin := e.Group("/admin") -admin.GET("/dashboard", dashboard).Name = "admin-dashboard" -admin.GET("/users", adminUsers).Name = "admin-users" +_, err := e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/users/:id", + Name: "user_details", + Handler: func(c *echo.Context) error { + return c.String(http.StatusOK, fmt.Sprintf("User ID: %s", c.Param("id"))) + }, + Middlewares: nil, +}) ``` ## URI Building @@ -393,15 +403,15 @@ Echo provides two methods for generating URIs: `Echo.URI()` and `Echo.Reverse()` ### Echo.URI() - Handler-based URI Generation -`Echo.URI(handler HandlerFunc, params ...interface{})` generates URIs based on handler functions: +`Echo.URI(handler HandlerFunc, params ...any)` generates URIs based on handler functions: ```go // Define handlers -func getUser(c echo.Context) error { +func getUser(c *echo.Context) error { return c.String(http.StatusOK, "User") } -func getUserPost(c echo.Context) error { +func getUserPost(c *echo.Context) error { return c.String(http.StatusOK, "User Post") } @@ -414,39 +424,23 @@ userURI := e.URI(getUser, 123) // "/users/123" postURI := e.URI(getUserPost, 123, 456) // "/users/123/posts/456" ``` -### Echo.Reverse() - Name-based URI Generation - -`Echo.Reverse(name string, params ...interface{})` generates URIs based on route names: +### Router.Reverse() - Name-based URI Generation -```go -// Register named routes -e.GET("/users/:id", getUser).Name = "get-user" -e.GET("/users/:id/posts/:postId", getUserPost).Name = "get-user-post" -e.GET("/api/v1/users/:id", getUser).Name = "api-user" - -// Generate URIs using names -userURI := e.Reverse("get-user", 123) // "/users/123" -postURI := e.Reverse("get-user-post", 123, 456) // "/users/123/posts/456" -apiURI := e.Reverse("api-user", 123) // "/api/v1/users/123" -``` - -### Practical Examples +`echo.Router.Reverse(name string, params ...any)` generates URIs based on route names: ```go -// In templates or handlers -func generateUserLinks(userID int) map[string]string { - return map[string]string{ - "profile": e.Reverse("get-user", userID), - "edit": e.Reverse("update-user", userID), - "delete": e.Reverse("delete-user", userID), - } +route := echo.Route{ + Method: http.MethodGet, + Path: "/users/:id", + Name: "user_details", + Handler: func(c *echo.Context) error { + return c.String(http.StatusOK, fmt.Sprintf("User ID: %s", c.Param("id"))) + }, + Middlewares: nil, } +_, err := e.AddRoute(route) -// In middleware for redirects -func redirectToUser(c echo.Context) error { - userID := c.Param("id") - return c.Redirect(http.StatusFound, e.Reverse("get-user", userID)) -} +userURI, err := e.Router().Routes().Reverse("user_details", 123) // "/users/123" ``` ### Benefits of Route Naming @@ -458,23 +452,23 @@ func redirectToUser(c echo.Context) error { ## List Routes -`Echo#Routes() []*Route` can be used to list all registered routes in the order +`Echo#Router#Routes() echo.Routes` can be used to list all registered routes in the order they are defined. Each route contains HTTP method, path and an associated handler. ### Example: Listing Routes ```go // Handlers -func createUser(c echo.Context) error { +func createUser(c *echo.Context) error { } -func findUser(c echo.Context) error { +func findUser(c *echo.Context) error { } -func updateUser(c echo.Context) error { +func updateUser(c *echo.Context) error { } -func deleteUser(c echo.Context) error { +func deleteUser(c *echo.Context) error { } // Routes @@ -487,7 +481,7 @@ e.DELETE("/users", deleteUser) Using the following code you can output all the routes to a JSON file: ```go -data, err := json.MarshalIndent(e.Routes(), "", " ") +data, err := json.MarshalIndent(e.Router().Routes(), "", " ") if err != nil { return err } @@ -499,24 +493,24 @@ os.WriteFile("routes.json", data, 0644) ```js [ { - "method": "POST", - "path": "/users", - "name": "main.createUser" + "Method": "POST", + "Path": "/users", + "Name": "main.createUser" }, { - "method": "GET", - "path": "/users", - "name": "main.findUser" + "Method": "GET", + "Path": "/users", + "Name": "main.findUser" }, { - "method": "PUT", - "path": "/users", - "name": "main.updateUser" + "Method": "PUT", + "Path": "/users", + "Name": "main.updateUser" }, { - "method": "DELETE", - "path": "/users", - "name": "main.deleteUser" + "Method": "DELETE", + "Path": "/users", + "Name": "main.deleteUser" } ] ``` diff --git a/website/docs/guide/start-server.md b/website/docs/guide/start-server.md index 3ed98cff..0832eac2 100644 --- a/website/docs/guide/start-server.md +++ b/website/docs/guide/start-server.md @@ -6,29 +6,44 @@ sidebar_position: 7 # Start Server -Echo provides following convenience methods to start the server: +Echo provides following `Echo.Start(address string)` convenience method to start the server. Which uses the default configuration for graceful shutdown. -- `Echo.Start(address string)` -- `Echo.StartTLS(address string, certFile, keyFile interface{})` -- `Echo.StartAutoTLS(address string)` -- `Echo.StartH2CServer(address string, h2s *http2.Server)` -- `Echo.StartServer(s *http.Server)` ## HTTP Server `Echo.Start` is convenience method that starts http server with Echo serving requests. ```go func main() { - e := echo.New() - // add middleware and routes - // ... - if err := e.Start(":8080"); err != http.ErrServerClosed { - log.Fatal(err) - } + e := echo.New() + // add middleware and routes + // ... + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } ``` -Following is equivalent to `Echo.Start` previous example +same functionality using server configuration `echo.StartConfig` +```go +func main() { + e := echo.New() + // add middleware and routes + // ... + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on signal + defer cancel() + + sc := echo.StartConfig{ + Address: ":1323", + GracefulTimeout: 5 * time.Second, // defaults to 10 seconds + } + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Following is server using `http.Server` ```go func main() { e := echo.New() @@ -40,7 +55,7 @@ func main() { //ReadTimeout: 30 * time.Second, // customize http.Server timeouts } if err := s.ListenAndServe(); err != http.ErrServerClosed { - log.Fatal(err) + e.Logger.Error("failed to start server", "error", err) } } ``` @@ -51,12 +66,14 @@ func main() { `server.crt` and `server.key` as TLS certificate pair. ```go func main() { - e := echo.New() - // add middleware and routes - // ... - if err := e.StartTLS(":8443", "server.crt", "server.key"); err != http.ErrServerClosed { - log.Fatal(err) - } + e := echo.New() + // add middleware and routes + // ... + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.StartTLS(context.Background(), e, "server.crt", "server.key"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } ``` @@ -89,17 +106,21 @@ See [Auto TLS Recipe](../cookbook/auto-tls.md#server) `Echo.StartH2CServer` is convenience method that starts a custom HTTP/2 cleartext server on given address ```go func main() { - e := echo.New() - // add middleware and routes - // ... - s := &http2.Server{ - MaxConcurrentStreams: 250, - MaxReadFrameSize: 1048576, - IdleTimeout: 10 * time.Second, - } - if err := e.StartH2CServer(":8080", s); err != http.ErrServerClosed { - log.Fatal(err) - } + e := echo.New() + // add middleware and routes + // ... + + h2s := &http2.Server{ + MaxConcurrentStreams: 250, + MaxReadFrameSize: 1048576, + IdleTimeout: 10 * time.Second, + } + h2Handler := h2c.NewHandler(e, h2s) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), h2Handler); err != nil { + e.Logger.Error("failed to start server", "error", err) + } } ``` diff --git a/website/docs/guide/static-files.md b/website/docs/guide/static-files.md index d020eec9..82780274 100644 --- a/website/docs/guide/static-files.md +++ b/website/docs/guide/static-files.md @@ -37,6 +37,27 @@ e.Static("/", "assets") Example above will serve any file from the assets directory for path `/*`. For example, a request to `/js/main.js` will fetch and serve `assets/js/main.js` file. +## Using Echo#StaticFS() + +Static files can be served from an `embed.FS` instance. Be sure to use `echo.MustSubFS` as embed.FS includes +subdirectories as their own entries and staticFS needs to server files from the correct root directory. + +```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 new route with path to serve a static diff --git a/website/docs/guide/templates.md b/website/docs/guide/templates.md index 4157b85e..859a14f1 100644 --- a/website/docs/guide/templates.md +++ b/website/docs/guide/templates.md @@ -8,22 +8,29 @@ sidebar_position: 12 ## Rendering -`Context#Render(code int, name string, data interface{}) error` renders a template +`Context#Render(code int, name string, data any) error` renders a template with data and sends a text/html response with status code. Templates can be registered by setting `Echo.Renderer`, allowing us to use any template engine. Example below shows how to use Go `html/template`: -1. Implement `echo.Renderer` interface +1. Use default template renderer +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` - ```go - type Template struct { - templates *template.Template - } +or Implement `echo.Renderer` interface - func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) - } - ``` +```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) +} + ``` 2. Pre-compile templates @@ -50,27 +57,27 @@ Example below shows how to use Go `html/template`: 4. Render a template inside your handler ```go - func Hello(c echo.Context) error { + func Hello(c *echo.Context) error { return c.Render(http.StatusOK, "hello", "World") } ``` ## Advanced - Calling Echo from templates -In certain situations it might be useful to generate URIs from the templates. In order to do so, you need to call `Echo#Reverse` from the templates itself. Golang's `html/template` package is not the best suited for this job, but this can be done in two ways: by providing a common method on all objects passed to templates or by passing `map[string]interface{}` and augmenting this object in the custom renderer. Given the flexibility of the latter approach, here is a sample program: +In certain situations it might be useful to generate URIs from the templates. In order to do so, you need to call `Echo#Reverse` from the templates itself. Golang's `html/template` package is not the best suited for this job, but this can be done in two ways: by providing a common method on all objects passed to templates or by passing `map[string]any` and augmenting this object in the custom renderer. Given the flexibility of the latter approach, here is a sample program: `template.html` ```html - -

Hello {{index . "name"}}

- -

{{ with $x := index . "reverse" }} - {{ call $x "foobar" }} <-- this will call the $x with parameter "foobar" - {{ end }} -

- + +

Hello {{index . "name"}}

+ +

{{ with $x := index . "reverse" }} + {{ call $x "foobar" }} + {{ end }} +

+ ``` @@ -84,7 +91,7 @@ import ( "io" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // TemplateRenderer is a custom html/template renderer for Echo framework @@ -93,30 +100,30 @@ type TemplateRenderer struct { } // Render renders a template document -func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { +func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error { // Add global methods if data is a map - if viewContext, isMap := data.(map[string]interface{}); isMap { - viewContext["reverse"] = c.Echo().Reverse + 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() - renderer := &TemplateRenderer{ - templates: template.Must(template.ParseGlob("*.html")), - } - e.Renderer = renderer - - // Named route "foobar" - e.GET("/something", func(c echo.Context) error { - return c.Render(http.StatusOK, "template.html", map[string]interface{}{ - "name": "Dolly!", - }) - }).Name = "foobar" - - e.Logger.Fatal(e.Start(":8000")) + 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/website/docs/guide/testing.md b/website/docs/guide/testing.md index cc07c501..8e864e2b 100644 --- a/website/docs/guide/testing.md +++ b/website/docs/guide/testing.md @@ -36,7 +36,7 @@ package handler import ( "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type ( @@ -49,7 +49,7 @@ type ( } ) -func (h *handler) createUser(c echo.Context) error { +func (h *handler) createUser(c *echo.Context) error { u := new(User) if err := c.Bind(u); err != nil { return err @@ -57,7 +57,7 @@ func (h *handler) createUser(c echo.Context) error { return c.JSON(http.StatusCreated, u) } -func (h *handler) getUser(c echo.Context) error { +func (h *handler) getUser(c *echo.Context) error { email := c.Param("email") user := h.db[email] if user == nil { @@ -78,7 +78,8 @@ import ( "strings" "testing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/echotest" "github.com/stretchr/testify/assert" ) @@ -94,9 +95,11 @@ func TestCreateUser(t *testing.T) { 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} + + h := &controller{mockDB} // Assertions if assert.NoError(t, h.createUser(c)) { @@ -105,16 +108,51 @@ func TestCreateUser(t *testing.T) { } } +// Same test as above but using `echotest` package helpers +func TestCreateUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(`{"name":"Jon Snow","email":"jon@labstack.com"}`), + }.ToContextRecorder(t) + + h := &controller{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} + +// Same test as above but even shorter +func TestCreateUserWithEchoTest2(t *testing.T) { + h := &controller{mockDB} + + rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(`{"name":"Jon Snow","email":"jon@labstack.com"}`), + }.ServeWithHandler(t, h.createUser) + + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) +} + func TestGetUser(t *testing.T) { // Setup e := echo.New() req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) + c.SetPath("/users/:email") - c.SetParamNames("email") - c.SetParamValues("jon@labstack.com") - h := &handler{mockDB} + c.SetPathValues(echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }) + h := &controller{mockDB} // Assertions if assert.NoError(t, h.getUser(c)) { @@ -122,6 +160,26 @@ func TestGetUser(t *testing.T) { assert.Equal(t, userJSON, rec.Body.String()) } } + +func TestGetUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }, + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ToContextRecorder(t) + + h := &controller{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 Form Payload @@ -135,11 +193,44 @@ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) ``` +Multipart form payload: +```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 Params ```go -c.SetParamNames("id", "email") -c.SetParamValues("1", "jon@labstack.com") +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) ``` ### Setting Query Params @@ -153,6 +244,34 @@ req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) ## Testing Middleware -*TBD* +```go +func TestCreateUserWithEchoTest2(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) + + err := middleware(handler)(c) + if err != nil { + t.Fatal(err) + } + // check that middleware set the value + userID, err := echo.ContextGet[int64](c, "user_id") + assert.NoError(t, err) + assert.Equal(t, int64(1234), userID) + + // check that handler returned the correct response + assert.Equal(t, http.StatusTeapot, rec.Code) +} +``` For now you can look into built-in middleware [test cases](https://github.com/labstack/echo/tree/master/middleware). diff --git a/website/docs/middleware/basic-auth.md b/website/docs/middleware/basic-auth.md index 6275fda6..8d1596e2 100644 --- a/website/docs/middleware/basic-auth.md +++ b/website/docs/middleware/basic-auth.md @@ -12,7 +12,7 @@ Basic auth middleware provides an HTTP basic authentication. ## Usage ```go -e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { +e.Use(middleware.BasicAuth(func(username, password string, c *echo.Context) (bool, error) { // Be careful to use constant time comparison to prevent timing attacks if subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 && subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 { diff --git a/website/docs/middleware/body-dump.md b/website/docs/middleware/body-dump.md index 72a4f1db..ad5755a0 100644 --- a/website/docs/middleware/body-dump.md +++ b/website/docs/middleware/body-dump.md @@ -10,7 +10,7 @@ Body dump middleware captures the request and response payload and calls the reg ```go e := echo.New() -e.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) { +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte) { })) ``` diff --git a/website/docs/middleware/gzip.md b/website/docs/middleware/gzip.md index 399c9278..28fe7a8e 100644 --- a/website/docs/middleware/gzip.md +++ b/website/docs/middleware/gzip.md @@ -32,7 +32,7 @@ A middleware skipper can be passed to avoid gzip to certain URL(s). ```go e := echo.New() e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return strings.Contains(c.Path(), "metrics") // Change "metrics" for your own path }, })) diff --git a/website/docs/middleware/jaeger.md b/website/docs/middleware/jaeger.md index 14195d83..5c249603 100644 --- a/website/docs/middleware/jaeger.md +++ b/website/docs/middleware/jaeger.md @@ -89,7 +89,7 @@ import ( ) // urlSkipper ignores metrics route on some middleware -func urlSkipper(c echo.Context) bool { +func urlSkipper(c *echo.Context) bool { if strings.HasPrefix(c.Path(), "/testurl") { return true } @@ -129,7 +129,7 @@ func main() { // Enable tracing middleware c := jaegertracing.New(e, nil) defer c.Close() - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { // Wrap slowFunc on a new span to trace it's execution passing the function arguments jaegertracing.TraceFunction(c, slowFunc, "Test String") return c.String(http.StatusOK, "Hello, World!") @@ -165,7 +165,7 @@ func main() { // Enable tracing middleware c := jaegertracing.New(e, nil) defer c.Close() - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { // Do something before creating the child span time.Sleep(40 * time.Millisecond) sp := jaegertracing.CreateChildSpan(c, "Child span for additional processing") diff --git a/website/docs/middleware/jwt.md b/website/docs/middleware/jwt.md index d1726d10..a372f00d 100644 --- a/website/docs/middleware/jwt.md +++ b/website/docs/middleware/jwt.md @@ -48,7 +48,7 @@ type Config struct { BeforeFunc middleware.BeforeFunc // SuccessHandler defines a function which is executed for a valid token. - SuccessHandler func(c echo.Context) + SuccessHandler func(c *echo.Context) // ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator // function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key. @@ -57,7 +57,7 @@ type Config struct { // Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler. // This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users // In that case you can use ErrorHandler to set default public JWT token value to request and continue with handler chain. - ErrorHandler func(c echo.Context, err error) error + ErrorHandler func(c *echo.Context, err error) error // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to // ignore the error (by returning `nil`). @@ -74,13 +74,13 @@ type Config struct { // This is one of the three options to provide a token validation key. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // Required if neither user-defined KeyFunc nor SigningKeys is provided. - SigningKey interface{} + SigningKey any // Map of signing keys to validate token with kid field usage. // This is one of the three options to provide a token validation key. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // Required if neither user-defined KeyFunc nor SigningKey is provided. - SigningKeys map[string]interface{} + SigningKeys map[string]any // Signing method used to check the token's signing algorithm. // Optional. Default value HS256. @@ -126,12 +126,12 @@ type Config struct { // ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token // parsing fails or parsed token is invalid. // Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library - ParseTokenFunc func(c echo.Context, auth string) (interface{}, error) + ParseTokenFunc func(c *echo.Context, auth string) (any, error) // Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation. // Not used if custom ParseTokenFunc is set. // Optional. Defaults to function returning jwt.MapClaims - NewClaimsFunc func(c echo.Context) jwt.Claims + NewClaimsFunc func(c *echo.Context) jwt.Claims } ``` diff --git a/website/docs/middleware/key-auth.md b/website/docs/middleware/key-auth.md index 3efe8c8d..4f3ee14b 100644 --- a/website/docs/middleware/key-auth.md +++ b/website/docs/middleware/key-auth.md @@ -13,7 +13,7 @@ Key auth middleware provides a key based authentication. ## Usage ```go -e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { +e.Use(middleware.KeyAuth(func(key string, c *echo.Context) (bool, error) { return key == "valid-key", nil })) ``` @@ -26,7 +26,7 @@ e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { e := echo.New() e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ KeyLookup: "query:api-key", - Validator: func(key string, c echo.Context) (bool, error) { + Validator: func(key string, c *echo.Context) (bool, error) { return key == "valid-key", nil }, })) diff --git a/website/docs/middleware/logger.md b/website/docs/middleware/logger.md index 92161747..ebd79ef3 100644 --- a/website/docs/middleware/logger.md +++ b/website/docs/middleware/logger.md @@ -118,10 +118,10 @@ type RequestLoggerConfig struct { Skipper Skipper // BeforeNextFunc defines a function that is called before next middleware or handler is called in chain. - BeforeNextFunc func(c echo.Context) + BeforeNextFunc func(c *echo.Context) // LogValuesFunc defines a function that is called with values extracted by logger from request/response. // Mandatory. - LogValuesFunc func(c echo.Context, v RequestLoggerValues) error + LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error // HandleError instructs logger to call global error handler when next middleware/handler returns an error. // This is useful when you have custom error handler that can decide to use different status codes. @@ -184,7 +184,7 @@ type RequestLoggerConfig struct { Example for naive `fmt.Printf` ```go -skipper := func(c echo.Context) bool { +skipper := func(c *echo.Context) bool { // Skip health check endpoint return c.Request().URL.Path == "/health" } @@ -192,10 +192,10 @@ e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogStatus: true, LogURI: true, Skipper: skipper, - BeforeNextFunc: func(c echo.Context) { + BeforeNextFunc: func(c *echo.Context) { c.Set("customValueFromContext", 42) }, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + 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 @@ -217,7 +217,7 @@ e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogURI: true, LogError: true, HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + 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), @@ -245,7 +245,7 @@ logger := zerolog.New(os.Stdout) e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogURI: true, LogStatus: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { logger.Info(). Str("URI", v.URI). Int("status", v.Status). @@ -266,7 +266,7 @@ logger, _ := zap.NewProduction() e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogURI: true, LogStatus: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { logger.Info("request", zap.String("URI", v.URI), zap.Int("status", v.Status), @@ -287,7 +287,7 @@ log := logrus.New() e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogURI: true, LogStatus: true, - LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error { + LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { log.WithFields(logrus.Fields{ "URI": values.URI, "status": values.Status, @@ -310,7 +310,7 @@ This panic arises when the `LogValuesFunc` callback function, which is mandatory To address this, you must define a suitable function that adheres to the `LogValuesFunc` specifications and then assign it within the middleware configuration. Consider the following straightforward illustration: ```go -func logValues(c echo.Context, v middleware.RequestLoggerValues) error { +func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) return nil } diff --git a/website/docs/middleware/prometheus.md b/website/docs/middleware/prometheus.md index 4929bf3e..b9af7fca 100644 --- a/website/docs/middleware/prometheus.md +++ b/website/docs/middleware/prometheus.md @@ -49,7 +49,7 @@ func main() { 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 { + e.GET("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "hello") }) @@ -74,7 +74,7 @@ func main() { } }() - app.GET("/hello", func(c echo.Context) error { + app.GET("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "hello") }) @@ -141,7 +141,7 @@ func main() { } e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ - AfterNext: func(c echo.Context, err error) { + AfterNext: func(c *echo.Context, err error) { customCounter.Inc() // use our custom metric in middleware. after every request increment the counter }, })) @@ -182,7 +182,7 @@ func main() { } e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ - AfterNext: func(c echo.Context, err error) { + AfterNext: func(c *echo.Context, err error) { customCounter.Inc() // use our custom metric in middleware. after every request increment the counter }, Registerer: customRegistry, // use our custom registry instead of default Prometheus registry @@ -217,7 +217,7 @@ func main() { e := echo.New() mwConfig := echoprometheus.MiddlewareConfig{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { return strings.HasPrefix(c.Path(), "/testurl") }, // does not gather metrics metrics on routes starting with `/testurl` } @@ -225,7 +225,7 @@ func main() { e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) @@ -257,10 +257,10 @@ func main() { e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ // labels of default metrics can be modified or added with `LabelFuncs` function LabelFuncs: map[string]echoprometheus.LabelValueFunc{ - "scheme": func(c echo.Context, err error) string { // additional custom label + "scheme": func(c *echo.Context, err error) string { // additional custom label return c.Scheme() }, - "host": func(c echo.Context, err error) string { // overrides default 'host' label value + "host": func(c *echo.Context, err error) string { // overrides default 'host' label value return "y_" + c.Request().Host }, }, @@ -286,7 +286,7 @@ func main() { e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics - e.GET("/hello", func(c echo.Context) error { + e.GET("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "hello") }) diff --git a/website/docs/middleware/recover.md b/website/docs/middleware/recover.md index dd67a401..d0335dbf 100644 --- a/website/docs/middleware/recover.md +++ b/website/docs/middleware/recover.md @@ -33,7 +33,7 @@ default values for `DisableStackAll` and `DisablePrintStack`. ```go // LogErrorFunc defines a function for custom logging in the middleware. -LogErrorFunc func(c echo.Context, err error, stack []byte) error +LogErrorFunc func(c *echo.Context, err error, stack []byte) error RecoverConfig struct { // Skipper defines a function to skip middleware. diff --git a/website/docs/middleware/request-id.md b/website/docs/middleware/request-id.md index fb896b9d..5d5f4c17 100644 --- a/website/docs/middleware/request-id.md +++ b/website/docs/middleware/request-id.md @@ -19,7 +19,7 @@ e.Use(middleware.RequestID()) e.Use(middleware.RequestID()) - e.GET("/", func(c echo.Context) error { + e.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID)) }) sc := echo.StartConfig{Address: ":1323"} diff --git a/website/docs/middleware/session.md b/website/docs/middleware/session.md index b9d6c413..9c6faa02 100644 --- a/website/docs/middleware/session.md +++ b/website/docs/middleware/session.md @@ -43,7 +43,7 @@ func main() { e := echo.New() e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret")))) - e.GET("/create-session", func(c echo.Context) error { + e.GET("/create-session", func(c *echo.Context) error { sess, err := session.Get("session", c) if err != nil { return err @@ -60,7 +60,7 @@ func main() { return c.NoContent(http.StatusOK) }) - e.GET("/read-session", func(c echo.Context) error { + e.GET("/read-session", func(c *echo.Context) error { sess, err := session.Get("session", c) if err != nil { return err diff --git a/website/docs/middleware/static.md b/website/docs/middleware/static.md index 9bf5b1b7..15faf437 100644 --- a/website/docs/middleware/static.md +++ b/website/docs/middleware/static.md @@ -63,7 +63,7 @@ func main() { Filesystem: http.FS(webAssets), })) api := e.Group("/api") - api.GET("/users", func(c echo.Context) error { + api.GET("/users", func(c *echo.Context) error { return c.String(http.StatusOK, "users") }) From b251a141e28a9187dd67b9ff95a14999f1f155ab Mon Sep 17 00:00:00 2001 From: toim Date: Tue, 13 Jan 2026 23:56:34 +0200 Subject: [PATCH 3/4] middlewares reworked for v5 --- cookbook/prometheus/server.go | 53 +++++++ go.mod | 23 ++- go.sum | 46 ++++++ website/docs/cookbook/http2-server-push.md | 5 +- website/docs/cookbook/http2.md | 5 +- website/docs/cookbook/reverse-proxy.md | 4 +- website/docs/middleware/basic-auth.md | 29 ++-- website/docs/middleware/body-dump.md | 31 +++- website/docs/middleware/body-limit.md | 16 +- website/docs/middleware/context-timeout.md | 12 +- website/docs/middleware/cors.md | 149 +++++++++++------- website/docs/middleware/csrf.md | 169 ++++++++++++++------- website/docs/middleware/decompress.md | 16 +- website/docs/middleware/gzip.md | 35 +++-- website/docs/middleware/jaeger.md | 16 +- website/docs/middleware/jwt.md | 7 +- website/docs/middleware/key-auth.md | 73 +++++---- website/docs/middleware/logger.md | 104 +------------ website/docs/middleware/method-override.md | 12 +- website/docs/middleware/prometheus.md | 96 ++++++------ website/docs/middleware/proxy.md | 93 ++++++++---- website/docs/middleware/rate-limiter.md | 30 ++-- website/docs/middleware/recover.md | 66 +++----- website/docs/middleware/redirect.md | 2 +- website/docs/middleware/request-id.md | 37 ++--- website/docs/middleware/rewrite.md | 34 +++-- website/docs/middleware/secure.md | 119 +++++++++------ website/docs/middleware/session.md | 21 +-- website/docs/middleware/static.md | 90 ++++++----- website/docs/middleware/trailing-slash.md | 29 ++-- 30 files changed, 835 insertions(+), 587 deletions(-) create mode 100644 cookbook/prometheus/server.go diff --git a/cookbook/prometheus/server.go b/cookbook/prometheus/server.go new file mode 100644 index 00000000..9e41bb4d --- /dev/null +++ b/cookbook/prometheus/server.go @@ -0,0 +1,53 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve route on port 8080 + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + // labels of default metrics can be modified or added with `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 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 `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/go.mod b/go.mod index 5335b35c..7ebd0a27 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,39 @@ go 1.25.0 require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/gorilla/websocket v1.5.3 + github.com/labstack/echo-contrib v0.17.5-0.20260113213056-68ad9fe34c3d github.com/labstack/echo-jwt/v5 v5.0.0-20260101195926-7cdd901b4337 github.com/labstack/echo/v5 v5.0.0-20260106091252-d6cb58b5c24e - github.com/lestrrat-go/jwx/v3 v3.0.12 + github.com/lestrrat-go/jwx/v3 v3.0.13 + github.com/prometheus/client_golang v1.23.2 github.com/r3labs/sse/v2 v2.10.0 - golang.org/x/crypto v0.46.0 - golang.org/x/net v0.48.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.47.0 + golang.org/x/net v0.49.0 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/segmentio/asm v1.2.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cb5b74ed..1c220a6c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,8 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,8 +12,20 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo-contrib v0.17.5-0.20260113213056-68ad9fe34c3d h1:P5nNuXYDv47aLCbf+jLWxmIgtCLPFZQPRqNP52KUoKI= +github.com/labstack/echo-contrib v0.17.5-0.20260113213056-68ad9fe34c3d/go.mod h1:tGZ39uN7bCDY4WtVWd2KiDFI2PxHB5lhLOtSNb+9tE4= github.com/labstack/echo-jwt/v5 v5.0.0-20260101195926-7cdd901b4337 h1:+9keiGOPRLJvrh+hp+4hApQK/seq3iVr0hVdRweUUYI= github.com/labstack/echo-jwt/v5 v5.0.0-20260101195926-7cdd901b4337/go.mod h1:7zV1rqeuZ57XS4nwL/ymw4tywVUzg5SEovZandLv9nI= github.com/labstack/echo/v5 v5.0.0-20260106091252-d6cb58b5c24e h1:QFu8S1iZijCXEUcP8u/h+JtZYlekbt0VIndrKbT/VAo= @@ -25,12 +42,26 @@ github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5 github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg= github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8= +github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= +github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -40,23 +71,38 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/website/docs/cookbook/http2-server-push.md b/website/docs/cookbook/http2-server-push.md index 1ad65e98..cd000978 100644 --- a/website/docs/cookbook/http2-server-push.md +++ b/website/docs/cookbook/http2-server-push.md @@ -49,8 +49,9 @@ If `http.Pusher` is supported, web assets are pushed; otherwise, client makes se ### 3) Start TLS server using cert.pem and key.pem ```go -if err := e.StartTLS(":1323", "cert.pem", "key.pem"); err != http.ErrServerClosed { - log.Fatal(err) +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) } ``` or use customized HTTP server with your own TLSConfig diff --git a/website/docs/cookbook/http2.md b/website/docs/cookbook/http2.md index cbb7efee..4b5cf38d 100644 --- a/website/docs/cookbook/http2.md +++ b/website/docs/cookbook/http2.md @@ -40,8 +40,9 @@ e.GET("/request", func(c *echo.Context) error { ## 3) Start TLS server using cert.pem and key.pem ```go -if err := e.StartTLS(":1323", "cert.pem", "key.pem"); err != http.ErrServerClosed { - log.Fatal(err) +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) } ``` or use customized HTTP server with your own TLSConfig diff --git a/website/docs/cookbook/reverse-proxy.md b/website/docs/cookbook/reverse-proxy.md index 869d8c0d..40dd6550 100644 --- a/website/docs/cookbook/reverse-proxy.md +++ b/website/docs/cookbook/reverse-proxy.md @@ -11,11 +11,11 @@ This recipe demonstrates how you can use Echo as a reverse proxy server and load ```go url1, err := url.Parse("http://localhost:8081") if err != nil { - e.Logger.Fatal(err) + e.Logger.Error("failed parse url", "error", err) } url2, err := url.Parse("http://localhost:8082") if err != nil { - e.Logger.Fatal(err) + e.Logger.Error("failed parse url", "error", err) } targets := []*middleware.ProxyTarget{ { diff --git a/website/docs/middleware/basic-auth.md b/website/docs/middleware/basic-auth.md index 8d1596e2..2afcda61 100644 --- a/website/docs/middleware/basic-auth.md +++ b/website/docs/middleware/basic-auth.md @@ -33,17 +33,24 @@ e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) ## Configuration ```go -BasicAuthConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Validator is a function to validate BasicAuth credentials. - // Required. - Validator BasicAuthValidator - - // Realm is a string to define realm attribute of BasicAuth. - // Default value "Restricted". - Realm string +type BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator is a function to validate BasicAuthWithConfig credentials. Note: if request contains multiple basic auth headers + // this function would be called once for each header until first valid result is returned + // Required. + Validator BasicAuthValidator + + // Realm is a string to define realm attribute of BasicAuthWithConfig. + // Default value "Restricted". + Realm string + + // AllowedCheckLimit set how many headers are allowed to be checked. This is useful + // environments like corporate test environments with application proxies restricting + // access to environment with their own auth scheme. + // Defaults to 1. + AllowedCheckLimit uint } ``` diff --git a/website/docs/middleware/body-dump.md b/website/docs/middleware/body-dump.md index ad5755a0..f38e96d0 100644 --- a/website/docs/middleware/body-dump.md +++ b/website/docs/middleware/body-dump.md @@ -10,7 +10,8 @@ Body dump middleware captures the request and response payload and calls the reg ```go e := echo.New() -e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte) { +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody []byte, resBody []byte, err error) { + // logic to handle request body and response body })) ``` @@ -26,13 +27,27 @@ e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) ## Configuration ```go -BodyDumpConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Handler receives request and response payload. - // Required. - Handler BodyDumpHandler +type BodyDumpConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Handler receives request, response payloads and handler error if there are 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. The handler callback receives truncated data. + // Default: 5 * MB (5,242,880 bytes) + // Set to -1 to disable limits (not recommended in production). + 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. The handler callback receives truncated data. + // Default: 5 * MB (5,242,880 bytes) + // Set to -1 to disable limits (not recommended in production). + MaxResponseBytes int64 } ``` diff --git a/website/docs/middleware/body-limit.md b/website/docs/middleware/body-limit.md index e0b06404..7ad474f5 100644 --- a/website/docs/middleware/body-limit.md +++ b/website/docs/middleware/body-limit.md @@ -9,14 +9,13 @@ size exceeds the configured limit, it sends "413 - Request Entity Too Large" response. The body limit is determined based on both `Content-Length` request header and actual content read, which makes it super secure. -Limit can be specified as `4x` or `4xB`, where x is one of the multiple from K, M, -G, T or P. +Limit is specified as bytes ## Usage ```go e := echo.New() -e.Use(middleware.BodyLimit("2M")) +e.Use(middleware.BodyLimit(2_097_152)) // 2MB ``` ## Custom Configuration @@ -31,13 +30,12 @@ e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) ## Configuration ```go -BodyLimitConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper +type BodyLimitConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper - // Maximum allowed size for a request body, it can be specified - // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. - Limit string `json:"limit"` + // LimitBytes is maximum allowed size in bytes for a request body + LimitBytes int64 } ``` diff --git a/website/docs/middleware/context-timeout.md b/website/docs/middleware/context-timeout.md index f7fe9e58..269a3a90 100644 --- a/website/docs/middleware/context-timeout.md +++ b/website/docs/middleware/context-timeout.md @@ -17,12 +17,12 @@ e.Use(middleware.ContextTimeout(60 * time.Second)) ```go e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ - // Skipper defines a function to skip middleware. - Skipper: nil, - // ErrorHandler is a function when error aries in middleware execution. - ErrorHandler: nil, - // Timeout configures a timeout for the middleware, defaults to 0 for no timeout - Timeout: 60 * time.Second, + // Skipper defines a function to skip middleware. + Skipper: nil, + // ErrorHandler is a function when error aries in middleware execution. + ErrorHandler: nil, + // Timeout configures a timeout for the middleware, defaults to 0 for no timeout + Timeout: 60 * time.Second, })) ``` diff --git a/website/docs/middleware/cors.md b/website/docs/middleware/cors.md index 2dd1be56..7e1f09af 100644 --- a/website/docs/middleware/cors.md +++ b/website/docs/middleware/cors.md @@ -11,7 +11,7 @@ data transfers. ## Usage ```go -e.Use(middleware.CORS()) +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) ``` ## Custom Configuration @@ -29,56 +29,103 @@ e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ ## Configuration ```go -CORSConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // AllowOrigin defines a list of origins that may access the resource. - // Optional. Default value []string{"*"}. - AllowOrigins []string `yaml:"allow_origins"` - - // AllowOriginFunc is a custom function to validate the origin. It takes the - // origin as an argument and returns true if allowed or false otherwise. If - // an error is returned, it is returned by the handler. If this option is - // set, AllowOrigins is ignored. - // Optional. - AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"` - - // AllowMethods defines a list methods allowed when accessing the resource. - // This is used in response to a preflight request. - // Optional. Default value DefaultCORSConfig.AllowMethods. - AllowMethods []string `yaml:"allow_methods"` - - // AllowHeaders defines a list of request headers that can be used when - // making the actual request. This is in response to a preflight request. - // Optional. Default value []string{}. - AllowHeaders []string `yaml:"allow_headers"` - - // AllowCredentials indicates whether or not the response to the request - // can be exposed when the credentials flag is true. When used as part of - // a response to a preflight request, this indicates whether or not the - // actual request can be made using credentials. - // Optional. Default value false. - AllowCredentials bool `yaml:"allow_credentials"` - - // ExposeHeaders defines a whitelist headers that clients are allowed to - // access. - // Optional. Default value []string{}. - ExposeHeaders []string `yaml:"expose_headers"` - - // MaxAge indicates how long (in seconds) the results of a preflight request - // can be cached. - // Optional. Default value 0. - MaxAge int `yaml:"max_age"` +type CORSConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // AllowOrigins determines the value of the Access-Control-Allow-Origin + // response header. This header defines a list of origins that may access the + // resource. + // + // Origin consist of following parts: `scheme + "://" + host + optional ":" + port` + // Wildcard can be used, but has to be set explicitly []string{"*"} + // Example: `https://example.com`, `http://example.com:8080`, `*` + // + // Security: use extreme caution when handling the origin, and carefully + // validate any logic. Remember that attackers may register hostile domain names. + // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + // + // Mandatory. + AllowOrigins []string + + // UnsafeAllowOriginFunc is an optional custom function to validate the origin. It takes the + // origin as an argument and returns + // - string, allowed origin + // - bool, true if allowed or false otherwise. + // - error, if an error is returned, it is returned immediately by the handler. + // If this option is set, AllowOrigins is ignored. + // + // Security: use extreme caution when handling the origin, and carefully + // validate any logic. Remember that attackers may register hostile (sub)domain names. + // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // Sub-domain checks 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. This header specified the list of methods allowed when + // accessing the resource. This is used in response to a preflight request. + // + // Optional. Default value DefaultCORSConfig.AllowMethods. + // If `allowMethods` is left empty, this middleware will fill for preflight + // request `Access-Control-Allow-Methods` header value + // from `Allow` header that echo.Router set into context. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + AllowMethods []string + + // AllowHeaders determines the value of the Access-Control-Allow-Headers + // response header. This header is used in response to a preflight request to + // indicate which HTTP headers can be used when making the actual request. + // + // Optional. Defaults to empty list. No domains allowed for CORS. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + AllowHeaders []string + + // AllowCredentials determines the value of the + // Access-Control-Allow-Credentials response header. This header indicates + // whether or not the response to the request can be exposed when the + // credentials mode (Request.credentials) is true. When used as part of a + // response to a preflight request, this indicates whether or not the actual + // request can be made using credentials. See also + // [MDN: Access-Control-Allow-Credentials]. + // + // Optional. Default value false, in which case the header is not set. + // + // Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`. + // See "Exploiting CORS misconfigurations for Bitcoins and bounties", + // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + AllowCredentials bool + + // ExposeHeaders determines the value of Access-Control-Expose-Headers, which + // defines a list of headers that clients are allowed to access. + // + // Optional. Default value []string{}, in which case the header is not set. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header + ExposeHeaders []string + + // MaxAge determines the value of the Access-Control-Max-Age response header. + // This header indicates how long (in seconds) the results of a preflight + // request can be cached. + // The header is set only if MaxAge != 0, negative value sends "0" which instructs browsers not to cache that response. + // + // Optional. Default value 0 - meaning header is not sent. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + MaxAge int } ``` -### Default Configuration - -```go -DefaultCORSConfig = CORSConfig{ - Skipper: DefaultSkipper, - AllowOrigins: []string{"*"}, - AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, -} -``` diff --git a/website/docs/middleware/csrf.md b/website/docs/middleware/csrf.md index abbdc062..954a5021 100644 --- a/website/docs/middleware/csrf.md +++ b/website/docs/middleware/csrf.md @@ -17,7 +17,42 @@ e.Use(middleware.CSRF()) ## Custom Configuration -### Usage +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. + +**How it works:** + +Modern browsers automatically send the `Sec-Fetch-Site` header with all requests, indicating the relationship +between the request origin and the target. The middleware uses this to make security decisions: + +- **`same-origin`** or **`none`**: Requests are allowed (exact origin match or direct user navigation) +- **`same-site`**: Falls back to token validation (e.g., subdomain to main domain) +- **`cross-site`**: Blocked by default with 403 error for unsafe methods (POST, PUT, DELETE, PATCH) + +For browsers that don't send this header (older browsers), the middleware seamlessly falls back to +traditional token-based CSRF protection. + +For `Sec-Fetch-Site` usage follow the configuration options: +- `TrustedOrigins []string`: Allowlist specific origins for cross-site requests (useful for OAuth callbacks, webhooks) +- `AllowSecFetchSiteFunc func(echo.Context) (bool, error)`: Custom logic for same-site/cross-site request validation + +```go +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + // Allow OAuth callbacks from trusted provider + TrustedOrigins: []string{"https://oauth-provider.com"}, + + // Custom validation for same-site requests + AllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) { + // Your custom authorization logic here + return validateCustomAuth(c), nil + // return true, err // blocks request with error + // return true, nil // allows CSRF request through + // return false, nil // falls back to legacy token logic + }, +})) +``` + +### Usage with token based CSRF protection ```go e := echo.New() @@ -55,63 +90,91 @@ CSRF token can be accessed from CSRF cookie. ## Configuration ```go -CSRFConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // TokenLength is the length of the generated token. - TokenLength uint8 `json:"token_length"` - // Optional. Default value 32. - - // TokenLookup is a string in the form of ":" that is used - // to extract token from the request. - // Optional. Default value "header:X-CSRF-Token". - // Possible values: - // - "header:" - // - "form:" - // - "query:" - // - "cookie:" - TokenLookup string `json:"token_lookup"` - - // Context key to store generated CSRF token into context. - // Optional. Default value "csrf". - ContextKey string `json:"context_key"` - - // Name of the CSRF cookie. This cookie will store CSRF token. - // Optional. Default value "_csrf". - CookieName string `json:"cookie_name"` - - // Domain of the CSRF cookie. - // Optional. Default value none. - CookieDomain string `json:"cookie_domain"` - - // Path of the CSRF cookie. - // Optional. Default value none. - CookiePath string `json:"cookie_path"` - - // Max age (in seconds) of the CSRF cookie. - // Optional. Default value 86400 (24hr). - CookieMaxAge int `json:"cookie_max_age"` - - // Indicates if CSRF cookie is secure. - // Optional. Default value false. - CookieSecure bool `json:"cookie_secure"` - - // Indicates if CSRF cookie is HTTP only. - // Optional. Default value false. - CookieHTTPOnly bool `json:"cookie_http_only"` +type CSRFConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + // TrustedOrigin permits any request with `Sec-Fetch-Site` header whose `Origin` header + // exactly matches the specified value. + // Values should be formated as Origin header "scheme://host[:port]". + // + // See [Origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin + // See [Sec-Fetch-Site]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers + TrustedOrigins []string + + // AllowSecFetchSameSite allows custom behaviour for `Sec-Fetch-Site` requests that are about to + // fail with CRSF error, to be allowed or replaced with custom error. + // This function applies to `Sec-Fetch-Site` values: + // - `same-site` same registrable domain (subdomain and/or different port) + // - `cross-site` request originates from different site + // See [Sec-Fetch-Site]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers + AllowSecFetchSiteFunc func(c *echo.Context) (bool, error) + + // TokenLength is the length of the generated token. + TokenLength uint8 + // Optional. Default value 32. + + // TokenLookup is a string in the form of ":" or ":,:" that is used + // to extract 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 token. + // Optional. Defaults tp randomString(TokenLength). + Generator func() string + + // Context key to store generated CSRF token into context. + // Optional. Default value "csrf". + ContextKey string + + // Name of the CSRF cookie. This cookie will store CSRF token. + // Optional. Default value "csrf". + CookieName string + + // Domain of the CSRF cookie. + // Optional. Default value none. + CookieDomain string + + // Path of the CSRF cookie. + // Optional. Default value none. + CookiePath string + + // Max age (in seconds) of the CSRF cookie. + // Optional. Default value 86400 (24hr). + CookieMaxAge int + + // Indicates if CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool + + // Indicates if CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // Indicates SameSite mode of the CSRF cookie. + // Optional. Default value SameSiteDefaultMode. + CookieSameSite http.SameSite + + // ErrorHandler defines a function which is executed for returning custom errors. + ErrorHandler func(c *echo.Context, err error) error } ``` ### Default Configuration ```go -DefaultCSRFConfig = CSRFConfig{ - Skipper: DefaultSkipper, - TokenLength: 32, - TokenLookup: "header:" + echo.HeaderXCSRFToken, - ContextKey: "csrf", - CookieName: "_csrf", - CookieMaxAge: 86400, +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, } ``` diff --git a/website/docs/middleware/decompress.md b/website/docs/middleware/decompress.md index a1dbe1f3..afda56a8 100644 --- a/website/docs/middleware/decompress.md +++ b/website/docs/middleware/decompress.md @@ -32,9 +32,19 @@ e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ ## Configuration ```go -DecompressConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper +type DecompressConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers + GzipDecompressPool Decompressor + + // MaxDecompressedSize limits the maximum size of decompressed request body in bytes. + // If the decompressed body exceeds this limit, the middleware returns HTTP 413 error. + // This prevents zip bomb attacks where small compressed payloads decompress to huge sizes. + // Default: 100 * MB (104,857,600 bytes) + // Set to -1 to disable limits (not recommended in production). + MaxDecompressedSize int64 } ``` diff --git a/website/docs/middleware/gzip.md b/website/docs/middleware/gzip.md index 28fe7a8e..b951b2c4 100644 --- a/website/docs/middleware/gzip.md +++ b/website/docs/middleware/gzip.md @@ -41,21 +41,26 @@ e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ ## Configuration ```go -GzipConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Gzip compression level. - // Optional. Default value -1. - Level int `json:"level"` +type GzipConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Gzip compression level. + // Optional. Default value -1. + Level int + + // Length threshold before gzip compression is applied. + // Optional. Default value 0. + // + // Most of the time you will not need to change the default. Compressing + // a short response might increase the transmitted data because of the + // gzip format overhead. Compressing the response will also consume CPU + // and time on the server and the client (for decompressing). Depending on + // your use case such a threshold might be useful. + // + // See also: + // https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits + MinLength int } ``` -### Default Configuration - -```go -DefaultGzipConfig = GzipConfig{ - Skipper: DefaultSkipper, - Level: -1, -} -``` diff --git a/website/docs/middleware/jaeger.md b/website/docs/middleware/jaeger.md index 5c249603..5f18a26f 100644 --- a/website/docs/middleware/jaeger.md +++ b/website/docs/middleware/jaeger.md @@ -18,7 +18,7 @@ Trace requests on Echo framework with Jaeger Tracing Middleware. package main import ( "github.com/labstack/echo-contrib/jaegertracing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) func main() { e := echo.New() @@ -26,7 +26,7 @@ func main() { c := jaegertracing.New(e, nil) defer c.Close() - sc := echo.StartConfig{Address: ":1323"} + sc := echo.StartConfig{Address: ":1323"} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) } @@ -85,7 +85,7 @@ package main import ( "strings" "github.com/labstack/echo-contrib/jaegertracing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // urlSkipper ignores metrics route on some middleware @@ -102,7 +102,7 @@ func main() { c := jaegertracing.New(e, urlSkipper) defer c.Close() - sc := echo.StartConfig{Address: ":1323"} + sc := echo.StartConfig{Address: ":1323"} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) } @@ -120,7 +120,7 @@ the duration of the invoked function. There is no need to change function argume package main import ( "github.com/labstack/echo-contrib/jaegertracing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "net/http" "time" ) @@ -134,7 +134,7 @@ func main() { jaegertracing.TraceFunction(c, slowFunc, "Test String") return c.String(http.StatusOK, "Hello, World!") }) - sc := echo.StartConfig{Address: ":1323"} + sc := echo.StartConfig{Address: ":1323"} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) } @@ -158,7 +158,7 @@ giving control on data to be appended to the span like log messages, baggages an package main import ( "github.com/labstack/echo-contrib/jaegertracing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) func main() { e := echo.New() @@ -176,7 +176,7 @@ func main() { time.Sleep(100 * time.Millisecond) return c.String(http.StatusOK, "Hello, World!") }) - sc := echo.StartConfig{Address: ":1323"} + 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/website/docs/middleware/jwt.md b/website/docs/middleware/jwt.md index a372f00d..7186c6d0 100644 --- a/website/docs/middleware/jwt.md +++ b/website/docs/middleware/jwt.md @@ -15,7 +15,7 @@ Basic middleware behavior: ## Dependencies ```go -import "github.com/labstack/echo-jwt/v4" +import "github.com/labstack/echo-jwt/v5" ``` ## Usage @@ -48,7 +48,9 @@ type Config struct { BeforeFunc middleware.BeforeFunc // SuccessHandler defines a function which is executed for a valid token. - SuccessHandler func(c *echo.Context) + // In case SuccessHandler error the middleware stops handler chain execution and + // returns error. + SuccessHandler func(c *echo.Context) error // ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator // function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key. @@ -83,6 +85,7 @@ type Config struct { SigningKeys map[string]any // Signing method used to check the token's signing algorithm. + // SigningMethod is not checked when a user-defined KeyFunc is provided. // Optional. Default value HS256. SigningMethod string diff --git a/website/docs/middleware/key-auth.md b/website/docs/middleware/key-auth.md index 4f3ee14b..4871f731 100644 --- a/website/docs/middleware/key-auth.md +++ b/website/docs/middleware/key-auth.md @@ -13,7 +13,7 @@ Key auth middleware provides a key based authentication. ## Usage ```go -e.Use(middleware.KeyAuth(func(key string, c *echo.Context) (bool, error) { +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { return key == "valid-key", nil })) ``` @@ -26,7 +26,7 @@ e.Use(middleware.KeyAuth(func(key string, c *echo.Context) (bool, error) { e := echo.New() e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ KeyLookup: "query:api-key", - Validator: func(key string, c *echo.Context) (bool, error) { + Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { return key == "valid-key", nil }, })) @@ -35,31 +35,50 @@ e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ ## Configuration ```go -KeyAuthConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // KeyLookup is a string in the form of ":" that is used - // to extract key from the request. - // Optional. Default value "header:Authorization". - // Possible values: - // - "header:" - // - "query:" - // - "cookie:" - // - "form:" - KeyLookup string `yaml:"key_lookup"` - - // AuthScheme to be used in the Authorization header. - // Optional. Default value "Bearer". - AuthScheme string - - // Validator is a function to validate key. - // Required. - Validator KeyAuthValidator - - // ErrorHandler defines a function which is executed for an invalid key. - // It may be used to define a custom error. - ErrorHandler KeyAuthErrorHandler +type KeyAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // KeyLookup is a string in the form of ":" or ":,:" that is used + // to extract key from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" or "header::" + // `` is argument value to cut/trim prefix of the extracted value. This is useful if header + // value has static prefix like `Authorization: ` where part that we + // want to cut is ` ` note the space at the end. + // In case of basic authentication `Authorization: Basic ` prefix we want to remove is `Basic `. + // - "query:" + // - "form:" + // - "cookie:" + // Multiple sources example: + // - "header:Authorization,header:X-Api-Key" + KeyLookup string + + // AllowedCheckLimit set how many KeyLookup values are allowed to be checked. This is + // useful environments like corporate test environments with application proxies restricting + // access to environment with their own auth scheme. + AllowedCheckLimit uint + + // Validator is a function to validate key. + // Required. + Validator KeyAuthValidator + + // ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator + // function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key. + // It may be used to define a custom error. + // + // Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler. + // This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users + // In that case you can use ErrorHandler to set default public auth value to request and continue with handler chain. + ErrorHandler KeyAuthErrorHandler + + // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to + // ignore the error (by returning `nil`). + // This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality. + // In that case you can use ErrorHandler to set a default public key auth value in the request context + // and continue. Some logic down the remaining execution chain needs to check that (public) key auth value then. + ContinueOnIgnoredError bool } ``` diff --git a/website/docs/middleware/logger.md b/website/docs/middleware/logger.md index ebd79ef3..7ce96c25 100644 --- a/website/docs/middleware/logger.md +++ b/website/docs/middleware/logger.md @@ -4,109 +4,9 @@ description: Logger middleware # Logger -Logger middleware logs the information about each HTTP request. +[`RequestLogger`](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) middleware logs the information about each HTTP request. -Echo has 2 different logger middlewares: - -- Older string template based logger [`Logger`](https://github.com/labstack/echo/blob/master/middleware/logger.go) - easy to start with but has limited capabilities -- Newer customizable function based logger [`RequestLogger`](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) - allows developer fully to customize what is logged and how it is logged. Suitable for usage with 3rd party logger libraries. - -## Old Logger middleware - -## Usage - -```go -e.Use(middleware.RequestLogger()) -``` - -*Sample output* - -```exec -{"time":"2017-01-12T08:58:07.372015644-08:00","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/","status":200,"error":"","latency":14743,"latency_human":"14.743µs","bytes_in":0,"bytes_out":2} -``` - -## Custom Configuration - -### Usage - -```go -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - Format: "method=${method}, uri=${uri}, status=${status}\n", -})) -``` - -Example above uses a `Format` which logs request method and request URI. - -*Sample output* - -```exec -method=GET, uri=/, status=200 -``` - -## Configuration - -```go -// LoggerConfig defines the config for Logger middleware. -LoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Tags to construct the logger format. - // - // - time_unix - // - time_unix_milli - // - time_unix_micro - // - time_unix_nano - // - time_rfc3339 - // - time_rfc3339_nano - // - time_custom - // - id (Request ID) - // - remote_ip - // - uri - // - host - // - method - // - path - // - protocol - // - referer - // - user_agent - // - status - // - error - // - latency (In nanoseconds) - // - latency_human (Human readable) - // - bytes_in (Bytes received) - // - bytes_out (Bytes sent) - // - header: - // - query: - // - form: - // - // Example "${remote_ip} ${status}" - // - // Optional. Default value DefaultLoggerConfig.Format. - Format string `yaml:"format"` - - // Optional. Default value DefaultLoggerConfig.CustomTimeFormat. - CustomTimeFormat string `yaml:"custom_time_format"` - - // Output is a writer where logs in JSON format are written. - // Optional. Default value os.Stdout. - Output io.Writer -} -``` - -### Default Configuration - -```go -DefaultLoggerConfig = LoggerConfig{ - Skipper: DefaultSkipper, - Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` + - `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + - `"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` + - `,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", - CustomTimeFormat: "2006-01-02 15:04:05.00000", -} -``` - -## New RequestLogger middleware +## RequestLogger middleware RequestLogger middleware allows developer fully to customize what is logged and how it is logged and is more suitable for usage with 3rd party (structured logging) libraries. diff --git a/website/docs/middleware/method-override.md b/website/docs/middleware/method-override.md index b8f24f9f..451935b5 100644 --- a/website/docs/middleware/method-override.md +++ b/website/docs/middleware/method-override.md @@ -33,13 +33,13 @@ e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ ## Configuration ```go -MethodOverrideConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper +type MethodOverrideConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper - // Getter is a function that gets overridden method from the request. - // Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride). - Getter MethodOverrideGetter + // Getter is a function that gets overridden method from the request. + // Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride). + Getter MethodOverrideGetter } ``` diff --git a/website/docs/middleware/prometheus.md b/website/docs/middleware/prometheus.md index b9af7fca..0ba70d90 100644 --- a/website/docs/middleware/prometheus.md +++ b/website/docs/middleware/prometheus.md @@ -12,12 +12,7 @@ Echo community contribution Prometheus middleware generates metrics for HTTP requests. -There are 2 versions of Prometheus middleware: - -- latest (recommended) https://github.com/labstack/echo-contrib/blob/master/echoprometheus/prometheus.go -- old (deprecated) https://github.com/labstack/echo-contrib/blob/master/prometheus/prometheus.go) - -Migration guide from old to newer middleware can found [here](https://github.com/labstack/echo-contrib/blob/master/echoprometheus/README.md). +https://github.com/labstack/echo-contrib/blob/master/echoprometheus/prometheus.go ## Usage @@ -37,24 +32,24 @@ Serve metric from the same server as where metrics is gathered package main import ( - "errors" - "github.com/labstack/echo-contrib/echoprometheus" - "github.com/labstack/echo/v4" - "log" "net/http" + + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v5" ) func main() { e := echo.New() - e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + + 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 && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) } } ``` @@ -63,23 +58,23 @@ Serve metrics on a separate port ```go func main() { - app := echo.New() // this Echo instance will serve route on port 8080 - app.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + e := echo.New() // this Echo instance will serve route 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 := 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 && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + if err := metrics.Start(":8081"); err != nil { + e.Logger.Error("failed to start metrics server", "error", err) } }() - app.GET("/hello", func(c *echo.Context) error { + e.GET("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "hello") }) - if err := app.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) } } ``` @@ -119,16 +114,15 @@ Using custom metrics with Prometheus default registry: package main import ( - "errors" + "log" + "github.com/labstack/echo-contrib/echoprometheus" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/prometheus/client_golang/prometheus" - "log" - "net/http" ) func main() { - e := echo.New() + e := echo.New() // this Echo instance will serve route on port 8080 customCounter := prometheus.NewCounter( // create new counter metric. This is replacement for `prometheus.Metric` struct prometheus.CounterOpts{ @@ -147,10 +141,11 @@ func main() { })) e.GET("/metrics", echoprometheus.NewHandler()) // register route for getting gathered metrics - if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + 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 that: @@ -159,16 +154,15 @@ or create your own registry and register custom metrics with that: package main import ( - "errors" + "log" + "github.com/labstack/echo-contrib/echoprometheus" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/prometheus/client_golang/prometheus" - "log" - "net/http" ) func main() { - e := echo.New() + e := echo.New() // this Echo instance will serve route on port 8080 customRegistry := prometheus.NewRegistry() // create custom registry for your custom metrics customCounter := prometheus.NewCounter( // create new counter metric. This is replacement for `prometheus.Metric` struct @@ -189,10 +183,11 @@ func main() { })) e.GET("/metrics", echoprometheus.NewHandlerWithConfig(echoprometheus.HandlerConfig{Gatherer: customRegistry})) // register route for getting gathered metrics data from our custom Registry - if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) } } + ``` ### Skipping URL(s) @@ -205,16 +200,15 @@ A middleware skipper can be passed to avoid generating metrics to certain URL(s) package main import ( - "errors" - "github.com/labstack/echo-contrib/echoprometheus" - "github.com/labstack/echo/v4" - "log" "net/http" "strings" + + "github.com/labstack/echo-contrib/echoprometheus" + "github.com/labstack/echo/v5" ) func main() { - e := echo.New() + e := echo.New() // this Echo instance will serve route on port 8080 mwConfig := echoprometheus.MiddlewareConfig{ Skipper: func(c *echo.Context) bool { @@ -228,11 +222,12 @@ func main() { e.GET("/", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) - - if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) } } + ``` ## Complex Scenarios @@ -243,16 +238,15 @@ Example: modify default `echoprometheus` metrics definitions package main import ( - "errors" + "net/http" + "github.com/labstack/echo-contrib/echoprometheus" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/prometheus/client_golang/prometheus" - "log" - "net/http" ) func main() { - e := echo.New() + e := echo.New() // this Echo instance will serve route on port 8080 e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ // labels of default metrics can be modified or added with `LabelFuncs` function @@ -290,8 +284,8 @@ func main() { return c.String(http.StatusOK, "hello") }) - if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) } } ``` diff --git a/website/docs/middleware/proxy.md b/website/docs/middleware/proxy.md index 3ccb95c1..e5051de5 100644 --- a/website/docs/middleware/proxy.md +++ b/website/docs/middleware/proxy.md @@ -12,11 +12,11 @@ to upstream server using a configured load balancing technique. ```go url1, err := url.Parse("http://localhost:8081") if err != nil { - e.Logger.Fatal(err) + e.Logger.Error("failed parse url", "error", err) } url2, err := url.Parse("http://localhost:8082") if err != nil { - e.Logger.Fatal(err) + e.Logger.Error("failed parse url", "error", err) } e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ { @@ -40,33 +40,68 @@ e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) ### Configuration ```go -// ProxyConfig defines the config for Proxy middleware. - ProxyConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Balancer defines a load balancing technique. - // Required. - Balancer ProxyBalancer - - // Rewrite defines URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - Rewrite map[string]string - - // RegexRewrite defines rewrite rules using regexp.Rexexp with captures - // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. - 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 +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.Rexexp 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 diff --git a/website/docs/middleware/rate-limiter.md b/website/docs/middleware/rate-limiter.md index 5060297f..5a48c646 100644 --- a/website/docs/middleware/rate-limiter.md +++ b/website/docs/middleware/rate-limiter.md @@ -15,7 +15,7 @@ To add a rate limit to your application simply add the `RateLimiter` middleware. The example below will limit the application to 20 requests/sec using the default in-memory store: ```go -e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(20)))) +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) ``` :::info @@ -28,20 +28,20 @@ If the provided rate is a float number, Burst will be treated as the rounded dow ```go config := middleware.RateLimiterConfig{ - Skipper: middleware.DefaultSkipper, - Store: middleware.NewRateLimiterMemoryStoreWithConfig( - middleware.RateLimiterMemoryStoreConfig{Rate: rate.Limit(10), Burst: 30, ExpiresIn: 3 * time.Minute}, - ), - IdentifierExtractor: func(ctx echo.Context) (string, error) { - id := ctx.RealIP() - return id, nil - }, - ErrorHandler: func(context echo.Context, err error) error { - return context.JSON(http.StatusForbidden, nil) - }, - DenyHandler: func(context echo.Context, identifier string,err error) error { - return context.JSON(http.StatusTooManyRequests, nil) - }, + 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)) diff --git a/website/docs/middleware/recover.md b/website/docs/middleware/recover.md index d0335dbf..54c38a00 100644 --- a/website/docs/middleware/recover.md +++ b/website/docs/middleware/recover.md @@ -22,62 +22,40 @@ e.Use(middleware.Recover()) e := echo.New() e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ StackSize: 1 << 10, // 1 KB - LogLevel: log.ERROR, })) ``` -Example above uses a `StackSize` of 1 KB, `LogLevel` of error and -default values for `DisableStackAll` and `DisablePrintStack`. +Example above uses a `StackSize` of 1 KB and default values for `DisableStackAll` and `DisablePrintStack`. ## Configuration ```go -// LogErrorFunc defines a function for custom logging in the middleware. -LogErrorFunc func(c *echo.Context, err error, stack []byte) error - -RecoverConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Size of the stack to be printed. - // Optional. Default value 4KB. - StackSize int `yaml:"stack_size"` - - // DisableStackAll disables formatting stack traces of all other goroutines - // into buffer after the trace for the current goroutine. - // Optional. Default value false. - DisableStackAll bool `yaml:"disable_stack_all"` - - // DisablePrintStack disables printing stack trace. - // Optional. Default value as false. - DisablePrintStack bool `yaml:"disable_print_stack"` - - // LogLevel is log level to printing stack trace. - // Optional. Default value 0 (Print). - LogLevel log.Lvl - - // LogErrorFunc defines a function for custom logging in the middleware. - // If it's set you don't need to provide LogLevel for config. - LogErrorFunc LogErrorFunc - - // DisableErrorHandler disables the call to centralized HTTPErrorHandler. - // The recovered error is then passed back to upstream middleware, instead of swallowing the error. - // Optional. Default value false. - DisableErrorHandler bool `yaml:"disable_error_handler"` - +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 buffer after the trace for the current goroutine. + // Optional. Default value false. + DisableStackAll bool + + // DisablePrintStack disables printing stack trace. + // Optional. Default value as false. + DisablePrintStack bool } ``` ### Default Configuration ```go -DefaultRecoverConfig = RecoverConfig{ - Skipper: DefaultSkipper, - StackSize: 4 << 10, // 4 KB - DisableStackAll: false, - DisablePrintStack: false, - LogLevel: 0, - LogErrorFunc: nil, - DisableErrorHandler: false, +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, } ``` diff --git a/website/docs/middleware/redirect.md b/website/docs/middleware/redirect.md index 30919d0b..8723a706 100644 --- a/website/docs/middleware/redirect.md +++ b/website/docs/middleware/redirect.md @@ -87,7 +87,7 @@ RedirectConfig struct { // Status code to be used when redirecting the request. // Optional. Default value http.StatusMovedPermanently. - Code int `json:"code"` + Code int } ``` diff --git a/website/docs/middleware/request-id.md b/website/docs/middleware/request-id.md index 5d5f4c17..25e0067e 100644 --- a/website/docs/middleware/request-id.md +++ b/website/docs/middleware/request-id.md @@ -15,17 +15,19 @@ e.Use(middleware.RequestID()) *Example* ```go - e := echo.New() +func main() { + e := echo.New() - e.Use(middleware.RequestID()) + e.Use(middleware.RequestID()) - e.GET("/", func(c *echo.Context) error { - return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID)) - }) - sc := echo.StartConfig{Address: ":1323"} - if err := sc.Start(context.Background(), e); err != nil { + 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 @@ -43,19 +45,20 @@ e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ ## Configuration ```go -RequestIDConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper +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 + // 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(echo.Context, 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 - TargetHeader string + // TargetHeader defines what header to look for to populate the id. + // Optional. Default value is `X-Request-Id` + TargetHeader string } ``` diff --git a/website/docs/middleware/rewrite.md b/website/docs/middleware/rewrite.md index ee4a828d..8e9a0964 100644 --- a/website/docs/middleware/rewrite.md +++ b/website/docs/middleware/rewrite.md @@ -39,19 +39,27 @@ e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) ### Configuration ```go -// RewriteConfig defines the config for Rewrite middleware. - 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. - Rules map[string]string `yaml:"rules"` - - // RegexRules defines the URL path rewrite rules using regexp.Rexexp with captures - // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. - RegexRules map[*regexp.Regexp]string - } +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.Rexexp 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: diff --git a/website/docs/middleware/secure.md b/website/docs/middleware/secure.md index a6e629ff..bdb615d5 100644 --- a/website/docs/middleware/secure.md +++ b/website/docs/middleware/secure.md @@ -39,61 +39,80 @@ disables that protection. ## Configuration ```go -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 `json:"xss_protection"` - - // ContentTypeNosniff provides protection against overriding Content-Type - // header by setting the `X-Content-Type-Options` header. - // Optional. Default value "nosniff". - ContentTypeNosniff string `json:"content_type_nosniff"` - - // XFrameOptions can be used to indicate whether or not a browser should - // be allowed to render a page in a ,