From 2d1eed5a6de757527e69a51d8ddfc11f43d2317a Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Mon, 15 Jun 2026 11:15:02 -0700 Subject: [PATCH] chore: retire Docusaurus site, rewrite README for the Astro docs The docs now live in site/ (Astro + Starlight) and deploy from there (#400/#401). Remove the superseded website/ Docusaurus project, and: - rewrite README.md around site/ + cookbook/ (was Docusaurus instructions) - Makefile `serve` now runs the Astro dev server - test-deploy workflow builds site/ on PRs to master (was website/ on `main`, which never ran) with npm ci --ignore-scripts Verified: go test ./... passes, site builds clean (124 pages). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test-deploy.yaml | 27 +- Makefile | 2 +- README.md | 52 +- website/.gitignore | 20 - website/.nvmrc | 1 - website/babel.config.js | 3 - website/docs/cookbook/_category_.json | 7 - website/docs/cookbook/auto-tls.md | 25 - website/docs/cookbook/cors.md | 17 - website/docs/cookbook/crud.md | 82 - website/docs/cookbook/embed-resources.md | 12 - website/docs/cookbook/file-download.md | 47 - website/docs/cookbook/file-upload.md | 34 - website/docs/cookbook/graceful-shutdown.md | 17 - website/docs/cookbook/hello-world.md | 11 - website/docs/cookbook/http2-server-push.md | 90 - website/docs/cookbook/http2.md | 77 - website/docs/cookbook/jsonp.md | 19 - website/docs/cookbook/jwt.md | 57 - website/docs/cookbook/load-balancing.md | 51 - website/docs/cookbook/middleware.md | 40 - website/docs/cookbook/reverse-proxy.md | 79 - website/docs/cookbook/sse.md | 44 - website/docs/cookbook/streaming-response.md | 30 - website/docs/cookbook/subdomain.md | 11 - website/docs/cookbook/timeout.md | 11 - website/docs/cookbook/websocket.md | 45 - website/docs/guide/_category_.json | 8 - website/docs/guide/binding.md | 254 - website/docs/guide/cookies.md | 75 - website/docs/guide/customization.md | 71 - website/docs/guide/error-handling.md | 122 - website/docs/guide/ip-address.md | 111 - website/docs/guide/quick-start.md | 241 - website/docs/guide/request.md | 164 - website/docs/guide/response.md | 351 - website/docs/guide/routing.md | 522 - website/docs/guide/start-server.md | 146 - website/docs/guide/static-files.md | 98 - website/docs/guide/templates.md | 129 - website/docs/guide/testing.md | 277 - website/docs/introduction.md | 34 - website/docs/middleware/_category_.json | 7 - website/docs/middleware/basic-auth.md | 63 - website/docs/middleware/body-dump.md | 60 - website/docs/middleware/body-limit.md | 48 - website/docs/middleware/casbin-auth.md | 203 - website/docs/middleware/context-timeout.md | 29 - website/docs/middleware/cors.md | 131 - website/docs/middleware/csrf.md | 186 - website/docs/middleware/decompress.md | 57 - website/docs/middleware/gzip.md | 66 - .../middleware/img/docsVersionDropdown.png | Bin 25427 -> 0 bytes .../docs/middleware/img/localeDropdown.png | Bin 27841 -> 0 bytes website/docs/middleware/jwt.md | 142 - website/docs/middleware/key-auth.md | 93 - website/docs/middleware/logger.md | 224 - website/docs/middleware/method-override.md | 53 - website/docs/middleware/open-telemetry.md | 72 - website/docs/middleware/prometheus.md | 285 - website/docs/middleware/proxy.md | 134 - website/docs/middleware/rate-limiter.md | 110 - website/docs/middleware/recover.md | 61 - website/docs/middleware/redirect.md | 101 - website/docs/middleware/request-id.md | 89 - website/docs/middleware/rewrite.md | 88 - website/docs/middleware/secure.md | 118 - website/docs/middleware/session.md | 185 - website/docs/middleware/static.md | 148 - website/docs/middleware/trailing-slash.md | 66 - website/docusaurus.config.js | 171 - website/package-lock.json | 18099 ---------------- website/package.json | 45 - website/sidebars.js | 33 - website/src/components/AskEcho.js | 158 - website/src/components/DocActions.js | 44 - .../src/components/HomepageFeatures/index.js | 132 - .../HomepageFeatures/styles.module.css | 20 - .../src/components/HomepageSponsors/index.js | 54 - .../HomepageSponsors/styles.module.css | 12 - website/src/css/custom.css | 245 - website/src/pages/index.js | 53 - website/src/pages/index.module.css | 35 - website/src/theme/DocItem/Content/index.js | 13 - website/src/theme/Root.js | 15 - website/static/.nojekyll | 0 website/static/CNAME | 1 - website/static/echo-redesign.html | 361 - website/static/img/0075-cloud.svg | 6 - website/static/img/0101-database-upload.svg | 7 - website/static/img/0102-database-download.svg | 7 - website/static/img/0221-license2.svg | 12 - website/static/img/0243-equalizer.svg | 14 - website/static/img/0567-speed-fast.svg | 7 - website/static/img/0568-rocket.svg | 8 - website/static/img/0780-code.svg | 8 - website/static/img/0893-funnel.svg | 6 - website/static/img/doit-logo.png | Bin 36767 -> 0 bytes website/static/img/donation.png | Bin 22651 -> 0 bytes website/static/img/echo-social-card.png | Bin 28943 -> 0 bytes website/static/img/favicon.ico | Bin 900 -> 0 bytes website/static/img/favicon.svg | 6 - website/static/img/labstack-icon.png | Bin 4548 -> 0 bytes website/static/img/logo-dark.svg | 1 - website/static/img/logo-light.svg | 1 - website/static/img/microcms-logo.png | Bin 10208 -> 0 bytes website/static/img/shiguredo-logo.png | Bin 33515 -> 0 bytes website/static/img/terminal.png | Bin 42971 -> 0 bytes 108 files changed, 54 insertions(+), 25853 deletions(-) delete mode 100644 website/.gitignore delete mode 100644 website/.nvmrc delete mode 100644 website/babel.config.js delete mode 100644 website/docs/cookbook/_category_.json delete mode 100644 website/docs/cookbook/auto-tls.md delete mode 100644 website/docs/cookbook/cors.md delete mode 100644 website/docs/cookbook/crud.md delete mode 100644 website/docs/cookbook/embed-resources.md delete mode 100644 website/docs/cookbook/file-download.md delete mode 100644 website/docs/cookbook/file-upload.md delete mode 100644 website/docs/cookbook/graceful-shutdown.md delete mode 100644 website/docs/cookbook/hello-world.md delete mode 100644 website/docs/cookbook/http2-server-push.md delete mode 100644 website/docs/cookbook/http2.md delete mode 100644 website/docs/cookbook/jsonp.md delete mode 100644 website/docs/cookbook/jwt.md delete mode 100644 website/docs/cookbook/load-balancing.md delete mode 100644 website/docs/cookbook/middleware.md delete mode 100644 website/docs/cookbook/reverse-proxy.md delete mode 100644 website/docs/cookbook/sse.md delete mode 100644 website/docs/cookbook/streaming-response.md delete mode 100644 website/docs/cookbook/subdomain.md delete mode 100644 website/docs/cookbook/timeout.md delete mode 100644 website/docs/cookbook/websocket.md delete mode 100644 website/docs/guide/_category_.json delete mode 100644 website/docs/guide/binding.md delete mode 100644 website/docs/guide/cookies.md delete mode 100644 website/docs/guide/customization.md delete mode 100644 website/docs/guide/error-handling.md delete mode 100644 website/docs/guide/ip-address.md delete mode 100644 website/docs/guide/quick-start.md delete mode 100644 website/docs/guide/request.md delete mode 100644 website/docs/guide/response.md delete mode 100644 website/docs/guide/routing.md delete mode 100644 website/docs/guide/start-server.md delete mode 100644 website/docs/guide/static-files.md delete mode 100644 website/docs/guide/templates.md delete mode 100644 website/docs/guide/testing.md delete mode 100644 website/docs/introduction.md delete mode 100644 website/docs/middleware/_category_.json delete mode 100644 website/docs/middleware/basic-auth.md delete mode 100644 website/docs/middleware/body-dump.md delete mode 100644 website/docs/middleware/body-limit.md delete mode 100644 website/docs/middleware/casbin-auth.md delete mode 100644 website/docs/middleware/context-timeout.md delete mode 100644 website/docs/middleware/cors.md delete mode 100644 website/docs/middleware/csrf.md delete mode 100644 website/docs/middleware/decompress.md delete mode 100644 website/docs/middleware/gzip.md delete mode 100644 website/docs/middleware/img/docsVersionDropdown.png delete mode 100644 website/docs/middleware/img/localeDropdown.png delete mode 100644 website/docs/middleware/jwt.md delete mode 100644 website/docs/middleware/key-auth.md delete mode 100644 website/docs/middleware/logger.md delete mode 100644 website/docs/middleware/method-override.md delete mode 100644 website/docs/middleware/open-telemetry.md delete mode 100644 website/docs/middleware/prometheus.md delete mode 100644 website/docs/middleware/proxy.md delete mode 100644 website/docs/middleware/rate-limiter.md delete mode 100644 website/docs/middleware/recover.md delete mode 100644 website/docs/middleware/redirect.md delete mode 100644 website/docs/middleware/request-id.md delete mode 100644 website/docs/middleware/rewrite.md delete mode 100644 website/docs/middleware/secure.md delete mode 100644 website/docs/middleware/session.md delete mode 100644 website/docs/middleware/static.md delete mode 100644 website/docs/middleware/trailing-slash.md delete mode 100644 website/docusaurus.config.js delete mode 100644 website/package-lock.json delete mode 100644 website/package.json delete mode 100644 website/sidebars.js delete mode 100644 website/src/components/AskEcho.js delete mode 100644 website/src/components/DocActions.js delete mode 100644 website/src/components/HomepageFeatures/index.js delete mode 100644 website/src/components/HomepageFeatures/styles.module.css delete mode 100644 website/src/components/HomepageSponsors/index.js delete mode 100644 website/src/components/HomepageSponsors/styles.module.css delete mode 100644 website/src/css/custom.css delete mode 100644 website/src/pages/index.js delete mode 100644 website/src/pages/index.module.css delete mode 100644 website/src/theme/DocItem/Content/index.js delete mode 100644 website/src/theme/Root.js delete mode 100644 website/static/.nojekyll delete mode 100644 website/static/CNAME delete mode 100644 website/static/echo-redesign.html delete mode 100755 website/static/img/0075-cloud.svg delete mode 100755 website/static/img/0101-database-upload.svg delete mode 100755 website/static/img/0102-database-download.svg delete mode 100755 website/static/img/0221-license2.svg delete mode 100755 website/static/img/0243-equalizer.svg delete mode 100755 website/static/img/0567-speed-fast.svg delete mode 100755 website/static/img/0568-rocket.svg delete mode 100755 website/static/img/0780-code.svg delete mode 100755 website/static/img/0893-funnel.svg delete mode 100644 website/static/img/doit-logo.png delete mode 100644 website/static/img/donation.png delete mode 100644 website/static/img/echo-social-card.png delete mode 100644 website/static/img/favicon.ico delete mode 100644 website/static/img/favicon.svg delete mode 100644 website/static/img/labstack-icon.png delete mode 100644 website/static/img/logo-dark.svg delete mode 100644 website/static/img/logo-light.svg delete mode 100644 website/static/img/microcms-logo.png delete mode 100644 website/static/img/shiguredo-logo.png delete mode 100644 website/static/img/terminal.png diff --git a/.github/workflows/test-deploy.yaml b/.github/workflows/test-deploy.yaml index 6b572fba..88f6454b 100644 --- a/.github/workflows/test-deploy.yaml +++ b/.github/workflows/test-deploy.yaml @@ -1,26 +1,29 @@ -name: Test deployment +name: Test build on: pull_request: branches: - - main - # Review gh actions docs if you want to further define triggers, paths, etc - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + - master jobs: - test-deploy: - name: Test deployment + test-build: + name: Test build (docs site) runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: node-version: 24 - cache: yarn + cache: npm + cache-dependency-path: site/package-lock.json - name: Install dependencies - run: yarn install --frozen-lockfile - working-directory: website - - name: Test build website - run: yarn build - working-directory: website + run: npm ci --ignore-scripts + working-directory: site + - name: Test build + run: npm run build + working-directory: site + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 591e723f..5c340704 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,6 @@ test: go test -race ./... serve: - docker run --rm -it --name echo-docs -v ${CURDIR}/website:/home/app -w /home/app -p 3000:3000 -u node node:lts /bin/bash -c "npm install && npm start -- --host=0.0.0.0" + cd site && npm install && npm run dev .PHONY: test serve diff --git a/README.md b/README.md index 8f79f7d2..1d16acd4 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,51 @@ -# Echo Extra +# echox -### Running Recipes +Source for the [Echo](https://github.com/labstack/echo) documentation site — +published at **[echo.labstack.com](https://echo.labstack.com)** — together with +the runnable cookbook recipes the docs reference. -- Go into any cookbook folder and run `go run server.go` +## Layout -### Running/Developing Website Locally +| Path | What it is | +| ----------- | --------------------------------------------------------------------------------------------------- | +| `site/` | The docs site — [Astro](https://astro.build) + [Starlight](https://starlight.astro.build). Content lives in `site/src/content/docs/`. | +| `cookbook/` | Standalone, runnable Go example apps referenced from the docs. | +| `docs/` | Internal design specs. | -- Ensure that you have Node.js installed on your computer. If you don't have it, download and install it from the - official Node.js website (https://nodejs.org). Or you could use NVM (https://github.com/nvm-sh/nvm) to use appropriate - version of Node. -- Inside the `website` folder, install the required dependencies by running the following command `npm install`. -- Run the website locally by running the following command `npm start`. +## Documentation site + +Requires [Node.js](https://nodejs.org) (LTS). ```bash -cd website +cd site npm install -npm start +npm run dev # dev server at http://localhost:4321 +npm run build # production build to site/dist +npm run preview # preview the production build ``` -### Running/Developing Website in docker +Content is Markdown/MDX under `site/src/content/docs/` (`guide/`, `middleware/`, +`cookbook/`). To add a page, drop a file in the right folder — the sidebar is +generated from each page's `sidebar.order` frontmatter. Every page needs a +`title` and `description`. + +## Cookbook recipes -This will serve website on http://localhost:3000/ +Each folder under `cookbook/` is a self-contained example. Run one with: ```bash -docker run --rm -it --name echo-docs -v ${PWD}/website:/home/app -w /home/app -p 3000:3000 -u node node:lts /bin/bash -c "npm install && npm start -- --host=0.0.0.0" +cd cookbook/hello-world +go run . ``` + +## Deployment + +The site auto-deploys to GitHub Pages on every push to `master` (and once daily, +to refresh build-time data such as the GitHub star count) via +[`.github/workflows/deploy.yaml`](.github/workflows/deploy.yaml). Dependencies +are installed with `npm ci --ignore-scripts` and pinned via the committed +lockfile. + +## License + +[MIT](LICENSE) diff --git a/website/.gitignore b/website/.gitignore deleted file mode 100644 index b2d6de30..00000000 --- a/website/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/website/.nvmrc b/website/.nvmrc deleted file mode 100644 index 18c92ea9..00000000 --- a/website/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v24 \ No newline at end of file diff --git a/website/babel.config.js b/website/babel.config.js deleted file mode 100644 index e00595da..00000000 --- a/website/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [require.resolve('@docusaurus/core/lib/babel/preset')], -}; diff --git a/website/docs/cookbook/_category_.json b/website/docs/cookbook/_category_.json deleted file mode 100644 index 07bb7971..00000000 --- a/website/docs/cookbook/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Cookbook", - "position": 4, - "link": { - "type": "generated-index" - } -} diff --git a/website/docs/cookbook/auto-tls.md b/website/docs/cookbook/auto-tls.md deleted file mode 100644 index 1b1cfd2c..00000000 --- a/website/docs/cookbook/auto-tls.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: Automatic TLS certificates from Let's Encrypt recipe ---- - -# Auto TLS - -This recipe demonstrates how to obtain TLS certificates for a domain automatically from -Let's Encrypt. `Echo#StartAutoTLS` accepts an address which should listen on port `443`. - -Browse to `https://`. If everything goes fine, you should see a welcome -message with TLS enabled on the website. - -:::tip - -- For added security you should specify host policy in auto TLS manager -- Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) -- To redirect HTTP traffic to HTTPS, you can use [redirect middleware](../middleware/redirect#https-redirect) - -::: - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/auto-tls/server.go -``` diff --git a/website/docs/cookbook/cors.md b/website/docs/cookbook/cors.md deleted file mode 100644 index bccf6603..00000000 --- a/website/docs/cookbook/cors.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -description: CORS recipe ---- - -# CORS - -## Server using a list of allowed origins - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/cors/origin-list/server.go -``` - -## Server using a custom function to allow origins - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/cors/origin-func/server.go -``` diff --git a/website/docs/cookbook/crud.md b/website/docs/cookbook/crud.md deleted file mode 100644 index d8a10240..00000000 --- a/website/docs/cookbook/crud.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -description: CRUD (Create, read, update and delete) recipe ---- - -# CRUD - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/crud/server.go -``` - -## Client - -### Create user - -#### Request - -```sh -curl -X POST \ - -H 'Content-Type: application/json' \ - -d '{"name":"Joe Smith"}' \ - localhost:1323/users -``` - -#### Response - -```js -{ - "id": 1, - "name": "Joe Smith" -} -``` - -### Get user - -#### Request - -```sh -curl localhost:1323/users/1 -``` - -#### Response - -```js -{ - "id": 1, - "name": "Joe Smith" -} -``` - -### Update user - -#### Request - -```sh -curl -X PUT \ - -H 'Content-Type: application/json' \ - -d '{"name":"Joe"}' \ - localhost:1323/users/1 -``` - -#### Response - -```js -{ - "id": 1, - "name": "Joe" -} -``` - -### Delete user - -#### Request - -```sh -curl -X DELETE localhost:1323/users/1 -``` - -#### Response - -`NoContent - 204` diff --git a/website/docs/cookbook/embed-resources.md b/website/docs/cookbook/embed-resources.md deleted file mode 100644 index 3af43873..00000000 --- a/website/docs/cookbook/embed-resources.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: Embed resources recipe ---- - -# Embed Resources - -## With go 1.16 embed feature - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/embed/server.go -``` - diff --git a/website/docs/cookbook/file-download.md b/website/docs/cookbook/file-download.md deleted file mode 100644 index a2649ad4..00000000 --- a/website/docs/cookbook/file-download.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -description: File download recipe ---- - -# File Download - -## Download file - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/file-download/server.go -``` - -### Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/file-download/index.html -``` - -## Download file as inline - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/file-download/inline/server.go -``` - -### Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/file-download/inline/index.html -``` - -## Download file as attachment - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/file-download/attachment/server.go -``` - -### Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/file-download/attachment/index.html -``` diff --git a/website/docs/cookbook/file-upload.md b/website/docs/cookbook/file-upload.md deleted file mode 100644 index 68201b95..00000000 --- a/website/docs/cookbook/file-upload.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -description: File upload recipe ---- - -# File Upload - - -## Upload single file with parameters - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/file-upload/single/server.go -``` - -### Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/file-upload/single/public/index.html -``` - -## Upload multiple files with parameters - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/file-upload/multiple/server.go -``` - -### Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/file-upload/multiple/public/index.html -``` diff --git a/website/docs/cookbook/graceful-shutdown.md b/website/docs/cookbook/graceful-shutdown.md deleted file mode 100644 index b54dbc42..00000000 --- a/website/docs/cookbook/graceful-shutdown.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -description: Graceful shutdown recipe ---- - -# Graceful Shutdown - -## Using [http.Server#Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/graceful-shutdown/server.go -``` - -:::note - -Requires go1.16+ - -::: diff --git a/website/docs/cookbook/hello-world.md b/website/docs/cookbook/hello-world.md deleted file mode 100644 index 67dbc11c..00000000 --- a/website/docs/cookbook/hello-world.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -description: Hello world recipe ---- - -# Hello World - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/hello-world/server.go -``` diff --git a/website/docs/cookbook/http2-server-push.md b/website/docs/cookbook/http2-server-push.md deleted file mode 100644 index cd000978..00000000 --- a/website/docs/cookbook/http2-server-push.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -description: HTTP/2 server push recipe ---- - -# HTTP/2 Server Push - -:::note - -Requires go1.8+ - -::: - -## Send web assets using HTTP/2 server push - -### [Generate a self-signed X.509 TLS certificate](http2#step-1-generate-a-self-signed-x-509-tls-certificate) - -### 1) Register a route to serve web assets - -```go -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) { - pusher, ok := c.Response().Writer.(http.Pusher) - if ok { - if err = pusher.Push("/app.css", nil); err != nil { - return - } - if err = pusher.Push("/app.js", nil); err != nil { - return - } - if err = pusher.Push("/echo.png", nil); err != nil { - return - } - } - return c.File("index.html") -}) -``` - -:::info - -If `http.Pusher` is supported, web assets are pushed; otherwise, client makes separate requests to get them. - -::: - -### 3) Start TLS server using cert.pem and key.pem - -```go -sc := echo.StartConfig{Address: ":1323"} -if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { - e.Logger.Error("failed to start server", "error", err) -} -``` -or use customized HTTP server with your own TLSConfig -```go -s := http.Server{ - Addr: ":8443", - Handler: e, // set Echo as handler - TLSConfig: &tls.Config{ - //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field - }, - //ReadTimeout: 30 * time.Second, // use custom timeouts -} -if err := s.ListenAndServeTLS("cert.pem", "key.pem"); err != http.ErrServerClosed { - log.Fatal(err) -} -``` - -### 4) Start the server and browse to https://localhost:1323 - -```sh -Protocol: HTTP/2.0 -Host: localhost:1323 -Remote Address: [::1]:60288 -Method: GET -Path: / -``` - -## Source Code - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/http2-server-push/index.html -``` - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/http2-server-push/server.go -``` diff --git a/website/docs/cookbook/http2.md b/website/docs/cookbook/http2.md deleted file mode 100644 index 4b5cf38d..00000000 --- a/website/docs/cookbook/http2.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -description: HTTP/2 server recipe ---- - -# HTTP/2 Server - -## 1) Generate a self-signed X.509 TLS certificate - -Run the following command to generate `cert.pem` and `key.pem` files: - -```sh -go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost -``` - -:::note - -For demo purpose, we are using a self-signed certificate. Ideally, you should obtain -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 { - req := c.Request() - format := ` - - Protocol: %s
- Host: %s
- Remote Address: %s
- Method: %s
- Path: %s
-
- ` - return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) -}) -``` - -## 3) Start TLS server using cert.pem and key.pem - -```go -sc := echo.StartConfig{Address: ":1323"} -if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { - e.Logger.Error("failed to start server", "error", err) -} -``` -or use customized HTTP server with your own TLSConfig -```go -s := http.Server{ - Addr: ":8443", - Handler: e, // set Echo as handler - TLSConfig: &tls.Config{ - //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field - }, - //ReadTimeout: 30 * time.Second, // use custom timeouts -} -if err := s.ListenAndServeTLS("cert.pem", "key.pem"); err != http.ErrServerClosed { - log.Fatal(err) -} -``` - -## 4) Start the server and browse to https://localhost:1323/request to see the following output - -```sh -Protocol: HTTP/2.0 -Host: localhost:1323 -Remote Address: [::1]:60288 -Method: GET -Path: / -``` - -## Source Code - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/http2/server.go -``` diff --git a/website/docs/cookbook/jsonp.md b/website/docs/cookbook/jsonp.md deleted file mode 100644 index 1eee4872..00000000 --- a/website/docs/cookbook/jsonp.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -description: JSONP recipe ---- - -# JSONP - -JSONP is a method that allows cross-domain server calls. You can read more about it at the JSON versus JSONP Tutorial. - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/jsonp/server.go -``` - -## Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/jsonp/public/index.html -``` diff --git a/website/docs/cookbook/jwt.md b/website/docs/cookbook/jwt.md deleted file mode 100644 index ad523f3d..00000000 --- a/website/docs/cookbook/jwt.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -description: JWT recipe ---- - -# JWT - -[JWT middleware](../middleware/jwt.md) configuration can be found [here](../middleware/jwt.md#configuration). - -This is cookbook for: -- JWT authentication using HS256 algorithm. -- JWT is retrieved from `Authorization` request header. - -## Server - -### Using custom claims - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/jwt/custom-claims/server.go -``` - -### Using a user-defined KeyFunc - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/jwt/user-defined-keyfunc/server.go -``` - -## Client - -### Login - -Login using username and password to retrieve a token. - -```sh -curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login -``` - -### Response - -```js -{ - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" -} -``` - -### Request - -Request a restricted resource using the token in `Authorization` request header. - -```sh -curl localhost:1323/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" -``` - -### Response - -```sh -Welcome Jon Snow! -``` diff --git a/website/docs/cookbook/load-balancing.md b/website/docs/cookbook/load-balancing.md deleted file mode 100644 index 5b024034..00000000 --- a/website/docs/cookbook/load-balancing.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -description: Load balancing recipe ---- - -# Load Balancing - -This recipe demonstrates how you can use Nginx as a reverse proxy server and load balance between multiple Echo servers. - -## Echo - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/load-balancing/upstream/server.go -``` - -### Start servers - -- `cd upstream` -- `go run server.go server1 :8081` -- `go run server.go server2 :8082` - -## Nginx - -### 1) Install Nginx - -https://www.nginx.com/resources/wiki/start/topics/tutorials/install - -### 2) Configure Nginx - -Create a file `/etc/nginx/sites-enabled/localhost` with the following content: - -``` reference -https://github.com/labstack/echox/blob/master/cookbook/load-balancing/nginx.conf -``` - -:::info - -Change listen, server_name, access_log per your need. - -::: - -### 3) Restart Nginx - -```sh -service nginx restart -``` - -Browse to https://localhost:8080, and you should see a webpage being served from either "server 1" or "server 2". - -```sh -Hello from upstream server server1 -``` diff --git a/website/docs/cookbook/middleware.md b/website/docs/cookbook/middleware.md deleted file mode 100644 index 3a32cce3..00000000 --- a/website/docs/cookbook/middleware.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Middleware recipe ---- - -# Middleware - -## Write a custom middleware - -- Middleware to collect request count, statuses and uptime. -- Middleware to write custom `Server` header to the response. - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/middleware/server.go -``` - -### Response - -#### Headers - -```sh -Content-Length:122 -Content-Type:application/json; charset=utf-8 -Date:Thu, 14 Apr 2016 20:31:46 GMT -Server:Echo/3.0 -``` - -#### Body - -```js -{ - "uptime": "2016-04-14T13:28:48.486548936-07:00", - "requestCount": 5, - "statuses": { - "200": 4, - "404": 1 - } -} -``` diff --git a/website/docs/cookbook/reverse-proxy.md b/website/docs/cookbook/reverse-proxy.md deleted file mode 100644 index 40dd6550..00000000 --- a/website/docs/cookbook/reverse-proxy.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -description: Reverse proxy recipe ---- - -# Reverse Proxy - -This recipe demonstrates how you can use Echo as a reverse proxy server and load balancer in front of your favorite applications like WordPress, Node.js, Java, Python, Ruby or even Go. For simplicity, I will use Go upstream servers with WebSocket. - -## 1) Identify upstream target URL(s) - -```go -url1, err := url.Parse("http://localhost:8081") -if err != nil { - e.Logger.Error("failed parse url", "error", err) -} -url2, err := url.Parse("http://localhost:8082") -if err != nil { - e.Logger.Error("failed parse url", "error", err) -} -targets := []*middleware.ProxyTarget{ - { - URL: url1, - }, - { - URL: url2, - }, -} -``` - -## 2) Setup proxy middleware with upstream targets - -In the following code snippet we are using round-robin load balancing technique. You may also use `middleware.NewRandomBalancer()`. - -```go -e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) -``` - -To setup proxy for a sub-route use `Echo#Group()`. - -```go -g := e.Group("/blog") -g.Use(middleware.Proxy(...)) -``` - -## 3) Start upstream servers - -- `cd upstream` -- `go run server.go server1 :8081` -- `go run server.go server2 :8082` - -## 4) Start the proxy server - -```sh -go run server.go -``` - -Browse to http://localhost:1323, and you should see a webpage with an HTTP request being served from "server 1" and a WebSocket request being served from "server 2." - -```sh -HTTP - -Hello from upstream server server1 - -WebSocket - -Hello from upstream server server2! -Hello from upstream server server2! -Hello from upstream server server2! -``` - -## Source Code - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/reverse-proxy/upstream/server.go -``` - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/reverse-proxy/server.go -``` diff --git a/website/docs/cookbook/sse.md b/website/docs/cookbook/sse.md deleted file mode 100644 index 8e4eb1d7..00000000 --- a/website/docs/cookbook/sse.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -description: SSE recipe ---- - -# Server-Sent-Events (SSE) - -[Server-sent events]( -https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) can be -used in different ways. This example here is per connection - per handler SSE. If your requirements need more complex -broadcasting logic see https://github.com/r3labs/sse library. - -## Using SSE - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/sse/simple/server.go -``` - -### Event structure and Marshal method - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/sse/simple/serversentevent.go -``` - -### HTML serving SSE - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/sse/simple/index.html -``` - -## Using 3rd party library [r3labs/sse](https://github.com/r3labs/sse) to broadcast events - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/sse/broadcast/server.go -``` - -### HTML serving SSE - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/sse/broadcast/index.html -``` \ No newline at end of file diff --git a/website/docs/cookbook/streaming-response.md b/website/docs/cookbook/streaming-response.md deleted file mode 100644 index aaa10494..00000000 --- a/website/docs/cookbook/streaming-response.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -description: Streaming response recipe ---- - -# Streaming Response - -- Send data as it is produced -- Streaming JSON response with chunked transfer encoding - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/streaming-response/server.go -``` - -## Client - -```sh -$ curl localhost:1323 -``` - -### Output - -```js -{"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255} -{"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351} -{"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329} -{"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974} -{"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416} -``` diff --git a/website/docs/cookbook/subdomain.md b/website/docs/cookbook/subdomain.md deleted file mode 100644 index 53fb7530..00000000 --- a/website/docs/cookbook/subdomain.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -description: Subdomain recipe ---- - -# Subdomain - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/subdomain/server.go -``` diff --git a/website/docs/cookbook/timeout.md b/website/docs/cookbook/timeout.md deleted file mode 100644 index 85feb91a..00000000 --- a/website/docs/cookbook/timeout.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -description: Timeout recipe ---- - -# Timeout - -## Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/timeout/server.go -``` diff --git a/website/docs/cookbook/websocket.md b/website/docs/cookbook/websocket.md deleted file mode 100644 index 05e9c1d3..00000000 --- a/website/docs/cookbook/websocket.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -description: WebSocket recipe ---- - -# WebSocket - -## Using net WebSocket - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/websocket/net/server.go -``` - -## Using gorilla WebSocket - -### Server - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/websocket/gorilla/server.go -``` - -## Client - -```html reference -https://github.com/labstack/echox/blob/master/cookbook/websocket/public/index.html -``` - -## Output - -```sh -Hello, Client! -Hello, Client! -Hello, Client! -Hello, Client! -Hello, Client! -``` - -```sh -Hello, Server! -Hello, Server! -Hello, Server! -Hello, Server! -Hello, Server! -``` diff --git a/website/docs/guide/_category_.json b/website/docs/guide/_category_.json deleted file mode 100644 index 359cc66c..00000000 --- a/website/docs/guide/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Guide", - "position": 2, - "link": { - "type": "generated-index", - "description": "5 minutes to learn the most important Echo concepts." - } -} diff --git a/website/docs/guide/binding.md b/website/docs/guide/binding.md deleted file mode 100644 index dac817ac..00000000 --- a/website/docs/guide/binding.md +++ /dev/null @@ -1,254 +0,0 @@ ---- -description: Binding request data -slug: /binding -sidebar_position: 3 ---- - -# Binding - -Parsing request data is a crucial part of a web application. In Echo this is done with a process called _binding_. This is done with information passed by the client in the following parts of an HTTP request: - -* URL Path parameter -* URL Query parameter -* Header -* Request body - -Echo provides different ways to perform binding, each described in the sections below. - -## 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 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`: - -```go -type User struct { - ID string `query:"id"` -} - -// in the handler for /users?id= -var user User -err := c.Bind(&user); if err != nil { - return c.String(http.StatusBadRequest, "bad request") -} -``` - -### Data Sources - -Echo supports the following tags specifying data sources: - -* `query` - query parameter -* `param` - path parameter (also called route) -* `header` - header parameter -* `json` - request body. Uses builtin Go [json](https://golang.org/pkg/encoding/json/) package for unmarshalling. -* `xml` - request body. Uses builtin Go [xml](https://golang.org/pkg/encoding/xml/) package for unmarshalling. -* `form` - form data. Values are taken from query and request body. Uses Go standard library form parsing. - -### Data Types - -When decoding the request body, the following data types are supported as specified by the `Content-Type` header: - -* `application/json` -* `application/xml` -* `application/x-www-form-urlencoded` - -When binding path parameter, query parameter, header, or form data, tags must be explicitly set on each struct field. However, JSON and XML binding is done on the struct field name if the tag is omitted. This is according to the behaviour of [Go's json package](https://pkg.go.dev/encoding/json#Unmarshal). - -For form data, Echo uses Go standard library form parsing. This parses form data from both the request URL and body if content type is not `MIMEMultipartForm`. See documentation for [non-MIMEMultipartForm](https://golang.org/pkg/net/http/#Request.ParseForm)and [MIMEMultipartForm](https://golang.org/pkg/net/http/#Request.ParseMultipartForm) - -### Multiple Sources - -It is possible to specify multiple sources on the same field. In this case request data is bound in this order: - -1. Path parameters -2. Query parameters (only for GET/DELETE methods) -3. Request body - -```go -type User struct { - ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` -} -``` - -Note that binding at each stage will overwrite data bound in a previous stage. This means if your JSON request contains the query param `name=query` and body `{"name": "body"}` then the result will be `User{Name: "body"}`. - -### Direct Source - -It is also possible to bind data directly from a specific source: - -Request body: -```go -err := echo.BindBody(c, &payload)) -``` - -Query parameters: -```go -err := echo.BindQueryParams(c, &payload) -``` - -Path parameters: -```go -err := echo.BindPathValues(c, &payload) -``` - -Header parameters: -```go -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. - -### Security - -To keep your application secure, avoid passing bound structs directly to other methods if these structs contain fields that should not be bindable. It is advisable to have a separate struct for binding and map it explicitly to your business struct. - -Consider what will happen if your bound struct has an Exported field `IsAdmin bool` and the request body contains `{IsAdmin: true, Name: "hacker"}`. - -### Example - -In this example we define a `User` struct type with field tags to bind from `json`, `form`, or `query` request data: - -```go -type UserDTO struct { - Name string `json:"name" form:"name" query:"name"` - Email string `json:"email" form:"email" query:"email"` -} - -type User struct { - Name string - Email string - IsAdmin bool -} -``` - -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 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) -}) -``` - -#### JSON Data - -```sh -curl -X POST http://localhost:1323/users \ - -H 'Content-Type: application/json' \ - -d '{"name":"Joe","email":"joe@labstack"}' -``` - -#### Form Data - -```sh -curl -X POST http://localhost:1323/users \ - -d 'name=Joe' \ - -d 'email=joe@labstack.com' -``` - -#### Query Parameters - -```sh -curl -X GET 'http://localhost:1323/users?name=Joe&email=joe@labstack.com' -``` - -## Fluent Binding - -Echo provides an interface to bind explicit data types from a specified source. It uses method chaining, also known as a [Fluent Interface](https://en.wikipedia.org/wiki/Fluent_interface). - -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.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 -A binder is usually completed by calling `BindError()` or `BindErrors()`. If any errors have occurred, `BindError()` returns the first error encountered, while`BindErrors()` returns all bind errors. Any errors stored in the binder are also reset. - -With `FailFast(true)` the binder can be configured to stop binding on the first error, or with `FailFast(false)` execute the entire binder call chain. Fail fast is enabled by default and should be disabled when using `BindErrors()`. - -### Example - -```go -// url = "/api/search?active=true&id=1&id=2&id=3&length=25" -var opts struct { - IDs []int64 - Active bool -} -length := int64(50) // default length is 50 - -// creates query params binder that stops binding at first error -err := echo.QueryParamsBinder(c). - Int64("length", &length). - Int64s("ids", &opts.IDs). - Bool("active", &opts.Active). - BindError() // returns first binding error -``` - -### Supported Data Types - -| Data Type | Notes | -| ------------------- | ----- | -| `bool` | | -| `float32` | | -| `float64` | | -| `int` | | -| `int8` | | -| `int16` | | -| `int32` | | -| `int64` | | -| `uint` | | -| `uint8/byte` | Does not support `bytes()`. Use `BindUnmarshaler`/`CustomFunc` to convert value from base64 etc to `[]byte{}`.| -| `uint16` | | -| `uint32` | | -| `uint64` | | -| `string` | | -| `time` | | -| `duration` | | -| `BindUnmarshaler()` | binds to a type implementing BindUnmarshaler interface | -| `TextUnmarshaler()` | binds to a type implementing encoding.TextUnmarshaler interface | -| `JsonUnmarshaler()` | binds to a type implementing json.Unmarshaler interface | -| `UnixTime()` | converts Unix time (integer) to `time.Time` | -| `UnixTimeMilli()` | converts Unix time with millisecond precision (integer) to `time.Time` | -| `UnixTimeNano()` | converts Unix time with nanosecond precision (integer) to `time.Time` | -| `CustomFunc()` | callback function for your custom conversion logic | - -Each supported type has the following methods: - -* `("param", &destination)` - if parameter value exists then binds it to given destination of that type i.e `Int64(...)`. -* `Must("param", &destination)` - parameter value is required to exist, binds it to given destination of that type i.e `MustInt64(...)`. -* `s("param", &destination)` - (for slices) if parameter values exists then binds it to given destination of that type i.e `Int64s(...)`. -* `Musts("param", &destination)` - (for slices) parameter value is required to exist, binds it to given destination of that type i.e `MustInt64s(...)`. - -For certain slice types `BindWithDelimiter("param", &dest, ",")` supports splitting parameter values before type conversion is done. For example binding an integer slice from the URL `/api/search?id=1,2,3&id=1` will result in `[]int64{1,2,3,1}`. - -## Custom Binding - -A custom binder can be registered using `Echo#Binder`. - -```go -type CustomBinder struct {} - -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/cookies.md b/website/docs/guide/cookies.md deleted file mode 100644 index ea7ea0af..00000000 --- a/website/docs/guide/cookies.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -description: Handling cookie -slug: /cookies -sidebar_position: 5 ---- - -# Cookies - -Cookie is a small piece of data sent from a website server and stored in the user's web -browser while browsing. Every time the user loads the website, the browser -sends the cookies back to the server to notify the server of user's latest activity. -Cookies were designed to be a reliable mechanism for websites to remember stateful -information (e.g. items added to the shopping cart in an online store) or to -record the user's browsing activity (such as clicking particular buttons, logging -in, or user previously visited pages of the website). Cookies can also store form content a user has previously entered, such as username, gender, age, address, etc. - -## Cookie Attributes - -Attribute | Optional -:--- | :--- -`Name` | No -`Value` | No -`Path` | Yes -`Domain` | Yes -`Expires` | Yes -`Secure` | Yes -`HttpOnly` | Yes - -Echo uses go standard `http.Cookie` object to add/retrieve cookies from the context received in the handler function. - -## Create a Cookie - -```go -func writeCookie(c *echo.Context) error { - cookie := new(http.Cookie) - cookie.Name = "username" - cookie.Value = "jon" - cookie.Expires = time.Now().Add(24 * time.Hour) - c.SetCookie(cookie) - return c.String(http.StatusOK, "write a cookie") -} -``` - -- Cookie is created using `new(http.Cookie)`. -- Attributes for the cookie are set assigning to the `http.Cookie` instance public attributes. -- Finally `c.SetCookie(cookie)` adds a `Set-Cookie` header in HTTP response. - -## Read a Cookie - -```go -func readCookie(c *echo.Context) error { - cookie, err := c.Cookie("username") - if err != nil { - return err - } - fmt.Println(cookie.Name) - fmt.Println(cookie.Value) - return c.String(http.StatusOK, "read a cookie") -} -``` - -- Cookie is read by name using `c.Cookie("username")` from the HTTP request. -- Cookie attributes are accessed using `Getter` function. - -## Read all the Cookies - -```go -func readAllCookies(c *echo.Context) error { - for _, cookie := range c.Cookies() { - fmt.Println(cookie.Name) - fmt.Println(cookie.Value) - } - return c.String(http.StatusOK, "read all the cookies") -} -``` \ No newline at end of file diff --git a/website/docs/guide/customization.md b/website/docs/guide/customization.md deleted file mode 100644 index 5410ac2f..00000000 --- a/website/docs/guide/customization.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -description: Customization -slug: /customization -sidebar_position: 2 ---- - -# Customization - -## Logging - -`Echo#Logger` - the default format for logging is JSON, which writes to `os.Stdout` by default. - - -### Custom Logger - -Logging is implemented using `slog.Logger` interface which allows you to register -a custom logger using `Echo#Logger`. - -```go -e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) -``` - - -## Validator - -`Echo#Validator` can be used to register a validator for performing data validation -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. - -[Learn more](./error-handling.md) - - -## 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. - - -## 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 deleted file mode 100644 index bac9a547..00000000 --- a/website/docs/guide/error-handling.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -description: Error handling -slug: /error-handling -sidebar_position: 6 ---- - -# Error Handling - -Echo advocates for centralized HTTP error handling by returning error from middleware -and handlers. Centralized error handler allows us to log errors to external services -from a unified location and send a customized HTTP response to the client. - -You can return a standard `error` or `echo.*HTTPError`. - -For example, when basic auth middleware finds invalid credentials it returns -401 - Unauthorized error, aborting the current HTTP request. - -```go -e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c *echo.Context) error { - // Extract the credentials from HTTP request header and perform a security - // check - - // For invalid credentials - return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials") - - // For valid credentials call next - // return next(c) - } -}) -``` - -You can also use `echo.NewHTTPError()` without a message, in that case status text is used -as an error message. For example, "Unauthorized". - -## Default HTTP Error Handler - -Echo provides a default HTTP error handler which sends error in a JSON format. - -```js -{ - "message": "error connecting to redis" -} -``` - -For a standard `error`, response is sent as `500 - Internal Server Error`; however, -if you are running in a debug mode, the original error message is sent. If error -is `*HTTPError`, response is sent with the provided status code and message. -If logging is on, the error message is also logged. - -## Custom HTTP Error Handler - -Custom HTTP error handler can be set via `e.HTTPErrorHandler` - -For most cases default error HTTP handler should be sufficient; however, a custom HTTP -error handler can come handy if you want to capture different type of errors and -take action accordingly e.g. send notification email or log error to a centralized -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 -type of errors and logs the error. The name of the error page should be like `.html` e.g. `500.html`. You can look into this project -https://github.com/AndiDittrich/HttpErrorPages for pre-built error pages. - -```go -func customHTTPErrorHandler(c *echo.Context, err error) { - 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 - } - } - - 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 - } - } - - var cErr error - if c.Request().Method == http.MethodHead { - cErr = c.NoContent(code) - } else { - errorPage := fmt.Sprintf("%d.html", code) - cErr = c.File(errorPage) - } - if cErr != nil { - c.Logger().Error("failed to send error page to client", "error", errors.Join(err, cErr)) - } -} - -e.HTTPErrorHandler = customHTTPErrorHandler -``` - -:::tip - -Instead of writing logs to the logger, you can also write them to an external service like Elasticsearch or Splunk. - -::: diff --git a/website/docs/guide/ip-address.md b/website/docs/guide/ip-address.md deleted file mode 100644 index 174c4de2..00000000 --- a/website/docs/guide/ip-address.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -description: IP address -slug: /ip-address -sidebar_position: 8 ---- - -# IP Address - -IP address plays a fundamental role in HTTP; it's used for access control, auditing, geo-based access analysis, and more. -Echo provides a handy method [`Context#RealIP()`](https://godoc.org/github.com/labstack/echo#Context) for that. - -However, it is not trivial to retrieve the _real_ IP address from requests, especially when you put L7 proxies before the application. -In such situations, _real_ IP needs to be relayed on the HTTP layer from proxies to your app, however, you must not trust HTTP headers unconditionally. -Otherwise, you might give someone a chance of deceiving you. **A security risk!** - -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. -This guides show you why and how. - -:::caution - -Note: if you don't set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice. - -::: - - -Let's start from two questions to know the right direction: - -1. Do you put any HTTP (L7) proxy in front of the application? - - It includes both cloud solutions (such as AWS ALB or GCP HTTP LB) and OSS ones (such as Nginx, Envoy or Istio ingress gateway). -2. If yes, what HTTP header do your proxies use to pass client IP to the application? - -## Case 1. With no proxy - -If you put no proxy (e.g.: directory facing to the internet), all you need to (and have to) see is IP address from network layer. -Any HTTP header is untrustable because the clients have full control what headers to be set. - -In this case, use `echo.ExtractIPDirect()`. - -```go -e.IPExtractor = echo.ExtractIPDirect() -``` - -## Case 2. With proxies using X-Forwarded-For header - -[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) is the popular header to relay clients' IP addresses. -At each hop on the proxies, they append the request IP address at the end of the header. - -Following example diagram illustrates this behavior. - -```text - ┌──────────┐ ┌──────────┐ ┌──────────┐ -───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ - │ (IP: b) │ │ (IP: c) │ │ │ - └──────────┘ └──────────┘ └──────────┘ - -Case 1. -XFF: "" "a" "a, b" - ~~~~~~ -Case 2. -XFF: "x" "x, a" "x, a, b" - ~~~~~~~~~ - ↑ What your app will see -``` - -In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructure". In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`. - -In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`. - -```go -e.IPExtractor = echo.ExtractIPFromXFFHeader() -``` - -By default, it trusts internal IP addresses (loopback, link-local unicast, private-use and unique local address from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and [RFC4193](https://tools.ietf.org/html/rfc4193)). To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s. - -E.g.: - -```go -e.IPExtractor = echo.ExtractIPFromXFFHeader( - echo.TrustLoopback(false), // e.g. ipv4 start with 127. - echo.TrustLinkLocal(false), // e.g. ipv4 start with 169.254 - echo.TrustPrivateNet(false), // e.g. ipv4 start with 10. or 192.168 - echo.TrustIPRange(lbIPRange), -) -``` - -- Ref: https://godoc.org/github.com/labstack/echo#TrustOption - -## Case 3. With proxies using X-Real-IP header - -`X-Real-IP` is another HTTP header to relay clients' IP addresses, but it carries only one address unlike XFF. - -If your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`. - -```go -e.IPExtractor = echo.ExtractIPFromRealIPHeader() -``` - -Again, it trusts internal IP addresses by default (loopback, link-local unicast, private-use and unique local address from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and [RFC4193](https://tools.ietf.org/html/rfc4193)). To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s. - -- Ref: https://godoc.org/github.com/labstack/echo#TrustOption - -> **Never forget** to configure the outermost proxy (i.e.; at the edge of your infrastructure) **not to pass through incoming headers**. -> Otherwise there is a chance of fraud, as it is what clients can control. - -## About default behavior - -In default behavior, Echo sees all of first XFF header, X-Real-IP header and IP from network layer. - -As you might already notice, after reading this article, this is not good. -Sole reason this is default is just backward compatibility. diff --git a/website/docs/guide/quick-start.md b/website/docs/guide/quick-start.md deleted file mode 100644 index e83a1f77..00000000 --- a/website/docs/guide/quick-start.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -description: Quick start -slug: /quick-start -sidebar_position: 1 ---- - -# Quick Start - -## Installation - -### Requirements - -It is recommended to use [Go](https://go.dev/doc/install) for dependency management and versioning. Echo supports at least 2 latest versions of Go. - -```sh -$ mkdir myapp && cd myapp -$ go mod init myapp -$ go get github.com/labstack/echo/v5 -``` - -## Hello, World! - -Create `server.go` - -```go -package main - -import ( - "net/http" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" -) - -func main() { - e := echo.New() - e.Use(middleware.RequestLogger()) - - e.GET("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") - }) - - if err := e.Start(":1323"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} - -``` - -Start server - -```sh -$ go run server.go -``` - -Browse to [http://localhost:1323](http://localhost:1323) and you should see -Hello, World! on the page. - -## Routing - -```go -e.POST("/users", saveUser) -e.GET("/users/:id", getUser) -e.PUT("/users/:id", updateUser) -e.DELETE("/users/:id", deleteUser) -``` - -## Path Parameters - -```go -// e.GET("/users/:id", getUser) -func getUser(c *echo.Context) error { - // User ID from path `users/:id` - id := c.Param("id") - return c.String(http.StatusOK, id) -} -``` - -Browse to [http://localhost:1323/users/joe](http://localhost:1323/users/joe) and you should see 'joe' on the page. - -## Query Parameters - -`/show?team=x-men&member=wolverine` - -```go -//e.GET("/show", show) -func show(c *echo.Context) error { - // Get team and member from the query string - team := c.QueryParam("team") - member := c.QueryParam("member") - return c.String(http.StatusOK, "team:" + team + ", member:" + member) -} -``` - -Browse to [http://localhost:1323/show?team=x-men&member=wolverine](http://localhost:1323/show?team=x-men&member=wolverine) and you should see 'team:x-men, member:wolverine' on the page. - -## Form application/x-www-form-urlencoded - -`POST` `/save` - -name | value -:--- | :--- -name | Joe Smith -email | joe@labstack.com - - -```go -// e.POST("/save", save) -func save(c *echo.Context) error { - // Get name and email - name := c.FormValue("name") - email := c.FormValue("email") - return c.String(http.StatusOK, "name:" + name + ", email:" + email) -} -``` - -Run the following command: - -```sh -$ curl -d "name=Joe Smith" -d "email=joe@labstack.com" http://localhost:1323/save -// => name:Joe Smith, email:joe@labstack.com -``` - -## Form multipart/form-data - -`POST` `/save` - -name | value -:--- | :--- -name | Joe Smith -avatar | avatar - -```go -func save(c *echo.Context) error { - // Get name - name := c.FormValue("name") - // Get avatar - avatar, err := c.FormFile("avatar") - if err != nil { - return err - } - - // Source - src, err := avatar.Open() - if err != nil { - return err - } - defer src.Close() - - // Destination - dst, err := os.Create(avatar.Filename) - if err != nil { - return err - } - defer dst.Close() - - // Copy - if _, err = io.Copy(dst, src); err != nil { - return err - } - - return c.HTML(http.StatusOK, "Thank you! " + name + "") -} -``` - -Run the following command. -```sh -$ curl -F "name=Joe Smith" -F "avatar=@/path/to/your/avatar.png" http://localhost:1323/save -// => Thank you! Joe Smith -``` - -For checking uploaded image, run the following command. - -```sh -cd -ls avatar.png -// => avatar.png -``` - -## Handling Request - -- Bind `json`, `xml`, `form` or `query` payload into Go struct based on `Content-Type` -request header. -- Render response as `json` or `xml` with status code. - -```go -type User struct { - Name string `json:"name" xml:"name" form:"name" query:"name"` - Email string `json:"email" xml:"email" form:"email" query:"email"` -} - -e.POST("/users", func(c *echo.Context) error { - u := new(User) - if err := c.Bind(u); err != nil { - return err - } - return c.JSON(http.StatusCreated, u) - // or - // return c.XML(http.StatusCreated, u) -}) -``` - -## Static Content - -Serve any file from static directory for path `/static/*`. - -```go -e.Static("/static", "static") -``` - -[Learn More](/docs/static-files) - -## [Template Rendering](/docs/templates) - -## Middleware - -```go -// Root level middleware -e.Use(middleware.RequestLogger()) -e.Use(middleware.Recover()) - -// Group level middleware -g := e.Group("/admin") -g.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (bool, error) { - if username == "joe" && password == "secret" { - return true, nil - } - return false, nil -})) - -// Route level middleware -track := func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c *echo.Context) error { - println("request to /users") - return next(c) - } -} -e.GET("/users", func(c *echo.Context) error { - return c.String(http.StatusOK, "/users") -}, track) -``` diff --git a/website/docs/guide/request.md b/website/docs/guide/request.md deleted file mode 100644 index efc970a4..00000000 --- a/website/docs/guide/request.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -description: Handling request -slug: /request -sidebar_position: 9 ---- - -# Request - -## Retrieve Data - -### Form Data - -Form data can be retrieved by name using `Context#FormValue(name string)`. - -```go - e.POST("/form", func(c *echo.Context) error { - name := c.FormValue("name") - return c.String(http.StatusOK, name) - }) -``` - -For types other than `string` you can use `echo.FormValue[t]` genetic type function. -```go -age, err := echo.FormValue[int](c, "age") -if err != nil { - return err -} -``` - -Test with: -```sh -curl -X POST http://localhost:1323 -d 'name=Joe&age=30' -``` - -To bind a custom data type, you can implement `Echo#BindUnmarshaler` interface. - -```go -type Timestamp time.Time - -func (t *Timestamp) UnmarshalParam(src string) error { - ts, err := time.Parse(time.RFC3339, src) - *t = Timestamp(ts) - return err -} -``` - -### Query Parameters - -Query parameters can be retrieved by name using `Context#QueryParam(name string)`. - -```go -// Handler -func(c *echo.Context) error { - name := c.QueryParam("name") - return c.String(http.StatusOK, name) -}) -``` - -For types other than `string` you can use `echo.QueryParam[t]` genetic type function. -```go -age, err := echo.QueryParam[int](c, "age") -if err != nil { - return err -} -``` - -```sh -curl -X GET "http://localhost:1323?name=Joe&age=30" -``` - -### Path Parameters - -Registered path parameters can be retrieved by name using `Context#Param(name string) string`. - -```go -e.GET("/users/:name", func(c *echo.Context) error { - name := c.Param("name") - return c.String(http.StatusOK, name) -}) -``` - -For types other than `string` you can use `echo.PathParam[t]` genetic type function. -```go -ID, err := echo.PathParam[int](c, "id") -if err != nil { - return err -} -``` - -```sh -curl http://localhost:1323/users/Joe -curl http://localhost:1323/users/123 -``` - -### Binding Data - -Also binding of request data to native Go structs and variables is supported. -See [Binding Data](./binding.md) - -## Validate Data - -Echo doesn't have built-in data validation capabilities, however, you can register -a custom validator using `Echo#Validator` and leverage third-party [libraries](https://github.com/avelino/awesome-go#validation). - -Example below uses https://github.com/go-playground/validator framework for validation: - -```go -package main - -import ( - "context" - "fmt" - "net/http" - - "github.com/go-playground/validator/v10" // installed by `go get github.com/go-playground/validator/v10` - "github.com/labstack/echo/v5" -) - - -type CustomValidator struct { - validator *validator.Validate -} - -func (cv *CustomValidator) Validate(i any) error { - if err := cv.validator.Struct(i); err != nil { - // Optionally, you could return the error to give each route more control over the status code - return echo.ErrBadRequest.Wrap(err) - } - return nil -} - -type User struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` -} - -func main() { - e := echo.New() - - e.Validator = &CustomValidator{validator: validator.New()} - - e.POST("/users", func(c *echo.Context) (err error) { - u := new(User) - if err = c.Bind(u); err != nil { - return err - } - if err = c.Validate(u); err != nil { - return err - } - return c.JSON(http.StatusOK, u) - }) - - if err := e.Start(":1323"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -```sh -curl -X POST http://localhost:1323/users \ - -H 'Content-Type: application/json' \ - -d '{"name":"Joe","email":"joe@invalid-domain"}' -{"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"} -``` \ No newline at end of file diff --git a/website/docs/guide/response.md b/website/docs/guide/response.md deleted file mode 100644 index 42e452a3..00000000 --- a/website/docs/guide/response.md +++ /dev/null @@ -1,351 +0,0 @@ ---- -description: Sending response -slug: /response -sidebar_position: 9 ---- - -# Response - -## Send String - -`Context#String(code int, s string)` can be used to send plain text response with status -code. - -*Example* - -```go -func(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") -} -``` - -## Send HTML (Reference to templates) - -`Context#HTML(code int, html string)` can be used to send simple HTML response with -status code. If you are looking to send dynamically generate HTML see [templates](./templates.md). - -*Example* - -```go -func(c *echo.Context) error { - return c.HTML(http.StatusOK, "Hello, World!") -} -``` - -### Send HTML Blob - -`Context#HTMLBlob(code int, b []byte)` can be used to send HTML blob with status -code. You may find it handy using with a template engine which outputs `[]byte`. - -## Render Template - -[Learn more](./templates.md) - -## Send JSON - -`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* - -```go -// User -type User struct { - Name string `json:"name" xml:"name"` - Email string `json:"email" xml:"email"` -} - -// Handler -func(c *echo.Context) error { - u := &User{ - Name: "Jon", - Email: "jon@labstack.com", - } - return c.JSON(http.StatusOK, u) -} -``` - -### Stream JSON - -`Context#JSON()` internally uses `json.Marshal` which may not be efficient to large JSON, -in that case you can directly stream JSON. - -*Example* - -```go -func(c *echo.Context) error { - u := &User{ - Name: "Jon", - Email: "jon@labstack.com", - } - c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) - c.Response().WriteHeader(http.StatusOK) - return json.NewEncoder(c.Response()).Encode(u) -} -``` - -### JSON Pretty - -`Context#JSONPretty(code int, i any, indent string)` 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 { - u := &User{ - Name: "Jon", - Email: "joe@labstack.com", - } - return c.JSONPretty(http.StatusOK, u, " ") -} -``` - -```js -{ - "email": "joe@labstack.com", - "name": "Jon" -} -``` - -### JSON Blob - -`Context#JSONBlob(code int, b []byte)` can be used to send pre-encoded JSON blob directly -from external source, for example, database. - -*Example* - -```go -func(c *echo.Context) error { - encodedJSON := []byte{} // Encoded JSON from external source - return c.JSONBlob(http.StatusOK, encodedJSON) -} -``` - -## Send JSONP - -`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. - -[*Example*](../cookbook/jsonp.md) - -## Send XML - -`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 { - u := &User{ - Name: "Jon", - Email: "jon@labstack.com", - } - return c.XML(http.StatusOK, u) -} -``` - -### Stream XML - -`Context#XML` internally uses `xml.Marshal` which may not be efficient to large XML, -in that case you can directly stream XML. - -*Example* - -```go -func(c *echo.Context) error { - u := &User{ - Name: "Jon", - Email: "jon@labstack.com", - } - c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) - c.Response().WriteHeader(http.StatusOK) - return xml.NewEncoder(c.Response()).Encode(u) -} -``` - -### XML Pretty - -`Context#XMLPretty(code int, i any, indent string)` 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 { - u := &User{ - Name: "Jon", - Email: "joe@labstack.com", - } - return c.XMLPretty(http.StatusOK, u, " ") -} -``` - -```xml - - - Jon - joe@labstack.com - -``` -:::tip - -You can also use `Context#XML()` to output a pretty printed XML (indented with spaces) by appending `pretty` in the request URL query string. - -::: - -*Example* - -```sh -curl http://localhost:1323/users/1?pretty -``` - -### XML Blob - -`Context#XMLBlob(code int, b []byte)` can be used to send pre-encoded XML blob directly -from external source, for example, database. - -*Example* - -```go -func(c *echo.Context) error { - encodedXML := []byte{} // Encoded XML from external source - return c.XMLBlob(http.StatusOK, encodedXML) -} -``` - -## Send File - -`Context#File(file string)` can be used to send the content of file as response. -It automatically sets the correct content type and handles caching gracefully. - -*Example* - -```go -func(c *echo.Context) error { - return c.File("") -} -``` - -## Send Attachment - -`Context#Attachment(file, name string)` is similar to `File()` except that it is -used to send file as `Content-Disposition: attachment` with provided name. - -*Example* - -```go -func(c *echo.Context) error { - return c.Attachment("", "") -} -``` - -## Send Inline - -`Context#Inline(file, name string)` is similar to `File()` except that it is -used to send file as `Content-Disposition: inline` with provided name. - -*Example* - -```go -func(c *echo.Context) error { - return c.Inline("") -} -``` - -## Send Blob - -`Context#Blob(code int, contentType string, b []byte)` can be used to send an arbitrary -data response with provided content type and status code. - -*Example* - -```go -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) -} -``` - -## Send Stream - -`Context#Stream(code int, contentType string, r io.Reader)` can be used to send an -arbitrary data stream response with provided content type, `io.Reader` and status -code. - -*Example* - -```go -func(c *echo.Context) error { - f, err := os.Open("") - if err != nil { - return err - } - defer f.Close() - return c.Stream(http.StatusOK, "image/png", f) -} -``` - -## Send No Content - -`Context#NoContent(code int)` can be used to send empty body with status code. - -*Example* - -```go -func(c *echo.Context) error { - return c.NoContent(http.StatusOK) -} -``` - -## Redirect Request - -`Context#Redirect(code int, url string)` can be used to redirect the request to -a provided URL with status code. - -*Example* - -```go -func(c *echo.Context) error { - return c.Redirect(http.StatusMovedPermanently, "") -} -``` - -## Hooks - -### Before Response - -`Response#Before(func())` can be used to register a function which is called just before the response is written. - -### After Response - -`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 -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 - -It is possible to register multiple Before and After functions. - -::: diff --git a/website/docs/guide/routing.md b/website/docs/guide/routing.md deleted file mode 100644 index d4492d22..00000000 --- a/website/docs/guide/routing.md +++ /dev/null @@ -1,522 +0,0 @@ ---- -description: Routing requests -slug: /routing -sidebar_position: 10 ---- - -# Routing - -Echo's router is based on [radix tree](http://en.wikipedia.org/wiki/Radix_tree), making -route lookup really fast. It leverages [sync pool](https://golang.org/pkg/sync/#Pool) -to reuse memory and achieve zero dynamic memory allocation with no GC overhead. - -Routes can be registered by specifying HTTP method, path and a matching handler. -For example, code below registers a route for method `GET`, path `/hello` and a -handler which sends `Hello, World!` HTTP response. - -```go -// Handler -func hello(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") -} - -// Route -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) 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 | -| `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 - -- **`path`** (string): The route pattern (e.g., `/users/:id`, `/api/*`) -- **`handler`** (HandlerFunc): Function that handles the request -- **`middleware`** (...Middleware): Optional middleware functions - -### Handler Function Signature - -Echo defines handler functions as `func(c *echo.Context) error` where: - -- **`*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 - -```go -// GET request -e.GET("/users/:id", getUser) - -// POST request with JSON body -e.POST("/users", createUser) - -// PUT request for updates -e.PUT("/users/:id", updateUser) - -// DELETE request -e.DELETE("/users/:id", deleteUser) - -// PATCH request for partial updates -e.PATCH("/users/:id", patchUser) - -// HEAD request (same as GET but without body) -e.HEAD("/users/:id", getUser) - -// OPTIONS request for CORS -e.OPTIONS("/users", handleOptions) -``` - -## Route Registration Methods - -You can use `Echo.Any(path string, h Handler)` to register a handler for all HTTP methods. -If you want to register it for some methods use `Echo.Match(methods []string, path string, h Handler)`. - -### Echo.Any() - -Registers a handler for all HTTP methods on a given path: - -```go -e.Any("/api/*", func(c *echo.Context) error { - return c.String(http.StatusOK, "Handles all methods") -}) -``` - -### Echo.Match() - -Registers a handler for specific HTTP methods: - -```go -// Handle both GET and POST -e.Match([]string{"GET", "POST"}, "/users", userHandler) - -// Handle multiple methods -e.Match([]string{"PUT", "PATCH"}, "/users/:id", updateHandler) -``` - -## Route Parameters - -Echo supports various types of route parameters for flexible URL patterns. - -### Path Parameters - -Path parameters are defined using `:paramName` syntax and can be accessed in handlers using `c.Param("paramName")`. - -```go -// Single parameter -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 { - 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 { - id := c.QueryParam("id") // Optional - return c.String(http.StatusOK, "User ID: " + id) -}) -``` - -### Query Parameters - -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") - // 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)) -}) -``` - -**Example URLs:** - -- `/search?q=golang&limit=10` -- `/users/123?include=posts&limit=5` - -### Form Parameters - -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") - 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)) -}) -``` - -## Match-any / wildcard - -Matches zero or more characters in the path. For example, pattern `/users/*` will -match: - -- `/users/` -- `/users/1` -- `/users/1/files/1` -- `/users/anything...` - -:::caution - -There can be only one effective match-any parameter in route. When route is added with multiple match-any -`/v1/*/images/*`. The router matches always the first `*` till the end of request URL i.e. it works as `/v1/*`. - -::: - -## Path Matching Order - -- Static -- Param -- Match any - -### Example - -```go -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 { - return c.String(http.StatusOK, "/users/new") -}) - -e.GET("/users/1/files/*", func(c *echo.Context) error { - return c.String(http.StatusOK, "/users/1/files/*") -}) -``` - -Above routes would resolve in the following order: - -- `/users/new` -- `/users/:id` -- `/users/1/files/*` - -:::tip - -Routes can be written in any order. - -::: - -## Group - -`Echo#Group(prefix string, m ...Middleware) *Group` - -Routes with common prefix can be grouped to define a new sub-router with optional -middleware. In addition to specified middleware group also inherits parent middleware. -To add middleware later in the group you can use `Group.Use(m ...Middleware)`. -Groups can also be nested. - -:::note - -Group level middlewares are tied to the route and will work only if the group has at least one route. - -::: - -### Basic Group Usage - -```go -// Create a group with prefix -api := e.Group("/api") - -// Add routes to the group -api.GET("/users", getUsers) -api.POST("/users", createUser) -api.GET("/users/:id", getUser) -``` - -### Group with Middleware - -```go -// Admin group with authentication -admin := e.Group("/admin") -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 -admin.GET("/dashboard", dashboard) -admin.GET("/users", adminUsers) -admin.POST("/users", createUser) -``` - -### Advanced Group Examples - -```go -// API v1 group with logging and CORS -apiV1 := e.Group("/api/v1") -apiV1.Use(middleware.RequestLogger()) -apiV1.Use(middleware.CORS("https://api.example.com")) - -apiV1.GET("/users", getUsers) -apiV1.POST("/users", createUser) - -// API v2 group with different middleware -apiV2 := e.Group("/api/v2") -apiV2.Use(middleware.RequestLogger()) -apiV2.Use(middleware.CORS("https://api.example.com")) -apiV2.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20))) - -apiV2.GET("/users", getUsersV2) -apiV2.POST("/users", createUserV2) - -// Nested groups -admin := e.Group("/admin") -admin.Use(middleware.BasicAuth(...)) - -// Admin users sub-group -adminUsers := admin.Group("/users") -adminUsers.Use(middleware.RoleBasedAuth("admin")) -adminUsers.GET("/", listUsers) -adminUsers.POST("/", createUser) -``` - -### Group Middleware Management - -```go -// Create group without initial middleware -api := e.Group("/api") - -// Add middleware later -api.Use(middleware.RequestLogger()) -api.Use(middleware.Recover()) - -// Add routes -api.GET("/health", healthCheck) -api.GET("/users", getUsers) - -// Add more middleware to specific routes -api.GET("/sensitive", sensitiveHandler, middleware.AuthRequired()) -``` - -### Practical Example: RESTful API Structure - -```go -func setupRoutes(e *echo.Echo) { - // Public routes - e.GET("/", homeHandler) - e.GET("/login", loginPage) - e.POST("/login", loginHandler) - - // API group with common middleware - api := e.Group("/api") - api.Use(middleware.RequestLogger()) - api.Use(middleware.Recover()) - api.Use(middleware.CORS("https://api.example.com")) - - // Public API routes - api.GET("/health", healthCheck) - api.POST("/register", registerUser) - - // Protected API routes - protected := api.Group("") - protected.Use(middleware.JWT([]byte("secret"))) - - protected.GET("/users", getUsers) - protected.POST("/users", createUser) - protected.GET("/users/:id", getUser) - protected.PUT("/users/:id", updateUser) - protected.DELETE("/users/:id", deleteUser) - - // Admin API routes - admin := protected.Group("/admin") - admin.Use(middleware.RoleBasedAuth("admin")) - - admin.GET("/stats", getStats) - admin.POST("/users/:id/ban", banUser) -} -``` - -### Group Benefits - -1. **Middleware Inheritance**: Groups inherit parent middleware -2. **URL Organization**: Logical grouping of related routes -3. **Middleware Reuse**: Apply common middleware to multiple routes -4. **Nested Structure**: Create hierarchical route structures -5. **Easy Maintenance**: Modify group middleware affects all routes - -## Route Naming - -Each of the registration methods returns a `Route` object, which can be used to name a route after the registration. Route names are useful for generating URIs from templates, avoiding hardcoded URLs, and when you have multiple routes with the same handler. - -### Basic Route Naming - -```go -_, 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 - -Echo provides two methods for generating URIs: `Echo.URI()` and `Echo.Reverse()`. - -### Echo.URI() - Handler-based URI Generation - -`Echo.URI(handler HandlerFunc, params ...any)` generates URIs based on handler functions: - -```go -// Define handlers -func getUser(c *echo.Context) error { - return c.String(http.StatusOK, "User") -} - -func getUserPost(c *echo.Context) error { - return c.String(http.StatusOK, "User Post") -} - -// Register routes -e.GET("/users/:id", getUser) -e.GET("/users/:id/posts/:postId", getUserPost) - -// Generate URIs -userURI := e.URI(getUser, 123) // "/users/123" -postURI := e.URI(getUserPost, 123, 456) // "/users/123/posts/456" -``` - -### Router.Reverse() - Name-based URI Generation - -`echo.Router.Reverse(name string, params ...any)` generates URIs based on route names: - -```go -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) - -userURI, err := e.Router().Routes().Reverse("user_details", 123) // "/users/123" -``` - -### Benefits of Route Naming - -1. **URL Centralization**: Change URLs in one place -2. **Template Integration**: Generate links in HTML templates -3. **Refactoring Safety**: Rename handlers without breaking links -4. **API Documentation**: Generate API documentation with correct URLs - -## List Routes - -`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 findUser(c *echo.Context) error { -} - -func updateUser(c *echo.Context) error { -} - -func deleteUser(c *echo.Context) error { -} - -// Routes -e.POST("/users", createUser) -e.GET("/users", findUser) -e.PUT("/users", updateUser) -e.DELETE("/users", deleteUser) -``` - -Using the following code you can output all the routes to a JSON file: - -```go -data, err := json.MarshalIndent(e.Router().Routes(), "", " ") -if err != nil { - return err -} -os.WriteFile("routes.json", data, 0644) -``` - -`routes.json` - -```js -[ - { - "Method": "POST", - "Path": "/users", - "Name": "main.createUser" - }, - { - "Method": "GET", - "Path": "/users", - "Name": "main.findUser" - }, - { - "Method": "PUT", - "Path": "/users", - "Name": "main.updateUser" - }, - { - "Method": "DELETE", - "Path": "/users", - "Name": "main.deleteUser" - } -] -``` diff --git a/website/docs/guide/start-server.md b/website/docs/guide/start-server.md deleted file mode 100644 index 0832eac2..00000000 --- a/website/docs/guide/start-server.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -description: Starting server -slug: /start-server -sidebar_position: 7 ---- - -# Start Server - -Echo provides following `Echo.Start(address string)` convenience method to start the server. Which uses the default configuration for graceful shutdown. - - -## 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(":1323"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -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() - // add middleware and routes - // ... - s := http.Server{ - Addr: ":8080", - Handler: e, - //ReadTimeout: 30 * time.Second, // customize http.Server timeouts - } - if err := s.ListenAndServe(); err != http.ErrServerClosed { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## HTTPS Server - -`Echo.StartTLS` is convenience method that starts HTTPS server with Echo serving requests on given address and uses -`server.crt` and `server.key` as TLS certificate pair. -```go -func main() { - 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) - } -} -``` - -Following is equivalent to `Echo.StartTLS` previous example -```go -func main() { - e := echo.New() - // add middleware and routes - // ... - s := http.Server{ - Addr: ":8443", - Handler: e, // set Echo as handler - TLSConfig: &tls.Config{ - //MinVersion: 1, // customize TLS configuration - }, - //ReadTimeout: 30 * time.Second, // use custom timeouts - } - if err := s.ListenAndServeTLS("server.crt", "server.key"); err != http.ErrServerClosed { - log.Fatal(err) - } -} -``` - -## Auto TLS Server with Let’s Encrypt - -See [Auto TLS Recipe](../cookbook/auto-tls.md#server) - -## HTTP/2 Cleartext Server (HTTP2 over HTTP) - -`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 - // ... - - 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) - } -} -``` - -Following is equivalent to `Echo.StartH2CServer` previous example -```go -func main() { - e := echo.New() - // add middleware and routes - // ... - h2s := &http2.Server{ - MaxConcurrentStreams: 250, - MaxReadFrameSize: 1048576, - IdleTimeout: 10 * time.Second, - } - s := http.Server{ - Addr: ":8080", - Handler: h2c.NewHandler(e, h2s), - } - if err := s.ListenAndServe(); err != http.ErrServerClosed { - log.Fatal(err) - } -} -``` \ No newline at end of file diff --git a/website/docs/guide/static-files.md b/website/docs/guide/static-files.md deleted file mode 100644 index 2c1f4ff3..00000000 --- a/website/docs/guide/static-files.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -description: Serving static files -slug: /static-files -sidebar_position: 11 ---- - -# Serving Static Files - -Images, JavaScript, CSS, PDF, Fonts and so on... - -## Default file system - -Echo uses `os.DirFS(".")` as default a file system which is set to current working directory. -To change the default file system, use `Echo#Filesystem` field. - -```go -e := echo.New() -e.Filesystem = os.DirFS("assets") -``` - -## Using Static Middleware - -[See ](/middleware/static.md) - -## Using Echo#Static() - -`Echo#Static(prefix, root string)` registers a new route with path prefix to serve -static files from the provided root directory. - -*Usage 1* - -```go -e := echo.New() -e.Static("/static", "assets") -``` - -Example above will serve any file from the assets directory for path `/static/*`. For example, -a request to `/static/js/main.js` will fetch and serve `assets/js/main.js` file. - -*Usage 2* - -```go -e := echo.New() -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 -file. - -*Usage 1* - -Serving an index page from `public/index.html` - -```go -e.File("/", "public/index.html") -``` - -*Usage 2* - -:::important - -Leading `/` in the file path is will not work with most of the fs.FS implementations. - -::: - -Serving a favicon from `/app/assets/favicon.ico` - -```go -e := echo.New() -e.Filesystem = os.DirFS("/") -e.File("/favicon.ico", "app/assets/favicon.ico") // <--- file path must not have a leading slash -``` diff --git a/website/docs/guide/templates.md b/website/docs/guide/templates.md deleted file mode 100644 index 859a14f1..00000000 --- a/website/docs/guide/templates.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -description: Using templates -slug: /templates -sidebar_position: 12 ---- - -# Templates - -## Rendering - -`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. Use default template renderer -```go -e.Renderer = &echo.TemplateRenderer{ - Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), -} -``` - -or Implement `echo.Renderer` interface - -```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 - - `public/views/hello.html` - - ```html - {{define "hello"}}Hello, {{.}}!{{end}} - ``` - - ```go - t := &Template{ - templates: template.Must(template.ParseGlob("public/views/*.html")), - } - ``` - -3. Register templates - - ```go - e := echo.New() - e.Renderer = t - e.GET("/hello", Hello) - ``` - -4. Render a template inside your handler - - ```go - 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]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" }} - {{ end }} -

- - -``` - -`server.go` - -```go -package main - -import ( - "html/template" - "io" - "net/http" - - "github.com/labstack/echo/v5" -) - -// TemplateRenderer is a custom html/template renderer for Echo framework -type TemplateRenderer struct { - templates *template.Template -} - -// Render renders a template document -func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error { - - // Add global methods if data is a map - if viewContext, isMap := data.(map[string]any); isMap { - viewContext["reverse"] = c.RouteInfo().Reverse - } - - return t.templates.ExecuteTemplate(w, name, data) -} - -func main() { - e := echo.New() - e.Renderer = &TemplateRenderer{ - templates: template.Must(template.ParseGlob("main/*.html")), - } - - e.GET("/something/:name", func(c *echo.Context) error { - return c.Render(http.StatusOK, "template.html", map[string]any{ - "name": "Dolly!", - }) - }) - - if err := e.Start(":1323"); err != nil { - e.Logger.Error("shutting down the server", "error", err) - } -} -``` diff --git a/website/docs/guide/testing.md b/website/docs/guide/testing.md deleted file mode 100644 index 8e864e2b..00000000 --- a/website/docs/guide/testing.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -description: Testing handler and middleware -slug: /testing -sidebar_position: 13 ---- - -# Testing - -## Testing Handler - -`GET` `/users/:id` - -Handler below retrieves user by id from the database. If user is not found it returns -`404` error with a message. - -### CreateUser - -`POST` `/users` - -- Accepts JSON payload -- On success `201 - Created` -- On error `500 - Internal Server Error` - -### GetUser - -`GET` `/users/:email` - -- On success `200 - OK` -- On error `404 - Not Found` if user is not found otherwise `500 - Internal Server Error` - -`handler.go` - -```go -package handler - -import ( - "net/http" - - "github.com/labstack/echo/v5" -) - -type ( - User struct { - Name string `json:"name" form:"name"` - Email string `json:"email" form:"email"` - } - handler struct { - db map[string]*User - } -) - -func (h *handler) createUser(c *echo.Context) error { - u := new(User) - if err := c.Bind(u); err != nil { - return err - } - return c.JSON(http.StatusCreated, u) -} - -func (h *handler) getUser(c *echo.Context) error { - email := c.Param("email") - user := h.db[email] - if user == nil { - return echo.NewHTTPError(http.StatusNotFound, "user not found") - } - return c.JSON(http.StatusOK, user) -} -``` - -`handler_test.go` - -```go -package handler - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/echotest" - "github.com/stretchr/testify/assert" -) - -var ( - mockDB = map[string]*User{ - "jon@labstack.com": &User{"Jon Snow", "jon@labstack.com"}, - } - userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}` -) - -func TestCreateUser(t *testing.T) { - // Setup - e := echo.New() - req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - h := &controller{mockDB} - - // Assertions - if assert.NoError(t, h.createUser(c)) { - assert.Equal(t, http.StatusCreated, rec.Code) - assert.Equal(t, userJSON, rec.Body.String()) - } -} - -// 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.SetPathValues(echo.PathValues{ - {Name: "email", Value: "jon@labstack.com"}, - }) - h := &controller{mockDB} - - // Assertions - if assert.NoError(t, h.getUser(c)) { - assert.Equal(t, http.StatusOK, rec.Code) - 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 - -```go -// import "net/url" -f := make(url.Values) -f.Set("name", "Jon Snow") -f.Set("email", "jon@labstack.com") -req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) -req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) -``` - -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.SetPathValues(echo.PathValues{ - {Name: "id", Value: "1"}, - {Name: "email", Value: "jon@labstack.com"}, -}) -``` - -### Setting Query Params - -```go -// import "net/url" -q := make(url.Values) -q.Set("email", "jon@labstack.com") -req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) -``` - -## Testing Middleware - -```go -func 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/introduction.md b/website/docs/introduction.md deleted file mode 100644 index ec80d430..00000000 --- a/website/docs/introduction.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -sidebar_position: 1 -slug: / ---- - -# Introduction - -## ![LabStack](../static/img/labstack-icon.png) Echo Project - -The Echo project is a powerful and versatile web framework for building scalable and high-performance web applications in the Go programming language. It follows the principles of simplicity, flexibility, and performance to provide developers with an efficient toolkit for building robust web applications. - -## Key Features - -- **Fast and Lightweight**: Echo is designed for speed and efficiency, ensuring minimal overhead and high performance for handling HTTP requests and responses. -- **Routing**: The framework offers a flexible and intuitive routing system that allows developers to define routes with parameters, query strings, and custom handlers. -- **Middleware Support**: Echo provides extensive middleware support, enabling developers to easily implement cross-cutting concerns such as logging, authentication, error handling, and more. -- **Context-based Request Handling**: With its context-based request handling, Echo offers easy access to request-specific data and parameters, simplifying the development of web applications. -- **Powerful Template Rendering**: Echo includes a powerful template rendering engine that supports various template languages, allowing developers to generate dynamic HTML content effortlessly. -- **Validation and Binding**: The framework provides robust validation and data binding capabilities, making it straightforward to validate incoming request data and bind it to Go structs. -- **Extensibility**: Echo is highly extensible, with support for custom middleware, template engines, and other components, enabling developers to tailor the framework to their specific needs. -- **Community and Ecosystem**: The Echo project benefits from a vibrant and active community that contributes libraries, plugins, and extensions, fostering an ecosystem of reusable components. - -## Resources and Documentation - -To learn more about the Echo project, you can refer to the following resources: - -- Official Website: [https://echo.labstack.com](https://echo.labstack.com) -- GitHub Repository: [https://github.com/labstack/echo](https://github.com/labstack/echo) -- Documentation: [https://echo.labstack.com/docs](https://echo.labstack.com/guide) -- Community Forum: [https://github.com/labstack/echo/discussions](https://github.com/labstack/echo/discussions) - -The Echo project offers an array of features that empower developers to build robust web applications. Its fast and lightweight nature ensures optimal performance, while the flexible routing system and middleware support streamline development processes. Developers can leverage the context-based request handling, powerful template rendering, and validation capabilities to create dynamic and secure web applications. Additionally, the extensibility of Echo allows developers to customize and enhance the framework to suit their specific needs. - -Join the vibrant community of Echo developers, explore the vast ecosystem of plugins and extensions, and unleash the power of Echo for your web development needs. diff --git a/website/docs/middleware/_category_.json b/website/docs/middleware/_category_.json deleted file mode 100644 index e2d9492f..00000000 --- a/website/docs/middleware/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Middleware", - "position": 3, - "link": { - "type": "generated-index" - } -} diff --git a/website/docs/middleware/basic-auth.md b/website/docs/middleware/basic-auth.md deleted file mode 100644 index 7ef34b01..00000000 --- a/website/docs/middleware/basic-auth.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -description: Basic auth middleware ---- - -# Basic Auth - -Basic auth middleware provides an HTTP basic authentication. - -- For valid credentials it calls the next handler. -- For missing or invalid credentials, it sends "401 - Unauthorized" response. - -## Usage - -```go -e.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (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 { - return true, nil - } - return false, nil -})) -``` - -## Custom Configuration - -### Usage - -```go -e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) -``` - -## Configuration - -```go -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 -} -``` - -### Default Configuration - -```go -DefaultBasicAuthConfig = BasicAuthConfig{ - Skipper: DefaultSkipper, -} -``` diff --git a/website/docs/middleware/body-dump.md b/website/docs/middleware/body-dump.md deleted file mode 100644 index f38e96d0..00000000 --- a/website/docs/middleware/body-dump.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: Body dump middleware ---- - -# Body Dump - -Body dump middleware captures the request and response payload and calls the registered handler. Generally used for debugging/logging purpose. Avoid using it if your request/response payload is huge e.g. file upload/download, but if you still need to, add an exception for your endpoints in the skipper function. - -## Usage - -```go -e := echo.New() -e.Use(middleware.BodyDump(func(c *echo.Context, reqBody []byte, resBody []byte, err error) { - // logic to handle request body and response body -})) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) -``` - -## Configuration - -```go -type BodyDumpConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Handler receives 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 -} -``` - -### Default Configuration* - -```go -DefaultBodyDumpConfig = BodyDumpConfig{ - Skipper: DefaultSkipper, -} -``` diff --git a/website/docs/middleware/body-limit.md b/website/docs/middleware/body-limit.md deleted file mode 100644 index 7ad474f5..00000000 --- a/website/docs/middleware/body-limit.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Body limit middleware ---- - -# Body Limit - -Body limit middleware sets the maximum allowed size for a request body, if the -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 is specified as bytes - -## Usage - -```go -e := echo.New() -e.Use(middleware.BodyLimit(2_097_152)) // 2MB -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) -``` - -## Configuration - -```go -type BodyLimitConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // LimitBytes is maximum allowed size in bytes for a request body - LimitBytes int64 -} -``` - -### Default Configuration - -```go -DefaultBodyLimitConfig = BodyLimitConfig{ - Skipper: DefaultSkipper, -} -``` diff --git a/website/docs/middleware/casbin-auth.md b/website/docs/middleware/casbin-auth.md deleted file mode 100644 index 4dd24ef9..00000000 --- a/website/docs/middleware/casbin-auth.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -description: Casbin auth middleware ---- - -# Casbin Auth - - -[Casbin](https://github.com/casbin/casbin) is a powerful and efficient open-source access control library for Go. It provides support for enforcing authorization based on various models. So far, the access control models supported by Casbin are: - -- ACL (Access Control List) -- ACL with superuser -- ACL without users: especially useful for systems that don't have authentication or user log-ins. -- ACL without resources: some scenarios may target for a type of resources instead of an individual resource by using permissions like write-article, read-log. It doesn't control the access to a specific article or log. -- RBAC (Role-Based Access Control) -- RBAC with resource roles: both users and resources can have roles (or groups) at the same time. -- RBAC with domains/tenants: users can have different role sets for different domains/tenants. -- ABAC (Attribute-Based Access Control) -- RESTful -- Deny-override: both allow and deny authorizations are supported, deny overrides the allow. - - -See [API Overview](https://casbin.org/docs/api-overview). -For more information, see: [Casbin Documentation](https://casbin.org/docs/). - -## Dependencies - -```bash -go get -u github.com/casbin/casbin/v3 -``` - -```go -import ( - "github.com/casbin/casbin/v3" -) -``` - -## Implementation - -```go -// NewCasbinMiddleware returns middleware for [Casbin](https://casbin.org/). -func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c *echo.Context) error { - username, err := userGetter(c) - if err != nil { - return echo.ErrUnauthorized.Wrap(err) - } - if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { - return echo.ErrInternalServerError.Wrap(err) - } else if !pass { - return echo.NewHTTPError(http.StatusForbidden, "access denied") - } - return next(c) - } - } -} -``` - -## Usage - -Middleware can be initialized like this: -```go -e := echo.New() -enforcer, err := casbin.NewEnforcer("casbin_auth_model.conf", "casbin_auth_policy.csv") -e.Use(NewCasbinMiddleware(enforcer)) -``` - -## Example - -Create a Casbin policy file `auth_model.conf`: -```ini -[request_definition] -r = sub, obj, act - -[policy_definition] -p = sub, obj, act - -[role_definition] -g = _, _ - -[policy_effect] -e = some(where (p.eft == allow)) - -[matchers] -m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") -``` - -Create a Casbin policy file `auth_policy.csv`: -```csv -p, 1234567890, /dataset1/*, GET -p, alice, /dataset1/*, GET -p, alice, /dataset1/resource1, POST -p, bob, /dataset2/resource1, * -p, bob, /dataset2/resource2, GET -p, bob, /dataset2/folder1/*, POST -p, dataset1_admin, /dataset1/*, * -g, cathy, dataset1_admin -``` - -Authenticating user with JWT and authorizing access to resources based on Casbin policy. -```go - e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication - jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization - token, err := echo.ContextGet[*jwt.Token](c, "user") - if err != nil { - return "", err - } - return token.Claims.GetSubject() - } - e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization -``` - -Try with: -```bash -curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" -``` - -or authenticating user with Basic Auth and authorizing access to resources based on Casbin policy. -```go - // BasicAuth middleware does authentication - e.Use(middleware.BasicAuth(func(c *echo.Context, user string, password string) (bool, error) { - return subtle.ConstantTimeCompare([]byte(user), []byte("alice")) == 1 && - subtle.ConstantTimeCompare([]byte(password), []byte("password")) == 1, nil - })) - basicAuthUser := func(c *echo.Context) (string, error) { // basic auth user getter for Casbin authorization - username, _, _ := c.Request().BasicAuth() // NB: authorization (PASSWORD check) must be done somewhere!!! - return username, nil - } - e.Use(newCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization -``` - -Try with: -```bash -# should pass -curl -v -u "alice:password" http://localhost:8080/dataset1/any -# should fail -curl -v -u "alice:password" http://localhost:8080/dataset2/resource2 -``` - -### Full Casbin + JWT example - -```go -package main - -import ( - "log/slog" - "net/http" - - "github.com/casbin/casbin/v3" - "github.com/golang-jwt/jwt/v5" - echojwt "github.com/labstack/echo-jwt/v5" - "github.com/labstack/echo/v5" -) - -// NewCasbinMiddleware returns middleware for [Casbin](https://casbin.org/). -func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c *echo.Context) error { - username, err := userGetter(c) - if err != nil { - return echo.ErrUnauthorized.Wrap(err) - } - if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { - return echo.ErrInternalServerError.Wrap(err) - } else if !pass { - return echo.NewHTTPError(http.StatusForbidden, "access denied") - } - return next(c) - } - } -} - -/* -Test with: -curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" -*/ -func main() { - e := echo.New() - - ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") - if err != nil { - slog.Error("failed to initialize Casbin enforcer", "error", err) - } - - e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication - jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization - token, err := echo.ContextGet[*jwt.Token](c, "user") - if err != nil { - return "", err - } - return token.Claims.GetSubject() - } - e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization - - e.GET("/*", func(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` diff --git a/website/docs/middleware/context-timeout.md b/website/docs/middleware/context-timeout.md deleted file mode 100644 index 269a3a90..00000000 --- a/website/docs/middleware/context-timeout.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -description: Context Timeout middleware ---- - -# Context Timeout - -Timeout middleware is used to timeout request context within a predefined period so context aware methods could return -early. - -## Usage - -```go -e.Use(middleware.ContextTimeout(60 * time.Second)) -``` - -## Custom Configuration - -```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, -})) -``` - - diff --git a/website/docs/middleware/cors.md b/website/docs/middleware/cors.md deleted file mode 100644 index 7e1f09af..00000000 --- a/website/docs/middleware/cors.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -description: CORS middleware ---- - -# CORS - -CORS middleware implements [CORS](http://www.w3.org/TR/cors) specification. -CORS gives web servers cross-domain access controls, which enable secure cross-domain -data transfers. - -## Usage - -```go -e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, - AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, -})) -``` - -## Configuration - -```go -type CORSConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // AllowOrigins determines the value of the Access-Control-Allow-Origin - // response header. 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 -} -``` - diff --git a/website/docs/middleware/csrf.md b/website/docs/middleware/csrf.md deleted file mode 100644 index 41c7192e..00000000 --- a/website/docs/middleware/csrf.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -description: CSRF middleware ---- - -# CSRF - -Cross-site request forgery, also known as one-click attack or session riding and -abbreviated as CSRF (sometimes pronounced sea-surf) or XSRF, is a type of malicious -exploit of a website where unauthorized commands are transmitted from a user that -the website trusts. - -## Usage - -```go -e.Use(middleware.CSRF()) -``` - -## Custom Configuration - -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() -e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ - TokenLookup: "header:X-XSRF-TOKEN", -})) -``` - -Example above uses `X-XSRF-TOKEN` request header to extract CSRF token. - -*Example Configuration that reads token from Cookie* - -```go -middleware.CSRFWithConfig(middleware.CSRFConfig{ - TokenLookup: "cookie:_csrf", - CookiePath: "/", - CookieDomain: "example.com", - CookieSecure: true, - CookieHTTPOnly: true, - CookieSameSite: http.SameSiteStrictMode, -}) -``` - -## Accessing CSRF Token - -### Server-side - -CSRF token can be accessed from `Echo#Context` using `ContextKey` and passed to -the client via template. - -### Client-side - -CSRF token can be accessed from CSRF cookie. - -## Configuration - -```go -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 -var DefaultCSRFConfig = CSRFConfig{ - Skipper: DefaultSkipper, - TokenLength: 32, - TokenLookup: "header:" + echo.HeaderXCSRFToken, - ContextKey: "csrf", - CookieName: "_csrf", - CookieMaxAge: 86400, - CookieSameSite: http.SameSiteDefaultMode, -} -``` - -## Full example - -```go reference -https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go -``` \ No newline at end of file diff --git a/website/docs/middleware/decompress.md b/website/docs/middleware/decompress.md deleted file mode 100644 index afda56a8..00000000 --- a/website/docs/middleware/decompress.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -description: Decompress middleware ---- - -# Decompress - -Decompress middleware decompresses HTTP request if Content-Encoding header is set to gzip. - -:::note - -The body will be decompressed in memory and consume it for the lifetime of the request (and garbage collection). - -::: - -## Usage - -```go -e.Use(middleware.Decompress()) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ - Skipper: Skipper -})) -``` - -## Configuration - -```go -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 -} -``` - -### Default Configuration - -```go -DefaultDecompressConfig = DecompressConfig{ - Skipper: DefaultSkipper, -} -``` diff --git a/website/docs/middleware/gzip.md b/website/docs/middleware/gzip.md deleted file mode 100644 index b951b2c4..00000000 --- a/website/docs/middleware/gzip.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -description: Gzip middleware ---- - -# Gzip - -Gzip middleware compresses HTTP response using gzip compression scheme. - -## Usage - -`e.Use(middleware.Gzip())` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Level: 5, -})) -``` - -:::tip - -A middleware skipper can be passed to avoid gzip to certain URL(s). - -::: - -#### Example - -```go -e := echo.New() -e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Skipper: func(c *echo.Context) bool { - return strings.Contains(c.Path(), "metrics") // Change "metrics" for your own path - }, -})) -``` - -## Configuration - -```go -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 -} -``` - diff --git a/website/docs/middleware/img/docsVersionDropdown.png b/website/docs/middleware/img/docsVersionDropdown.png deleted file mode 100644 index 97e4164618b5f8beda34cfa699720aba0ad2e342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25427 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 diff --git a/website/docs/middleware/img/localeDropdown.png b/website/docs/middleware/img/localeDropdown.png deleted file mode 100644 index e257edc1f932985396bf59584c7ccfaddf955779..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27841 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 JrC|B>e*nH7H{}2T diff --git a/website/docs/middleware/jwt.md b/website/docs/middleware/jwt.md deleted file mode 100644 index 7186c6d0..00000000 --- a/website/docs/middleware/jwt.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -description: JWT middleware ---- - -# JWT - -JWT provides a JSON Web Token (JWT) authentication middleware. Echo JWT middleware is located at https://github.com/labstack/echo-jwt - -Basic middleware behavior: - -- For valid token, it sets the user in context and calls next handler. -- For invalid token, it sends "401 - Unauthorized" response. -- For missing or invalid `Authorization` header, it sends "400 - Bad Request". - -## Dependencies - -```go -import "github.com/labstack/echo-jwt/v5" -``` - -## Usage - -```go -e.Use(echojwt.JWT([]byte("secret"))) -``` - -## Custom Configuration - -### Usage - -```go -e.Use(echojwt.WithConfig(echojwt.Config{ - // ... - SigningKey: []byte("secret"), - // ... -})) -``` - - -## Configuration - -```go -type Config struct { - // Skipper defines a function to skip middleware. - Skipper middleware.Skipper - - // BeforeFunc defines a function which is executed just before the middleware. - BeforeFunc middleware.BeforeFunc - - // SuccessHandler defines a function which is executed for a valid token. - // 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. - // It may be used to define a custom JWT 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 JWT token value to request and continue with handler chain. - 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`). - // 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 JWT token value in the request context - // and continue. Some logic down the remaining execution chain needs to check that (public) token value then. - ContinueOnIgnoredError bool - - // Context key to store user information from the token into context. - // Optional. Default value "user". - ContextKey string - - // Signing key to validate token. - // 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 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]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 - - // KeyFunc defines a user-defined function that supplies the public key for a token validation. - // The function shall take care of verifying the signing algorithm and selecting the proper key. - // A user-defined KeyFunc can be useful if tokens are issued by an external party. - // Used by default ParseTokenFunc implementation. - // - // When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. - // 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 SigningKeys nor SigningKey is provided. - // Not used if custom ParseTokenFunc is set. - // Default to an internal implementation verifying the signing algorithm and selecting the proper key. - KeyFunc jwt.Keyfunc - - // TokenLookup is a string in the form of ":" or ":,:" that is used - // to extract token 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 JWT tokens `Authorization: Bearer ` prefix we cut is `Bearer `. - // If prefix is left empty the whole value is returned. - // - "query:" - // - "param:" - // - "cookie:" - // - "form:" - // Multiple sources example: - // - "header:Authorization:Bearer ,cookie:myowncookie" - TokenLookup string - - // TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context. - // This is one of the two options to provide a token extractor. - // The order of precedence is user-defined TokenLookupFuncs, and TokenLookup. - // You can also provide both if you want. - TokenLookupFuncs []middleware.ValuesExtractor - - // 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) (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 -} -``` - - -## [Example](../cookbook/jwt.md) diff --git a/website/docs/middleware/key-auth.md b/website/docs/middleware/key-auth.md deleted file mode 100644 index 4871f731..00000000 --- a/website/docs/middleware/key-auth.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -description: Key auth middleware ---- - -# Key Auth - -Key auth middleware provides a key based authentication. - -- For valid key it calls the next handler. -- For invalid key, it sends "401 - Unauthorized" response. -- For missing key, it sends "400 - Bad Request" response. - -## Usage - -```go -e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { - return key == "valid-key", nil -})) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ - KeyLookup: "query:api-key", - Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { - return key == "valid-key", nil - }, -})) -``` - -## Configuration - -```go -type KeyAuthConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // KeyLookup is a string in the form 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 -} -``` - -### Default Configuration - -```go -DefaultKeyAuthConfig = KeyAuthConfig{ - Skipper: DefaultSkipper, - KeyLookup: "header:" + echo.HeaderAuthorization, - AuthScheme: "Bearer", -} -``` diff --git a/website/docs/middleware/logger.md b/website/docs/middleware/logger.md deleted file mode 100644 index 7ce96c25..00000000 --- a/website/docs/middleware/logger.md +++ /dev/null @@ -1,224 +0,0 @@ ---- -description: Logger middleware ---- - -# Logger - -[`RequestLogger`](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) middleware logs the information about each HTTP request. - -## 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. - -You can quickly acquaint yourself with the values that the logger knows to extract by referring to the fields of the [`RequestLoggerConfig`](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) structure below. Or click the link to view the most up-to-date details. -```go -type RequestLoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeNextFunc defines a function that is called before next middleware or handler is called in chain. - 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 - - // 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. - // - // A side-effect of calling global error handler is that now Response has been committed and sent to the client - // and middlewares up in chain can not change Response status code or response body. - HandleError bool - - // LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call). - LogLatency bool - // LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`) - LogProtocol bool - // LogRemoteIP instructs logger to extract request remote IP. See `echo.Context.RealIP()` for implementation details. - LogRemoteIP bool - // LogHost instructs logger to extract request host value (i.e. `example.com`) - LogHost bool - // LogMethod instructs logger to extract request method value (i.e. `GET` etc) - LogMethod bool - // LogURI instructs logger to extract request URI (i.e. `/list?lang=en&page=1`) - LogURI bool - // LogURIPath instructs logger to extract request URI path part (i.e. `/list`) - LogURIPath bool - // LogRoutePath instructs logger to extract route path part to which request was matched to (i.e. `/user/:id`) - LogRoutePath bool - // LogRequestID instructs logger to extract request ID from request `X-Request-ID` header or response if request did not have value. - LogRequestID bool - // LogReferer instructs logger to extract request referer values. - LogReferer bool - // LogUserAgent instructs logger to extract request user agent values. - LogUserAgent bool - // LogStatus instructs logger to extract response status code. If handler chain returns an echo.HTTPError, - // the status code is extracted from the echo.HTTPError returned - LogStatus bool - // LogError instructs logger to extract error returned from executed handler chain. - LogError bool - // LogContentLength instructs logger to extract content length header value. Note: this value could be different from - // actual request body size as it could be spoofed etc. - LogContentLength bool - // LogResponseSize instructs logger to extract response content length value. Note: when used with Gzip middleware - // this value may not be always correct. - LogResponseSize bool - // LogHeaders instructs logger to extract given list of headers from request. Note: request can contain more than - // one header with same value so slice of values is been logger for each given header. - // - // Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header - // names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding". - LogHeaders []string - // LogQueryParams instructs logger to extract given list of query parameters from request URI. Note: request can - // contain more than one query parameter with same name so slice of values is been logger for each given query param name. - LogQueryParams []string - // LogFormValues instructs logger to extract given list of form values from request body+URI. Note: request can - // contain more than one form value with same name so slice of values is been logger for each given form value name. - LogFormValues []string - -} -``` - - -### Examples - -Example for naive `fmt.Printf` -```go -skipper := func(c *echo.Context) bool { - // Skip health check endpoint - return c.Request().URL.Path == "/health" -} -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - Skipper: skipper, - BeforeNextFunc: func(c *echo.Context) { - c.Set("customValueFromContext", 42) - }, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - value, _ := c.Get("customValueFromContext").(int) - fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) - return nil - }, -})) -``` -*Sample output* - -```exec -REQUEST: uri: /hello, status: 200, custom-value: 42 -``` - - -Example for slog (https://pkg.go.dev/log/slog) -```go -logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, -})) -``` -*Sample output* -```exec -{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} -``` - -Example for Zerolog (https://github.com/rs/zerolog) -```go -logger := zerolog.New(os.Stdout) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info(). - Str("URI", v.URI). - Int("status", v.Status). - Msg("request") - - return nil - }, -})) -``` -*Sample output* -```exec -{"level":"info","URI":"/hello","status":200,"message":"request"} -``` - -Example for Zap (https://github.com/uber-go/zap) -```go -logger, _ := zap.NewProduction() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info("request", - zap.String("URI", v.URI), - zap.Int("status", v.Status), - ) - - return nil - }, -})) -``` -*Sample output* -```exec -{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} -``` - -Example for Logrus (https://github.com/sirupsen/logrus) -```go -log := logrus.New() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { - log.WithFields(logrus.Fields{ - "URI": values.URI, - "status": values.Status, - }).Info("request") - - return nil - }, -})) -``` -*Sample output* -```exec -time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 -``` - -### Troubleshooting Tips - -#### 1. Solution for "panic: missing LogValuesFunc callback function for request logger middleware" -This panic arises when the `LogValuesFunc` callback function, which is mandatory for the request logger middleware configuration, is left unset. - -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 { - fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) - return nil -} - -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogValuesFunc: logValues, -})) -``` - -#### 2. If Parameters in Logs Are Empty -When investigating logging-related glitches, if you notice that certain parameters like `v.URI` and `v.Status` within the `LogValuesFunc` function produce empty outputs, your focus should shift to validating the relevant configuration elements. Specifically, check whether the corresponding items (such as `LogStatus`, `LogURI`, etc.) in `e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{...}))` have been erroneously set to `false` or failed to activate properly due to miscellaneous factors. Ensure these configuration particulars are accurately configured so that the pertinent request and response data can be precisely logged. \ No newline at end of file diff --git a/website/docs/middleware/method-override.md b/website/docs/middleware/method-override.md deleted file mode 100644 index 451935b5..00000000 --- a/website/docs/middleware/method-override.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: Method override middleware ---- - -# Method Override - -Method override middleware checks for the overridden method from the request and -uses it instead of the original method. - -:::info - -For security reasons, only `POST` method can be overridden. - -::: - -## Usage - -```go -e.Pre(middleware.MethodOverride()) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ - Getter: middleware.MethodFromForm("_method"), -})) -``` - -## Configuration - -```go -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 -} -``` - -### Default Configuration - -```go -DefaultMethodOverrideConfig = MethodOverrideConfig{ - Skipper: DefaultSkipper, - Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), -} -``` diff --git a/website/docs/middleware/open-telemetry.md b/website/docs/middleware/open-telemetry.md deleted file mode 100644 index 1a051b56..00000000 --- a/website/docs/middleware/open-telemetry.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -description: Open-telemetry middleware ---- - -# Open-telemetry - -Echo-opentelemetry is a middleware for Echo framework that provides OpenTelemetry instrumentation for HTTP requests. -https://github.com/labstack/echo-opentelemetry - -Open-telemetry is a set of open-source tools that provide instrumentation for cloud native applications. -* [OpenTelemetry Exporters](https://opentelemetry.io/docs/languages/go/exporters/) -* [OpenTelemetry HTTP spec](https://opentelemetry.io/docs/specs/semconv/http/) -* [HTTP metrics spec](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/) - - - -## Usage - -Add OpenTelemetry middleware dependency with go modules - -```bash -go get github.com/labstack/echo-opentelemetry -``` - -Use as an import statement - -```go -e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ - ServerName: "my-server", - TracerProvider: tp, - - //Skipper: nil, - //OnNextError: nil, - //OnExtractionError: nil, - //MeterProvider: nil, - //Propagators: nil, - //SpanStartOptions: nil, - //SpanStartAttributes: nil, - //SpanEndAttributes: nil, - //MetricAttributes: nil, - //Metrics: nil, -})) -``` -For configuration options documentation see [Config](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28) struct. - -Add middleware in simplified form, by providing only the server name - -```go -e.Use(echootel.NewMiddleware("app.example.com")) -``` - -Add middleware with configuration options - -```go -e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ - TracerProvider: tp, -})) -``` - -Retrieving the tracer from the Echo context -```go -tp, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) -``` - -## Example - -This example is exporting metrics and spans to stdout but you can use any exporter (OTLP etc). -Read about [OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters) - -```go reference -https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go -``` diff --git a/website/docs/middleware/prometheus.md b/website/docs/middleware/prometheus.md deleted file mode 100644 index 53dc38c3..00000000 --- a/website/docs/middleware/prometheus.md +++ /dev/null @@ -1,285 +0,0 @@ ---- -description: Prometheus metrics middleware ---- - -# Prometheus - -Prometheus middleware generates metrics for HTTP requests. - -https://github.com/labstack/echo-prometheus - -## Usage - -- Add needed module `go get -u github.com/labstack/echo-prometheus` -- Add Prometheus middleware and metrics serving route - ```go - e := echo.New() - e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics - e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics - ``` - -## Examples - -Serve metric from the same server as where metrics is gathered - -```go -package main - -import ( - "net/http" - - echoprometheus "github.com/labstack/echo-prometheus" - "github.com/labstack/echo/v5" -) - -func main() { - e := echo.New() - - e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics - e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics - - e.GET("/hello", func(c *echo.Context) error { - return c.String(http.StatusOK, "hello") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -Serve metrics on a separate port - -```go -func main() { - e := echo.New() // this Echo instance will serve 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.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics - if err := metrics.Start(":8081"); err != nil { - e.Logger.Error("failed to start metrics server", "error", err) - } - }() - - e.GET("/hello", func(c *echo.Context) error { - return c.String(http.StatusOK, "hello") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -*Sample output (for first example)* - -```bash -curl http://localhost:8080/metrics - -# HELP echo_request_duration_seconds The HTTP request latencies in seconds. -# TYPE echo_request_duration_seconds summary -echo_request_duration_seconds_sum 0.41086482 -echo_request_duration_seconds_count 1 -# HELP echo_request_size_bytes The HTTP request sizes in bytes. -# TYPE echo_request_size_bytes summary -echo_request_size_bytes_sum 56 -echo_request_size_bytes_count 1 -# HELP echo_requests_total How many HTTP requests processed, partitioned by status code and HTTP method. -# TYPE echo_requests_total counter -echo_requests_total{code="200",host="localhost:8080",method="GET",url="/"} 1 -# HELP echo_response_size_bytes The HTTP response sizes in bytes. -# TYPE echo_response_size_bytes summary -echo_response_size_bytes_sum 61 -echo_response_size_bytes_count 1 -... -``` - -## Custom Configuration - -### Serving custom Prometheus Metrics - -*Usage* - -Using custom metrics with Prometheus default registry: - -```go -package main - -import ( - "log" - - echoprometheus "github.com/labstack/echo-prometheus" - "github.com/labstack/echo/v5" - "github.com/prometheus/client_golang/prometheus" -) - -func main() { - e := echo.New() // this Echo instance will serve route on port 8080 - - customCounter := prometheus.NewCounter( // create new counter metric. This is replacement for `prometheus.Metric` struct - prometheus.CounterOpts{ - Name: "custom_requests_total", - Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", - }, - ) - if err := prometheus.Register(customCounter); err != nil { // register your new counter metric with default metrics registry - log.Fatal(err) - } - - e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ - AfterNext: func(c *echo.Context, err error) { - customCounter.Inc() // use our custom metric in middleware. after every request increment the counter - }, - })) - e.GET("/metrics", echoprometheus.NewHandler()) // register route for getting gathered metrics - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} - -``` - -or create your own registry and register custom metrics with that: - -```go -package main - -import ( - "log" - - echoprometheus "github.com/labstack/echo-prometheus" - "github.com/labstack/echo/v5" - "github.com/prometheus/client_golang/prometheus" -) - -func main() { - e := echo.New() // this Echo instance will serve 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 - prometheus.CounterOpts{ - Name: "custom_requests_total", - Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", - }, - ) - if err := customRegistry.Register(customCounter); err != nil { // register your new counter metric with metrics registry - log.Fatal(err) - } - - e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ - 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 - })) - 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 { - e.Logger.Error("failed to start server", "error", err) - } -} - -``` - -### Skipping URL(s) - -*Usage* - -A middleware skipper can be passed to avoid generating metrics to certain URL(s) - -```go -package main - -import ( - "net/http" - "strings" - - echoprometheus "github.com/labstack/echo-prometheus" - "github.com/labstack/echo/v5" -) - -func main() { - e := echo.New() // this Echo instance will serve route on port 8080 - - mwConfig := echoprometheus.MiddlewareConfig{ - Skipper: func(c *echo.Context) bool { - return strings.HasPrefix(c.Path(), "/testurl") - }, // does not gather metrics metrics on routes starting with `/testurl` - } - e.Use(echoprometheus.NewMiddlewareWithConfig(mwConfig)) // adds middleware to gather metrics - - e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics - - e.GET("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} - -``` - -## Complex Scenarios - -Example: modify default `echoprometheus` metrics definitions - -```go -package main - -import ( - "net/http" - - echoprometheus "github.com/labstack/echo-prometheus" - "github.com/labstack/echo/v5" - "github.com/prometheus/client_golang/prometheus" -) - -func main() { - e := echo.New() // this Echo instance will serve 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/website/docs/middleware/proxy.md b/website/docs/middleware/proxy.md deleted file mode 100644 index e5051de5..00000000 --- a/website/docs/middleware/proxy.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -description: Reverse proxy middleware ---- - -# Proxy - -Proxy provides an HTTP/WebSocket reverse proxy middleware. It forwards a request -to upstream server using a configured load balancing technique. - -### Usage - -```go -url1, err := url.Parse("http://localhost:8081") -if err != nil { - e.Logger.Error("failed parse url", "error", err) -} -url2, err := url.Parse("http://localhost:8082") -if err != nil { - e.Logger.Error("failed parse url", "error", err) -} -e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ - { - URL: url1, - }, - { - URL: url2, - }, -}))) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) -``` - -### Configuration - -```go -type ProxyConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Balancer defines a load balancing technique. - // Required. - Balancer ProxyBalancer - - // RetryCount defines the number of times a failed proxied request should be retried - // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried. - RetryCount int - - // RetryFilter defines a function used to determine if a failed request to a - // ProxyTarget should be retried. The RetryFilter will only be called when the number - // of previous retries is less than RetryCount. If the function returns true, the - // request will be retried. The provided error indicates the reason for the request - // failure. When the ProxyTarget is unavailable, the error will be an instance of - // echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error - // will indicate an internal error in the Proxy middleware. When a RetryFilter is not - // specified, all requests that fail with http.StatusBadGateway will be retried. A custom - // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is - // only called when the request to the target fails, or an internal error in the Proxy - // middleware has occurred. Successful requests that return a non-200 response code cannot - // be retried. - RetryFilter func(c *echo.Context, e error) bool - - // ErrorHandler defines a function which can be used to return custom errors from - // the Proxy middleware. ErrorHandler is only invoked when there has been - // either an internal error in the Proxy middleware or the ProxyTarget is - // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked - // when a ProxyTarget returns a non-200 response. In these cases, the response - // is already written so errors cannot be modified. ErrorHandler is only - // invoked after all retry attempts have been exhausted. - ErrorHandler func(c *echo.Context, err error) error - - // Rewrite defines URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - // Examples: - // "/old": "/new", - // "/api/*": "/$1", - // "/js/*": "/public/javascripts/$1", - // "/users/*/orders/*": "/user/$1/order/$2", - Rewrite map[string]string - - // RegexRewrite defines rewrite rules using regexp.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 - -| Name | Value | -| ---------- | -------------- | -| Skipper | DefaultSkipper | -| ContextKey | `target` | - -### Regex-based Rules - -For advanced rewriting of proxy requests rules may also be defined using -regular expression. Normal capture groups can be defined using `()` and referenced by index (`$1`, `$2`, ...) for the rewritten path. - -`RegexRules` and normal `Rules` can be combined. - -```go - e.Use(ProxyWithConfig(ProxyConfig{ - Balancer: rrb, - Rewrite: map[string]string{ - "^/v1/*": "/v2/$1", - }, - RegexRewrite: map[*regexp.Regexp]string{ - regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", - regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", - }, - })) -``` - -## [Example](../cookbook/reverse-proxy.md) diff --git a/website/docs/middleware/rate-limiter.md b/website/docs/middleware/rate-limiter.md deleted file mode 100644 index 5a48c646..00000000 --- a/website/docs/middleware/rate-limiter.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -description: Rate limiter middleware ---- - -# Rate Limiter - -`RateLimiter` provides a Rate Limiter middleware for limiting the amount of requests to the server from a particular IP or id within a time period. - -By default an in-memory store is used for keeping track of requests. The default in-memory implementation is focused on correctness and -may not be the best option for a high number of concurrent requests or a large number of different identifiers (>16k). - -## Usage - -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(20.0))) -``` - -:::info - -If the provided rate is a float number, Burst will be treated as the rounded down value of the rate. - -::: - -## Custom Configuration - -```go -config := middleware.RateLimiterConfig{ - Skipper: middleware.DefaultSkipper, - Store: middleware.NewRateLimiterMemoryStoreWithConfig( - middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}, - ), - IdentifierExtractor: func(c *echo.Context) (string, error) { - id := c.RealIP() - return id, nil - }, - ErrorHandler: func(c *echo.Context, err error) error { - return c.JSON(http.StatusForbidden, nil) - }, - DenyHandler: func(c *echo.Context, identifier string,err error) error { - return c.JSON(http.StatusTooManyRequests, nil) - }, -} - -e.Use(middleware.RateLimiterWithConfig(config)) -``` - -### Errors - -```go -var ( - // ErrRateLimitExceeded denotes an error raised when rate limit is exceeded - ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") - // ErrExtractorError denotes an error raised when extractor function is unsuccessful - ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier") -) -``` - -:::tip - -If you need to implement your own store, be sure to implement the RateLimiterStore interface and pass it to RateLimiterConfig and you're good to go! - -::: - -## Configuration - -```go -type RateLimiterConfig struct { - Skipper Skipper - BeforeFunc BeforeFunc - // IdentifierExtractor uses echo.Context to extract the identifier for a visitor - IdentifierExtractor Extractor - // Store defines a store for the rate limiter - Store RateLimiterStore - // ErrorHandler provides a handler to be called when IdentifierExtractor returns a non-nil error - ErrorHandler func(context echo.Context, err error) error - // DenyHandler provides a handler to be called when RateLimiter denies access - DenyHandler func(context echo.Context, identifier string, err error) error -} -``` - -### Default Configuration - -```go -// DefaultRateLimiterConfig defines default values for RateLimiterConfig -var DefaultRateLimiterConfig = RateLimiterConfig{ - Skipper: DefaultSkipper, - IdentifierExtractor: func(ctx echo.Context) (string, error) { - id := ctx.RealIP() - return id, nil - }, - ErrorHandler: func(context echo.Context, err error) error { - return &echo.HTTPError{ - Code: ErrExtractorError.Code, - Message: ErrExtractorError.Message, - Internal: err, - } - }, - DenyHandler: func(context echo.Context, identifier string, err error) error { - return &echo.HTTPError{ - Code: ErrRateLimitExceeded.Code, - Message: ErrRateLimitExceeded.Message, - Internal: err, - } - }, -} -``` - diff --git a/website/docs/middleware/recover.md b/website/docs/middleware/recover.md deleted file mode 100644 index 54c38a00..00000000 --- a/website/docs/middleware/recover.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -description: Recover middleware ---- - -# Recover - -Recover middleware recovers from panics anywhere in the chain, prints stack trace -and handles the control to the centralized -[HTTPErrorHandler](../guide/customization.md#http-error-handler). - -## Usage - -```go -e.Use(middleware.Recover()) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ - StackSize: 1 << 10, // 1 KB -})) -``` - -Example above uses a `StackSize` of 1 KB and default values for `DisableStackAll` and `DisablePrintStack`. - -## Configuration - -```go -type RecoverConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Size of the stack to be printed. - // Optional. Default value 4KB. - StackSize int - - // DisableStackAll disables formatting stack traces of all other goroutines - // into 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 -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 deleted file mode 100644 index 8723a706..00000000 --- a/website/docs/middleware/redirect.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -description: Redirect middleware ---- - -# Redirect - -## HTTPS Redirect - -HTTPS redirect middleware redirects http requests to https. -For example, http://labstack.com will be redirected to https://labstack.com. - -### Usage - -```go -e := echo.New() -e.Pre(middleware.HTTPSRedirect()) -``` - -## HTTPS WWW Redirect - -HTTPS WWW redirect redirects http requests to www https. -For example, http://labstack.com will be redirected to https://www.labstack.com. - -## Usage - -```go -e := echo.New() -e.Pre(middleware.HTTPSWWWRedirect()) -``` - -## HTTPS NonWWW Redirect - -HTTPS NonWWW redirect redirects http requests to https non www. -For example, http://www.labstack.com will be redirect to https://labstack.com. - -### Usage - -```go -e := echo.New() -e.Pre(middleware.HTTPSNonWWWRedirect()) -``` - -## WWW Redirect - -WWW redirect redirects non www requests to www. - -For example, http://labstack.com will be redirected to http://www.labstack.com. - -### Usage - -```go -e := echo.New() -e.Pre(middleware.WWWRedirect()) -``` - -## NonWWW Redirect - -NonWWW redirect redirects www requests to non www. -For example, http://www.labstack.com will be redirected to http://labstack.com. - -### Usage - -```go -e := echo.New() -e.Pre(middleware.NonWWWRedirect()) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ - Code: http.StatusTemporaryRedirect, -})) -``` - -Example above will redirect the request HTTP to HTTPS with status code `307 - StatusTemporaryRedirect`. - -## Configuration - -```go -RedirectConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Status code to be used when redirecting the request. - // Optional. Default value http.StatusMovedPermanently. - Code int -} -``` - -### Default Configuration* - -```go -DefaultRedirectConfig = RedirectConfig{ - Skipper: DefaultSkipper, - Code: http.StatusMovedPermanently, -} -``` diff --git a/website/docs/middleware/request-id.md b/website/docs/middleware/request-id.md deleted file mode 100644 index 25e0067e..00000000 --- a/website/docs/middleware/request-id.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -description: Request ID middleware ---- - -# Request ID - -Request ID middleware generates a unique id for a request. - -## Usage - -```go -e.Use(middleware.RequestID()) -``` - -*Example* - -```go -func main() { - e := echo.New() - - e.Use(middleware.RequestID()) - - e.GET("/", func(c *echo.Context) error { - return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID)) - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## Custom Configuration - -### Usage - -```go -e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ - Generator: func() string { - return customGenerator() - }, -})) -``` - -## Configuration - -```go -type RequestIDConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Generator defines a function to generate an ID. - // Optional. Default value random.String(32). - Generator func() string - - // RequestIDHandler defines a function which is executed for a request id. - RequestIDHandler func(c *echo.Context, requestID string) - - // TargetHeader defines what header to look for to populate the id. - // Optional. Default value is `X-Request-Id` - TargetHeader string -} -``` - -### Default Configuration - -```go -DefaultRequestIDConfig = RequestIDConfig{ - Skipper: DefaultSkipper, - Generator: generator, - TargetHeader: echo.HeaderXRequestID, -} -``` - -## Set ID - -You can set the id from the requester with the `X-Request-ID`-Header - -### Request - -```sh -curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param" -``` - -### Log - -```js -{"time":"2017-11-13T20:26:28.6438003+01:00","id":"3","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/?my=param","my":"param","status":200, "latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":13} -``` diff --git a/website/docs/middleware/rewrite.md b/website/docs/middleware/rewrite.md deleted file mode 100644 index 8e9a0964..00000000 --- a/website/docs/middleware/rewrite.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: Rewrite middleware ---- - -# Rewrite - -Rewrite middleware allows to rewrite an URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. - -## Usage - -```go -e.Pre(middleware.Rewrite(map[string]string{ - "/old": "/new", - "/api/*": "/$1", - "/js/*": "/public/javascripts/$1", - "/users/*/orders/*": "/user/$1/order/$2", -})) -``` - -The values captured in asterisk can be retrieved by index e.g. $1, $2 and so on. -Each asterisk will be non-greedy (translated to a capture group `(.*?)`) and if using -multiple asterisk a trailing `*` will match the "rest" of the path. - -:::caution - -Rewrite middleware should be registered via `Echo#Pre()` to get triggered before the router. - -::: - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) -``` - -### Configuration - -```go -type RewriteConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Rules defines the URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - // Example: - // "/old": "/new", - // "/api/*": "/$1", - // "/js/*": "/public/javascripts/$1", - // "/users/*/orders/*": "/user/$1/order/$2", - // Required. - Rules map[string]string - - // RegexRules defines the URL path rewrite rules using regexp.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: - -| Name | Value | -| ------- | -------------- | -| Skipper | DefaultSkipper | - -### Regex-based Rules - -For advanced rewriting of paths rules may also be defined using regular expression. -Normal capture groups can be defined using `()` and referenced by index (`$1`, `$2`, ...) for the rewritten path. - -`RegexRules` and normal `Rules` can be combined. - -```go - e.Pre(RewriteWithConfig(RewriteConfig{ - Rules: map[string]string{ - "^/v1/*": "/v2/$1", - }, - RegexRules: map[*regexp.Regexp]string{ - regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", - regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", - }, - })) -``` diff --git a/website/docs/middleware/secure.md b/website/docs/middleware/secure.md deleted file mode 100644 index bdb615d5..00000000 --- a/website/docs/middleware/secure.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -description: Secure middleware ---- - -# Secure - -Secure middleware provides protection against cross-site scripting (XSS) attack, -content type sniffing, clickjacking, insecure connection and other code injection -attacks. - -## Usage - -```go -e.Use(middleware.Secure()) -``` - -## Custom Configuration - -### Usage - -```go -e := echo.New() -e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ - XSSProtection: "", - ContentTypeNosniff: "", - XFrameOptions: "", - HSTSMaxAge: 3600, - ContentSecurityPolicy: "default-src 'self'", -})) -``` - -:::info - -Passing empty `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions` or `ContentSecurityPolicy` -disables that protection. - -::: - -## Configuration - -```go -type SecureConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // XSSProtection provides protection against cross-site scripting attack (XSS) - // by setting the `X-XSS-Protection` header. - // Optional. Default value "1; mode=block". - XSSProtection string - - // ContentTypeNosniff provides protection against overriding Content-Type - // header by setting the `X-Content-Type-Options` header. - // Optional. Default value "nosniff". - ContentTypeNosniff string - - // XFrameOptions can be used to indicate whether or not a browser should - // be allowed to render a page in a ,