From 77d0ebad069c20e8607c1677410bfcdb8bcee2c5 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Mon, 15 Jun 2026 21:15:26 -0700 Subject: [PATCH 1/4] feat(docs): i18n config (zh-cn, ja) --- site/astro.config.mjs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/site/astro.config.mjs b/site/astro.config.mjs index b6a54196..579e3a9d 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -10,6 +10,12 @@ export default defineConfig({ integrations: [ starlight({ title: 'Echo', + defaultLocale: 'root', + locales: { + root: { label: 'English', lang: 'en' }, + 'zh-cn': { label: '简体中文', lang: 'zh-CN' }, + ja: { label: '日本語', lang: 'ja' }, + }, logo: { light: './src/assets/logo-light.svg', dark: './src/assets/logo-dark.svg', @@ -73,9 +79,9 @@ export default defineConfig({ // Autogenerated from the content dirs — new pages appear automatically, // ordered by each page's `sidebar.order` frontmatter. sidebar: [ - { label: 'Guide', items: [{ autogenerate: { directory: 'guide' } }] }, - { label: 'Middleware', items: [{ autogenerate: { directory: 'middleware' } }] }, - { label: 'Cookbook', items: [{ autogenerate: { directory: 'cookbook' } }] }, + { label: 'Guide', translations: { 'zh-CN': '指南', ja: 'ガイド' }, items: [{ autogenerate: { directory: 'guide' } }] }, + { label: 'Middleware', translations: { 'zh-CN': '中间件', ja: 'ミドルウェア' }, items: [{ autogenerate: { directory: 'middleware' } }] }, + { label: 'Cookbook', translations: { 'zh-CN': '示例', ja: 'クックブック' }, items: [{ autogenerate: { directory: 'cookbook' } }] }, ], // tune the built-in code theme toward our terminal palette expressiveCode: { themes: ['github-dark', 'github-light'] }, From 3a39423b06c5dbd0695a1d1db65815633e7e177e Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Mon, 15 Jun 2026 21:28:04 -0700 Subject: [PATCH 2/4] docs(i18n): guide (zh-cn, ja) --- site/src/content/docs/ja/guide/binding.md | 154 +++++++++ site/src/content/docs/ja/guide/context.md | 80 +++++ site/src/content/docs/ja/guide/cookies.md | 71 ++++ .../content/docs/ja/guide/customization.md | 64 ++++ .../content/docs/ja/guide/error-handling.md | 82 +++++ .../src/content/docs/ja/guide/installation.md | 57 ++++ site/src/content/docs/ja/guide/ip-address.md | 112 ++++++ site/src/content/docs/ja/guide/quickstart.md | 76 +++++ site/src/content/docs/ja/guide/request.md | 170 ++++++++++ site/src/content/docs/ja/guide/response.md | 318 ++++++++++++++++++ site/src/content/docs/ja/guide/routing.md | 77 +++++ .../src/content/docs/ja/guide/static-files.md | 88 +++++ site/src/content/docs/ja/guide/templates.md | 132 ++++++++ site/src/content/docs/ja/guide/testing.md | 264 +++++++++++++++ site/src/content/docs/zh-cn/guide/binding.md | 149 ++++++++ site/src/content/docs/zh-cn/guide/context.md | 77 +++++ site/src/content/docs/zh-cn/guide/cookies.md | 70 ++++ .../content/docs/zh-cn/guide/customization.md | 61 ++++ .../docs/zh-cn/guide/error-handling.md | 81 +++++ .../content/docs/zh-cn/guide/installation.md | 57 ++++ .../content/docs/zh-cn/guide/ip-address.md | 105 ++++++ .../content/docs/zh-cn/guide/quickstart.md | 75 +++++ site/src/content/docs/zh-cn/guide/request.md | 168 +++++++++ site/src/content/docs/zh-cn/guide/response.md | 316 +++++++++++++++++ site/src/content/docs/zh-cn/guide/routing.md | 75 +++++ .../content/docs/zh-cn/guide/static-files.md | 85 +++++ .../src/content/docs/zh-cn/guide/templates.md | 131 ++++++++ site/src/content/docs/zh-cn/guide/testing.md | 262 +++++++++++++++ 28 files changed, 3457 insertions(+) create mode 100644 site/src/content/docs/ja/guide/binding.md create mode 100644 site/src/content/docs/ja/guide/context.md create mode 100644 site/src/content/docs/ja/guide/cookies.md create mode 100644 site/src/content/docs/ja/guide/customization.md create mode 100644 site/src/content/docs/ja/guide/error-handling.md create mode 100644 site/src/content/docs/ja/guide/installation.md create mode 100644 site/src/content/docs/ja/guide/ip-address.md create mode 100644 site/src/content/docs/ja/guide/quickstart.md create mode 100644 site/src/content/docs/ja/guide/request.md create mode 100644 site/src/content/docs/ja/guide/response.md create mode 100644 site/src/content/docs/ja/guide/routing.md create mode 100644 site/src/content/docs/ja/guide/static-files.md create mode 100644 site/src/content/docs/ja/guide/templates.md create mode 100644 site/src/content/docs/ja/guide/testing.md create mode 100644 site/src/content/docs/zh-cn/guide/binding.md create mode 100644 site/src/content/docs/zh-cn/guide/context.md create mode 100644 site/src/content/docs/zh-cn/guide/cookies.md create mode 100644 site/src/content/docs/zh-cn/guide/customization.md create mode 100644 site/src/content/docs/zh-cn/guide/error-handling.md create mode 100644 site/src/content/docs/zh-cn/guide/installation.md create mode 100644 site/src/content/docs/zh-cn/guide/ip-address.md create mode 100644 site/src/content/docs/zh-cn/guide/quickstart.md create mode 100644 site/src/content/docs/zh-cn/guide/request.md create mode 100644 site/src/content/docs/zh-cn/guide/response.md create mode 100644 site/src/content/docs/zh-cn/guide/routing.md create mode 100644 site/src/content/docs/zh-cn/guide/static-files.md create mode 100644 site/src/content/docs/zh-cn/guide/templates.md create mode 100644 site/src/content/docs/zh-cn/guide/testing.md diff --git a/site/src/content/docs/ja/guide/binding.md b/site/src/content/docs/ja/guide/binding.md new file mode 100644 index 00000000..da9283b5 --- /dev/null +++ b/site/src/content/docs/ja/guide/binding.md @@ -0,0 +1,154 @@ +--- +title: バインディング +description: パス、クエリ、header、リクエストボディからリクエストデータを型付き Go struct に解析します。 +sidebar: + order: 5 +--- + +リクエストデータの解析は、Web アプリケーションの重要な要素です。Echo ではこれを +_バインディング_ と呼び、HTTP リクエストの 4 つの部分から読み取れます。 + +- URL パスパラメーター +- URL クエリパラメーター +- Header +- リクエストボディ + +## Struct タグによるバインディング + +データソースとキーを指定するタグ付きの struct を定義し、そのポインターを +`c.Bind()` に渡します。ここではクエリパラメーター `id` が `ID` フィールドに +バインドされます。 + +```go +type User struct { + ID string `query:"id"` +} + +// handler for /users?id= +var user User +if err := c.Bind(&user); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +``` + +### データソース + +| タグ | ソース | +| -------- | ------ | +| `query` | クエリパラメーター | +| `param` | パスパラメーター | +| `header` | Header 値 | +| `form` | フォームデータ(クエリ + ボディ) | +| `json` | リクエストボディ(`encoding/json`) | +| `xml` | リクエストボディ(`encoding/xml`) | + +パス、クエリ、header、フォームのフィールドには**明示的なタグ**が必要です。 +JSON と XML はタグが省略された場合、標準ライブラリと同じように struct フィールド名へ +フォールバックします。 + +### ボディのコンテンツタイプ + +リクエストボディをデコードするときは、`Content-Type` header によってデコーダーが選ばれます。 + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### 複数ソースと優先順位 + +1 つのフィールドで複数のソースを宣言できます。データは次の順序でバインドされ、 +各ステップが前の値を上書きします。 + +1. パスパラメーター +2. クエリパラメーター(GET / DELETE のみ) +3. リクエストボディ + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### 1 つのソースから直接バインドする + +```go +echo.BindBody(c, &payload) // request body +echo.BindQueryParams(c, &payload) // query parameters +echo.BindPathValues(c, &payload) // path parameters +echo.BindHeaders(c, &payload) // headers +``` + +:::note +Header は `c.Bind()` に**含まれません**。`echo.BindHeaders` で直接バインドしてください。 +::: + +:::caution[セキュリティ] +ビジネス用の struct に直接バインドしないでください。バインド対象の struct が +`IsAdmin bool` フィールドを公開している場合、`{"IsAdmin": true}` というリクエストボディで +その値が設定されます。専用の DTO を使い、明示的にマッピングしてください。 +::: + +```go +type UserDTO struct { + Name string `json:"name" form:"name" query:"name"` + Email string `json:"email" form:"email" query:"email"` +} + +e.POST("/users", func(c *echo.Context) error { + var dto UserDTO + if err := c.Bind(&dto); err != nil { + return c.String(http.StatusBadRequest, "bad request") + } + user := User{Name: dto.Name, Email: dto.Email, IsAdmin: false} + executeSomeBusinessLogic(user) + return c.JSON(http.StatusOK, user) +}) +``` + +## フルーエントバインディング + +単一ソースから明示的かつ型安全にバインドするには、フルーエント binder を使います。 +設定をチェーンし、実行時にエラーを収集します。 + +```go +// /api/search?active=true&id=1&id=2&id=3&length=25 +var opts struct { + IDs []int64 + Active bool +} +length := int64(50) + +err := echo.QueryParamsBinder(c). + Int64("length", &length). + Int64s("id", &opts.IDs). + Bool("active", &opts.Active). + BindError() // first error, if any +``` + +利用できる binder は `echo.QueryParamsBinder(c)`、`echo.PathValuesBinder(c)`、 +`echo.FormFieldBinder(c)` です。チェーンは `BindError()`(最初のエラー)または +`BindErrors()`(すべてのエラー)で終了します。`FailFast(false)` はチェーン全体を実行します。 +デフォルトでは早期終了が有効です。 + +各サポート型には `Type(...)`、`MustType(...)`、`Types(...)`(スライス)、 +`MustTypes(...)` メソッドがあります。例:`Int64`、`MustInt64`、`Int64s`。 +カンマ区切りの値を分割するには `BindWithDelimiter("id", &dest, ",")` を使います。 + +## カスタム binder + +`Echo#Binder` でカスタム binder を登録します。 + +```go +type CustomBinder struct{} + +func (cb *CustomBinder) Bind(c *echo.Context, i any) error { + db := new(echo.DefaultBinder) + if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType { + return err + } + // custom logic here + return nil +} + +e.Binder = &CustomBinder{} +``` diff --git a/site/src/content/docs/ja/guide/context.md b/site/src/content/docs/ja/guide/context.md new file mode 100644 index 00000000..3c79522c --- /dev/null +++ b/site/src/content/docs/ja/guide/context.md @@ -0,0 +1,80 @@ +--- +title: コンテキスト +description: リクエスト、レスポンス、パラメーター、ヘルパーを保持するリクエストごとのオブジェクトです。 +sidebar: + order: 4 +--- + +`echo.Context` は現在の HTTP リクエストのコンテキストを表します。そのポインター +(`*echo.Context`)はすべてのハンドラとミドルウェアに渡され、リクエストとレスポンス、 +パスパラメーター、バインド済みデータ、レスポンス作成用のヘルパーを保持します。 + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## 入力を読む + +```go +id := c.Param("id") // path parameter +q := c.QueryParam("q") // query string value +all := c.QueryParams() // url.Values of all query params +name := c.FormValue("name") // form field (URL + body) +ua := c.Request().Header.Get(echo.HeaderUserAgent) +``` + +値が存在しない場合にデフォルト値を返す、対応する `*Or` ヘルパーもあります。 +`c.ParamOr("id", "0")`、`c.QueryParamOr("page", "1")`、`c.FormValueOr(...)` +などです。 + +## レスポンスを書く + +```go +c.String(http.StatusOK, "plain text") +c.JSON(http.StatusOK, payload) +c.JSONPretty(http.StatusOK, payload, " ") +c.HTML(http.StatusOK, "hi") +c.XML(http.StatusOK, payload) +c.Blob(http.StatusOK, "application/pdf", bytes) +c.Stream(http.StatusOK, "application/octet-stream", reader) +c.NoContent(http.StatusNoContent) +c.Redirect(http.StatusFound, "/elsewhere") +``` + +## ファイル + +```go +c.File("public/report.pdf") // serve a file +c.Attachment("invoice.pdf", "inv.pdf") // prompt download +c.Inline("photo.png", "photo.png") // render inline +``` + +## リクエストごとのストレージ + +`Get`/`Set` を使ってミドルウェアとハンドラの間でデータを共有します。 + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +ジェネリクスヘルパーで型付きアクセスもできます。 + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## バインディングと検証 + +`c.Bind()` はリクエストデータを struct に解析します。詳しくは +[バインディング](/ja/guide/binding/)を参照してください。 + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/ja/guide/cookies.md b/site/src/content/docs/ja/guide/cookies.md new file mode 100644 index 00000000..11559267 --- /dev/null +++ b/site/src/content/docs/ja/guide/cookies.md @@ -0,0 +1,71 @@ +--- +title: Cookie +description: 標準の http.Cookie 型を使って HTTP Cookie を作成、読み取り、一覧表示します。 +sidebar: + order: 11 +--- + +Cookie はサーバーがブラウザーに送信する小さなデータで、ブラウザーはそれを保存し、 +後続のリクエストで送り返します。Cookie により、ショッピングカート、認証状態、 +以前入力したフォーム値などの状態を Web サイトが記憶できます。 + +Echo は Go 標準の `http.Cookie` 型を使い、ハンドラ内の `echo.Context` から +Cookie を追加および取得します。 + +## Cookie 属性 + +| 属性 | 任意 | +| ---------- | ---- | +| `Name` | いいえ | +| `Value` | いいえ | +| `Path` | はい | +| `Domain` | はい | +| `Expires` | はい | +| `Secure` | はい | +| `HttpOnly` | はい | + +## 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") +} +``` + +- `new(http.Cookie)` で Cookie を作成します。 +- `http.Cookie` フィールドに属性を設定します。 +- `c.SetCookie(cookie)` を呼び出して、レスポンスに `Set-Cookie` header を追加します。 + +## 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") +} +``` + +- `c.Cookie("username")` で名前から Cookie を読み取ります。 +- `http.Cookie` フィールドを通じて属性にアクセスします。 + +## すべての Cookie を読む + +```go +func readAllCookies(c *echo.Context) error { + for _, cookie := range c.Cookies() { + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + } + return c.String(http.StatusOK, "read all the cookies") +} +``` diff --git a/site/src/content/docs/ja/guide/customization.md b/site/src/content/docs/ja/guide/customization.md new file mode 100644 index 00000000..5b69f976 --- /dev/null +++ b/site/src/content/docs/ja/guide/customization.md @@ -0,0 +1,64 @@ +--- +title: カスタマイズ +description: Echo の logger、validator、binder、renderer、serializer、エラー処理をカスタマイズします。 +sidebar: + order: 12 +--- + +Echo は `Echo` インスタンス上に一連のフィールドを公開しており、組み込みの挙動を +独自実装に置き換えられます。 + +## ログ + +`Echo#Logger` は構造化ログを書き込みます。デフォルトのハンドラは JSON を +`os.Stdout` に出力します。 + +### カスタム logger + +logger は `*slog.Logger` なので、任意の `slog` ハンドラを登録できます。 + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` はリクエストペイロード検証用の validator を登録します。 + +[詳しく見る](/ja/guide/request/#validate-data) + +## カスタム binder + +`Echo#Binder` はリクエストペイロードをバインドするカスタム binder を登録します。 + +[詳しく見る](/ja/guide/binding/#custom-binder) + +## カスタム JSON serializer + +`Echo#JSONSerializer` はカスタム JSON serializer を登録します。 +[json.go](https://github.com/labstack/echo/blob/master/json.go) の +`DefaultJSONSerializer` を参照してください。 + +## Renderer + +`Echo#Renderer` はテンプレートレンダリング用の renderer を登録します。 + +[詳しく見る](/ja/guide/templates/) + +## HTTP エラーハンドラ + +`Echo#HTTPErrorHandler` はカスタム HTTP エラーハンドラを登録します。 + +[詳しく見る](/ja/guide/error-handling/) + +## ルートコールバック + +`Echo#OnAddRoute` は、新しいルートがルーターに追加されるたびに呼び出される +コールバックを登録します。 + +## IP 抽出器 + +`Echo#IPExtractor` は実際のクライアント IP アドレスをどう判定するかを制御します。 +信頼性と安全性を保って取得するには、アプリケーションがインフラ全体を把握している必要があります。 + +[詳しく見る](/ja/guide/ip-address/) diff --git a/site/src/content/docs/ja/guide/error-handling.md b/site/src/content/docs/ja/guide/error-handling.md new file mode 100644 index 00000000..59a44593 --- /dev/null +++ b/site/src/content/docs/ja/guide/error-handling.md @@ -0,0 +1,82 @@ +--- +title: エラー処理 +description: ハンドラとミドルウェアからエラーを返すことで HTTP エラーを一元処理します。 +sidebar: + order: 6 +--- + +Echo は**一元的な**エラー処理を推奨します。ハンドラとミドルウェアは `error` を返し、 +単一のエラーハンドラがそれを HTTP レスポンスに変換します。これにより、ログ記録と +レスポンス整形を 1 か所にまとめられます。 + +通常の `error` または `*echo.HTTPError` を返します。 + +```go +e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + if !authenticated(c) { + // invalid credentials → abort with 401 + return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials") + } + return next(c) + } +}) +``` + +メッセージなしの `echo.NewHTTPError(code)` はステータステキスト(例: +`"Unauthorized"`)を使います。Echo には `echo.ErrBadRequest`、`echo.ErrNotFound`、 +`echo.ErrUnauthorized` などの sentinel エラーもあります。 + +## デフォルトエラーハンドラ + +Echo のデフォルトハンドラは JSON で応答します。 + +```json +{ "message": "error connecting to redis" } +``` + +通常の `error` は `500 Internal Server Error` になります(エラーを公開して実行している場合は +元のメッセージが含まれます)。`*HTTPError` は自身のステータスコードとメッセージを使います。 + +## カスタムエラーハンドラ + +`e.HTTPErrorHandler` で独自のハンドラを設定します。エラーページ、通知、 +またはエラーを集中管理システムへ送る場合に有用です。 + +`echo.UnwrapResponse()` でレスポンスがすでに送信済みか確認し、`echo.HTTPStatusCoder` +でエラーチェーン内のステータスコードを探します。 + +```go +func customHTTPErrorHandler(c *echo.Context, err error) { + if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + if resp.Committed { + return // already sent by a handler/middleware + } + } + + code := http.StatusInternalServerError + var sc echo.HTTPStatusCoder + if errors.As(err, &sc) { + if tmp := sc.StatusCode(); tmp != 0 { + code = tmp + } + } + + var cErr error + if c.Request().Method == http.MethodHead { + cErr = c.NoContent(code) + } else { + cErr = c.File(fmt.Sprintf("%d.html", code)) // e.g. 404.html, 500.html + } + if cErr != nil { + c.Logger().Error("failed to send error page", "error", errors.Join(err, cErr)) + } +} + +e.HTTPErrorHandler = customHTTPErrorHandler +``` + +:::tip +logger の代わりに、または logger に加えて、中央のハンドラから Sentry、Elasticsearch、 +Splunk などの外部サービスへエラーを転送できます。 +::: diff --git a/site/src/content/docs/ja/guide/installation.md b/site/src/content/docs/ja/guide/installation.md new file mode 100644 index 00000000..e0e776f2 --- /dev/null +++ b/site/src/content/docs/ja/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: インストール +description: Echo v5 を Go module に追加します。 +sidebar: + order: 2 +--- + +Echo は Go module として配布されています:`github.com/labstack/echo/v5`。 + +## 要件 + +Echo v5 には **Go 1.25 以降**が必要です。 + +```bash +go version +``` + +## プロジェクトに追加する + +既存の module 内では次を実行します。 + +```bash +go get github.com/labstack/echo/v5 +``` + +または新しい module を開始します。 + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +コードで import します。 + +```go +import "github.com/labstack/echo/v5" +``` + +## バージョン + +| バージョン | インポートパス | ステータス | +| ---------- | ----------------------------- | ---------- | +| **v5** | `github.com/labstack/echo/v5` | 現行 | +| v4 | `github.com/labstack/echo/v4` | LTS(保守) | + +:::note +Echo は[セマンティックインポートバージョニング](https://go.dev/blog/v2-go-modules)に従います。 +メジャーバージョンはインポートパスの一部なので、移行中に v4 と v5 を共存させられます。 +::: + +## 最新に保つ + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/ja/guide/ip-address.md b/site/src/content/docs/ja/guide/ip-address.md new file mode 100644 index 00000000..fdf710df --- /dev/null +++ b/site/src/content/docs/ja/guide/ip-address.md @@ -0,0 +1,112 @@ +--- +title: IP アドレス +description: プロキシの背後で実際のクライアント IP アドレスを安全に取得します。 +sidebar: + order: 14 +--- + +IP アドレスは HTTP において基本的な役割を持ち、アクセス制御、監査、地理ベースの分析などに +使われます。Echo はそれを取得するために `Context#RealIP()` を提供しています。 + +_実際の_ クライアント IP を取得するのは簡単ではありません。特に L7 プロキシが +アプリケーションの前にある場合はそうです。その場合、実際の IP はプロキシから +HTTP 経由でアプリへ伝えられる必要があります。ただし HTTP header を無条件に信頼してはいけません。 +だまされる可能性があります。**これはセキュリティリスクです。** + +IP を信頼性高く安全に取得するには、アプリケーションがインフラ全体を把握している必要があります。 +Echo では `Echo#IPExtractor` でこれを設定します。 + +:::caution +`Echo#IPExtractor` を明示的に設定しない場合、Echo は従来の挙動にフォールバックします。 +これは安全なデフォルトではありません。 +::: + +適切な方法を見つけるため、まず 2 つの質問から始めます。 + +1. アプリケーションの前に HTTP(L7)プロキシを置いていますか?これにはクラウドの + ロードバランサー(AWS ALB や GCP HTTP LB など)や、オープンソースプロキシ + (Nginx、Envoy、Istio ingress gateway など)が含まれます。 +2. そうであれば、プロキシはどの HTTP header を使ってクライアント IP をアプリケーションへ渡していますか? + +## ケース 1:プロキシなし + +プロキシがない場合(アプリがインターネットに直接向いている場合)、信頼できる唯一のアドレスは +ネットワーク層から得られるものです。クライアントが完全に制御できるため、すべての HTTP header は +信頼できません。 + +`echo.ExtractIPDirect()` を使います。 + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## ケース 2:X-Forwarded-For header を使うプロキシ + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +は、クライアント IP を中継するもっとも一般的な header です。各ホップで、プロキシは +リクエスト IP を header の末尾に追加します。 + +```text + ┌──────────┐ ┌──────────┐ ┌──────────┐ +───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ + │ (IP: b) │ │ (IP: c) │ │ │ + └──────────┘ └──────────┘ └──────────┘ + +Case 1. +XFF: "" "a" "a, b" + ~~~~~~ +Case 2. +XFF: "x" "x, a" "x, a, b" + ~~~~~~~~~ + ↑ What your app will see +``` + +この場合、**右から見て最初の信頼できない IP 読み取り値**を使います。左端の最初の値は、 +クライアントが制御するため絶対に使わないでください。ここで「信頼できる」とは、その IP が +自分のインフラに属していると確信できることです。上の例では `b` と `c` が信頼できるなら、 +どちらのケースでもクライアント IP は `a` であり、`x` ではありません。 + +`ExtractIPFromXFFHeader(...TrustOption)` を使います。 + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +デフォルトでは、loopback、link-local unicast、private-use、 +[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890)、 +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291)、 +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193) の unique local アドレスなど、 +内部 IP アドレスを信頼します。これは `TrustOption` で制御できます。 + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader( + echo.TrustLoopback(false), // e.g. IPv4 starting with 127. + echo.TrustLinkLocal(false), // e.g. IPv4 starting with 169.254. + echo.TrustPrivateNet(false), // e.g. IPv4 starting with 10. or 192.168. + echo.TrustIPRange(lbIPRange), +) +``` + +## ケース 3:X-Real-IP header を使うプロキシ + +`X-Real-IP` はクライアント IP を中継するもう 1 つの header ですが、XFF と異なり +単一のアドレスだけを保持します。 + +プロキシがこの header を設定している場合は、`ExtractIPFromRealIPHeader(...TrustOption)` を使います。 + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +XFF と同様に、デフォルトでは内部 IP アドレスを信頼し、同じ `TrustOption` を受け付けます。 + +:::danger +インフラのエッジにある最外部プロキシを、**受信 header をそのまま通さない**ように設定することを +**絶対に忘れないでください**。そうしないと、クライアントがそれらを偽造でき、不正への道を開きます。 +::: + +## デフォルトの挙動 + +デフォルトでは、Echo は最初の XFF header、X-Real-IP header、ネットワーク層の IP を同時に考慮します。 + +この記事から分かるように、これは良い選択ではありません。後方互換性のためだけにデフォルトとして残されています。 diff --git a/site/src/content/docs/ja/guide/quickstart.md b/site/src/content/docs/ja/guide/quickstart.md new file mode 100644 index 00000000..fa52bc41 --- /dev/null +++ b/site/src/content/docs/ja/guide/quickstart.md @@ -0,0 +1,76 @@ +--- +title: クイックスタート +description: 5 分以内に本番対応の Echo API を構築します。 +sidebar: + order: 1 +--- + +Echo は高性能でミニマルな Go Web フレームワークです。このガイドでは、5 分以内に +サーバーを起動します。 + +## 要件 + +Echo には **Go 1.25 以降**が必要です。バージョンを確認してください。 + +```bash +go version +``` + +## インストール + +module を作成し、Echo を追加します。 + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +`main.go` を作成します。 + +```go +package main + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"message": "Hello, World!"}) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +実行します。 + +```bash +go run main.go +``` + +サーバーは `http://localhost:1323` で動作しています。Echo のルーターは、 +ルートごとのリクエストディスパッチを**動的メモリ割り当てゼロ**で行います。 + +:::tip[Ask Echo] +困ったら右下の **Ask Echo** ボタンを押して +*"How do I add JWT auth?"* と質問してください。回答はこのドキュメントから直接得られます。 +::: + +## 次のステップ + +- [ルーティング](/ja/guide/routing/):静的、パラメーター付き、ワイルドカードのルート。 +- [コンテキスト](/ja/guide/context/):リクエストごとのリクエスト/レスポンスオブジェクト。 +- [バインディング](/ja/guide/binding/):リクエストデータを型付き struct に解析します。 diff --git a/site/src/content/docs/ja/guide/request.md b/site/src/content/docs/ja/guide/request.md new file mode 100644 index 00000000..1f0fc2f1 --- /dev/null +++ b/site/src/content/docs/ja/guide/request.md @@ -0,0 +1,170 @@ +--- +title: リクエスト +description: リクエストからフォーム、クエリ、パスのデータを取得し、検証します。 +sidebar: + order: 7 +--- + +ハンドラは `echo.Context` を通じてリクエストデータを読み取ります。Echo は値を名前で個別に取得し、 +struct にバインドし([バインディング](/ja/guide/binding/)を参照)、登録した validator に +検証を委ねられます。 + +## データを取得する + +### フォームデータ + +`Context#FormValue(name string)` でフォームフィールドを名前から取得します。 + +```go +e.POST("/form", func(c *echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +`string` 以外の型には、ジェネリック関数 `echo.FormValue[T]` を使います。 + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +次でテストします。 + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +カスタムデータ型をバインドするには、`echo.BindUnmarshaler` インターフェイスを実装します。 + +```go +type Timestamp time.Time + +func (t *Timestamp) UnmarshalParam(src string) error { + ts, err := time.Parse(time.RFC3339, src) + if err != nil { + return err + } + *t = Timestamp(ts) + return nil +} +``` + +### クエリパラメーター + +`Context#QueryParam(name string)` でクエリパラメーターを名前から取得します。 + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +`string` 以外の型には、ジェネリック関数 `echo.QueryParam[T]` を使います。 + +```go +age, err := echo.QueryParam[int](c, "age") +if err != nil { + return err +} +``` + +```sh +curl -X GET "http://localhost:1323?name=Joe&age=30" +``` + +### パスパラメーター + +`Context#Param(name string)` で登録済みのパスパラメーターを名前から取得します。 + +```go +e.GET("/users/:name", func(c *echo.Context) error { + name := c.Param("name") + return c.String(http.StatusOK, name) +}) +``` + +`string` 以外の型には、ジェネリック関数 `echo.PathParam[T]` を使います。 + +```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 +``` + +### データをバインドする + +Echo はリクエストデータをネイティブの Go struct や変数にもバインドできます。 +[バインディング](/ja/guide/binding/)を参照してください。 + +## データを検証する + +Echo には組み込みのデータ検証はありません。`Echo#Validator` でカスタム validator を登録し、 +[go-playground/validator](https://github.com/go-playground/validator) などのサードパーティライブラリを +使えます。 + +次の例は、バインド済み struct を検証します。 + +```go +package main + +import ( + "net/http" + + "github.com/go-playground/validator/v10" // go get github.com/go-playground/validator/v10 + "github.com/labstack/echo/v5" +) + +type CustomValidator struct { + validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i any) error { + if err := cv.validator.Struct(i); err != nil { + // Optionally return the error to let each route control the status code. + return echo.ErrBadRequest.Wrap(err) + } + return nil +} + +type User struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` +} + +func main() { + e := echo.New() + e.Validator = &CustomValidator{validator: validator.New()} + + e.POST("/users", func(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + if err := c.Validate(u); err != nil { + return err + } + return c.JSON(http.StatusOK, u) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +```sh +curl -X POST http://localhost:1323/users \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe","email":"joe@invalid-domain"}' +{"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"} +``` diff --git a/site/src/content/docs/ja/guide/response.md b/site/src/content/docs/ja/guide/response.md new file mode 100644 index 00000000..42611b35 --- /dev/null +++ b/site/src/content/docs/ja/guide/response.md @@ -0,0 +1,318 @@ +--- +title: レスポンス +description: 文字列、HTML、JSON、XML、ファイル、ストリーム、リダイレクト、レスポンスフックを送信します。 +sidebar: + order: 8 +--- + +ハンドラは `echo.Context` を通じてレスポンスを書き込みます。各ヘルパーは適切な +`Content-Type` とステータスコードを設定します。 + +## 文字列を送信する + +`Context#String(code int, s string)` は、ステータスコード付きのプレーンテキストレスポンスを送信します。 + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## HTML を送信する + +`Context#HTML(code int, html string)` は、ステータスコード付きのシンプルな HTML レスポンスを送信します。 +HTML を動的に生成するには、[テンプレート](/ja/guide/templates/)を参照してください。 + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### HTML blob を送信する + +`Context#HTMLBlob(code int, b []byte)` は、ステータスコード付きの HTML blob を送信します。 +`[]byte` を出力するテンプレートエンジンと組み合わせると便利です。 + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## テンプレートをレンダリングする + +[テンプレート](/ja/guide/templates/)を参照してください。 + +## JSON を送信する + +`Context#JSON(code int, i any)` は Go の値を JSON としてエンコードし、ステータスコード付きで送信します。 + +```go +type User struct { + Name string `json:"name" xml:"name"` + Email string `json:"email" xml:"email"` +} + +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSON(http.StatusOK, u) +} +``` + +### JSON をストリーミングする + +`Context#JSON()` は内部で `json.Marshal` を使うため、大きなペイロードでは非効率な場合があります。 +その場合は JSON を直接ストリーミングします。 + +```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)` は整形済み JSON レスポンスを送信します。 +インデントにはスペースまたはタブを使えます。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSONPretty(http.StatusOK, u, " ") +} +``` + +```json +{ + "email": "jon@labstack.com", + "name": "Jon" +} +``` + +### JSON blob + +`Context#JSONBlob(code int, b []byte)` は、たとえばデータベース由来の +事前エンコード済み JSON blob を直接送信します。 + +```go +func(c *echo.Context) error { + encodedJSON := []byte{} // Encoded JSON from an external source. + return c.JSONBlob(http.StatusOK, encodedJSON) +} +``` + +## JSONP を送信する + +`Context#JSONP(code int, callback string, i any)` は Go の値を JSON としてエンコードし、 +指定した callback でラップした JSONP ペイロードとして送信します。 + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +[JSONP cookbook](/ja/cookbook/jsonp/)を参照してください。 + +## XML を送信する + +`Context#XML(code int, i any)` は Go の値を XML としてエンコードし、ステータスコード付きで送信します。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XML(http.StatusOK, u) +} +``` + +### XML をストリーミングする + +`Context#XML` は内部で `xml.Marshal` を使うため、大きなペイロードでは非効率な場合があります。 +その場合は XML を直接ストリーミングします。 + +```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)` は整形済み XML レスポンスを送信します。 +インデントにはスペースまたはタブを使えます。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XMLPretty(http.StatusOK, u, " ") +} +``` + +```xml + + + Jon + jon@labstack.com + +``` + +:::tip +リクエスト URL のクエリ文字列に `pretty` を追加すると、`Context#XML()` でも +整形済み XML を出力できます。 + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### XML blob + +`Context#XMLBlob(code int, b []byte)` は、たとえばデータベース由来の +事前エンコード済み XML blob を直接送信します。 + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## ファイルを送信する + +`Context#File(file string)` はファイルの内容をレスポンスとして送信します。 +正しいコンテンツタイプを設定し、キャッシュも自動的に処理します。 + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## 添付ファイルを送信する + +`Context#Attachment(file, name string)` は `File()` と似ていますが、 +`Content-Disposition: attachment` と指定した名前でファイルを送信します。 + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## インライン送信する + +`Context#Inline(file, name string)` は `File()` と似ていますが、 +`Content-Disposition: inline` と指定した名前でファイルを送信します。 + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## blob を送信する + +`Context#Blob(code int, contentType string, b []byte)` は、指定したコンテンツタイプと +ステータスコードで任意のデータを送信します。 + +```go +func(c *echo.Context) error { + data := []byte(`0306703,0035866,NO_ACTION,06/19/2006 +0086003,"0005866",UPDATED,06/19/2006`) + return c.Blob(http.StatusOK, "text/csv", data) +} +``` + +## ストリームを送信する + +`Context#Stream(code int, contentType string, r io.Reader)` は、指定したコンテンツタイプ、 +`io.Reader`、ステータスコードで任意のデータストリームを送信します。 + +```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) +} +``` + +## ボディなしで送信する + +`Context#NoContent(code int)` は、ステータスコード付きの空ボディを送信します。 + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## リクエストをリダイレクトする + +`Context#Redirect(code int, url string)` は、指定した URL とステータスコードでリクエストをリダイレクトします。 + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## フック + +### レスポンス前 + +`Response#Before(func())` は、レスポンスが書き込まれる直前に実行される関数を登録します。 + +### レスポンス後 + +`Response#After(func())` は、レスポンスが書き込まれた直後に実行される関数を登録します。 +`Content-Length` が不明な場合、after 関数は実行されません。 + +```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 +複数の `Before` 関数と `After` 関数を登録できます。 +::: diff --git a/site/src/content/docs/ja/guide/routing.md b/site/src/content/docs/ja/guide/routing.md new file mode 100644 index 00000000..1da2c349 --- /dev/null +++ b/site/src/content/docs/ja/guide/routing.md @@ -0,0 +1,77 @@ +--- +title: ルーティング +description: Echo のゼロ割り当て radix tree でリクエスト URL をハンドラにマッチさせます。 +sidebar: + order: 3 +--- + +Echo の最適化されたルーターは、radix tree を使ってリクエスト URL をハンドラへマッチさせ、 +**動的メモリ割り当てゼロ**とスマートなルート優先順位を実現します。 + +## ルートを登録する + +`Echo` インスタンス上の HTTP メソッドヘルパーを使います。各ヘルパーはパスパターンと +`HandlerFunc`(`func(c *echo.Context) error`)を受け取り、任意でルートレベルの +ミドルウェアも受け取れます。 + +```go +e := echo.New() + +e.GET("/users/:id", getUser) // named parameter +e.POST("/users", createUser) +e.PUT("/users/:id", updateUser) +e.DELETE("/users/:id", deleteUser) +e.GET("/static/*", serveFiles) // wildcard +``` + +`Any` はサポートされているすべてのメソッドにハンドラを登録し、`Match` は指定した集合に登録します。 + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## マッチ種別 + +| パターン | 種別 | マッチ例 | +| ------------------ | ------------ | -------------------------- | +| `/users/profile` | 静的 | `/users/profile` | +| `/users/:id` | パラメーター | `/users/42` | +| `/static/*` | ワイルドカード | `/static/css/app.css` | + +:::note +優先順位は**静的 → パラメーター → ワイルドカード**です。そのため +`/users/profile` は常に `/users/:id` に勝ち、`/users/:id` は `/users/*` に勝ちます。 +::: + +## パスパラメーター + +`c.Param()` でコンテキストから名前付きパラメーターを読み取ります +(デフォルト値には `c.ParamOr()` を使います)。 + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +ワイルドカードセグメントは `*` パラメーターとして取得できます。 + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## グループ + +`e.Group()` を使い、プレフィックスとミドルウェアを共有するルートをグループ化します。 + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +グループはネストでき、より大きなルートツリーを構成できます。 diff --git a/site/src/content/docs/ja/guide/static-files.md b/site/src/content/docs/ja/guide/static-files.md new file mode 100644 index 00000000..83d69779 --- /dev/null +++ b/site/src/content/docs/ja/guide/static-files.md @@ -0,0 +1,88 @@ +--- +title: 静的ファイルの配信 +description: Echo で画像、JavaScript、CSS、フォント、その他のアセットを配信します。 +sidebar: + order: 9 +--- + +Echo はファイルシステムまたは埋め込みファイルシステムから、画像、JavaScript、CSS、 +PDF、フォントなどの静的アセットを配信できます。 + +## デフォルトファイルシステム + +Echo は現在の作業ディレクトリをルートにした `os.DirFS(".")` をデフォルトファイルシステムとして使います。 +変更するには `Echo#Filesystem` フィールドを設定します。 + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## Static ミドルウェアを使う + +[Static ミドルウェア](/ja/middleware/static/)を参照してください。 + +## Echo#Static() を使う + +`Echo#Static(prefix, root string)` は、指定した root ディレクトリからパス prefix 配下の +静的ファイルを配信するルートを登録します。 + +`assets` から `/static/*` 配下で任意のファイルを配信します。`/static/js/main.js` へのリクエストは +`assets/js/main.js` を配信します。 + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +`assets` から `/*` 配下で任意のファイルを配信します。`/js/main.js` へのリクエストは +`assets/js/main.js` を配信します。 + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## Echo#StaticFS() を使う + +静的ファイルは `embed.FS` を含む任意の `fs.FS` から配信できます。配信されるファイルが +正しいサブディレクトリをルートにするよう、`echo.MustSubFS` を使います。`embed.FS` は +サブディレクトリもそれぞれのエントリとして含みます。 + +```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) + } +} +``` + +## Echo#File() を使う + +`Echo#File(path, file string)` は、単一の静的ファイルを配信するルートを登録します。 + +`public/index.html` からインデックスページを配信します。 + +```go +e.File("/", "public/index.html") +``` + +`app/assets/favicon.ico` から favicon を配信します。 + +```go +e := echo.New() +e.Filesystem = os.DirFS("/") +e.File("/favicon.ico", "app/assets/favicon.ico") // The file path must not have a leading slash. +``` + +:::caution +ファイルパス先頭の `/` は、ほとんどの `fs.FS` 実装では機能しません。相対パスを使ってください。 +::: diff --git a/site/src/content/docs/ja/guide/templates.md b/site/src/content/docs/ja/guide/templates.md new file mode 100644 index 00000000..4ae9cf26 --- /dev/null +++ b/site/src/content/docs/ja/guide/templates.md @@ -0,0 +1,132 @@ +--- +title: テンプレート +description: renderer を登録し、任意のエンジンで HTML テンプレートをレンダリングします。 +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` はデータでテンプレートをレンダリングし、 +ステータスコード付きの `text/html` レスポンスを送信します。`Echo#Renderer` を設定して +renderer を登録すると、任意のテンプレートエンジンを使えます。 + +## レンダリング + +次の例では Go の `html/template` を使います。 + +デフォルトのテンプレート renderer を使います。 + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +または `echo.Renderer` インターフェイスを自分で実装します。 + +```go +type Template struct { + templates *template.Template +} + +func (t *Template) Render(c *echo.Context, w io.Writer, name string, data any) error { + return t.templates.ExecuteTemplate(w, name, data) +} +``` + +1. テンプレートを事前コンパイルします。 + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. renderer を登録します。 + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. ハンドラ内でテンプレートをレンダリングします。 + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## 応用:テンプレートから Echo を呼び出す + +テンプレート内から `Echo#Reverse` を呼び出して URI を生成したい場合があります。 +Go の `html/template` はこれに最適というわけではありませんが、2 つの方法で実現できます。 +テンプレートに渡すすべてのオブジェクトに共通メソッドを用意するか、`map[string]any` を渡して +カスタム renderer 内で拡張する方法です。後者の方が柔軟です。完全な例を示します。 + +`template.html`: + +```html + + +

Hello {{index . "name"}}

+ +

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

+ + +``` + +`server.go`: + +```go +package main + +import ( + "html/template" + "io" + "net/http" + + "github.com/labstack/echo/v5" +) + +// TemplateRenderer is a custom html/template renderer for Echo. +type TemplateRenderer struct { + templates *template.Template +} + +// Render renders a template document. +func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error { + // Add global methods if the data is a map. + if viewContext, isMap := data.(map[string]any); isMap { + viewContext["reverse"] = c.RouteInfo().Reverse + } + + return t.templates.ExecuteTemplate(w, name, data) +} + +func main() { + e := echo.New() + e.Renderer = &TemplateRenderer{ + templates: template.Must(template.ParseGlob("main/*.html")), + } + + e.GET("/something/:name", func(c *echo.Context) error { + return c.Render(http.StatusOK, "template.html", map[string]any{ + "name": "Dolly!", + }) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("shutting down the server", "error", err) + } +} +``` diff --git a/site/src/content/docs/ja/guide/testing.md b/site/src/content/docs/ja/guide/testing.md new file mode 100644 index 00000000..8e146cda --- /dev/null +++ b/site/src/content/docs/ja/guide/testing.md @@ -0,0 +1,264 @@ +--- +title: テスト +description: httptest と echotest ヘルパーでハンドラとミドルウェアをテストします。 +sidebar: + order: 13 +--- + +Echo のハンドラとミドルウェアは `echo.Context` を扱う通常の関数なので、標準の +`net/http/httptest` パッケージで簡単にテストできます。`echotest` パッケージは、 +定型コードを減らすヘルパーを提供します。 + +## ハンドラをテストする + +2 つのハンドラを考えます。 + +**CreateUser**:`POST /users` + +- JSON ペイロードを受け取ります。 +- 成功時に `201 Created` を返します。 +- エラー時に `500 Internal Server Error` を返します。 + +**GetUser**:`GET /users/:email` + +- 成功時に `200 OK` を返します。 +- ユーザーが存在しない場合は `404 Not Found`、それ以外のエラーでは + `500 Internal Server Error` を返します。 + +`handler.go`: + +```go +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v5" +) + +type ( + User struct { + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + } + handler struct { + db map[string]*User + } +) + +func (h *handler) createUser(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + return c.JSON(http.StatusCreated, u) +} + +func (h *handler) getUser(c *echo.Context) error { + email := c.Param("email") + user := h.db[email] + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + return c.JSON(http.StatusOK, user) +} +``` + +`handler_test.go`: + +```go +package handler + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/echotest" + "github.com/stretchr/testify/assert" +) + +var ( + mockDB = map[string]*User{ + "jon@labstack.com": {Name: "Jon Snow", Email: "jon@labstack.com"}, + } + userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}` +) + +func TestCreateUser(t *testing.T) { + // Setup + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### echotest ヘルパーを使う + +`echotest.ContextConfig` は、リクエストの宣言的な記述からコンテキスト(と recorder)を構築します。 + +```go +// Same test as above, using echotest. +func TestCreateUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} + +// Even shorter, using ServeWithHandler. +func TestCreateUserWithServeHandler(t *testing.T) { + h := &handler{mockDB} + + rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ServeWithHandler(t, h.createUser) + + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) +} + +func TestGetUser(t *testing.T) { + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }, + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.getUser(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### フォームペイロードを使う + +```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) +``` + +`echotest` を使った multipart フォームペイロード: + +```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) +} +``` + +### パスパラメーターを設定する + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### クエリパラメーターを設定する + +```go +// import "net/url" +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) +``` + +## ミドルウェアをテストする + +```go +func TestMiddleware(t *testing.T) { + handler := func(c *echo.Context) error { + return c.JSON(http.StatusTeapot, fmt.Sprintf("email: %s", c.Param("email"))) + } + middleware := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("user_id", int64(1234)) + return next(c) + } + } + + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{{Name: "email", Value: "jon@labstack.com"}}, + }.ToContextRecorder(t) + + if err := middleware(handler)(c); err != nil { + t.Fatal(err) + } + + // Check that the middleware set the value. + userID, err := echo.ContextGet[int64](c, "user_id") + assert.NoError(t, err) + assert.Equal(t, int64(1234), userID) + + // Check that the handler returned the correct response. + assert.Equal(t, http.StatusTeapot, rec.Code) +} +``` + +:::tip +その他の例は、Echo ソース内の +[ミドルウェアテストケース](https://github.com/labstack/echo/tree/master/middleware)を参照してください。 +::: diff --git a/site/src/content/docs/zh-cn/guide/binding.md b/site/src/content/docs/zh-cn/guide/binding.md new file mode 100644 index 00000000..e6eb1ef7 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/binding.md @@ -0,0 +1,149 @@ +--- +title: 绑定 +description: 从路径、查询、header 和请求体将请求数据解析到带类型的 Go struct 中。 +sidebar: + order: 5 +--- + +解析请求数据是 Web 应用的重要组成部分。在 Echo 中这称为 +_绑定_,它可以从 HTTP 请求的四个部分读取数据: + +- URL 路径参数 +- URL 查询参数 +- Header +- 请求体 + +## Struct 标签绑定 + +定义一个带标签的 struct 来指定数据来源和键,然后把它的指针传给 `c.Bind()`。 +这里查询参数 `id` 会绑定到 `ID` 字段: + +```go +type User struct { + ID string `query:"id"` +} + +// handler for /users?id= +var user User +if err := c.Bind(&user); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +``` + +### 数据来源 + +| 标签 | 来源 | +| -------- | ---- | +| `query` | 查询参数 | +| `param` | 路径参数 | +| `header` | Header 值 | +| `form` | 表单数据(查询 + 请求体) | +| `json` | 请求体(`encoding/json`) | +| `xml` | 请求体(`encoding/xml`) | + +路径、查询、header 和表单字段都需要**显式标签**。JSON 和 XML 在省略标签时会回退到 +struct 字段名,这与标准库的行为一致。 + +### 请求体内容类型 + +解码请求体时,`Content-Type` header 会选择对应的解码器: + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### 多个来源和优先级 + +一个字段可以声明多个来源。数据会按下面的顺序绑定,每一步都会覆盖前一步的值: + +1. 路径参数 +2. 查询参数(仅 GET / DELETE) +3. 请求体 + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### 从单一来源直接绑定 + +```go +echo.BindBody(c, &payload) // request body +echo.BindQueryParams(c, &payload) // query parameters +echo.BindPathValues(c, &payload) // path parameters +echo.BindHeaders(c, &payload) // headers +``` + +:::note +Header **不会**包含在 `c.Bind()` 中。请直接使用 `echo.BindHeaders` 绑定它们。 +::: + +:::caution[安全] +不要直接绑定到业务 struct。如果被绑定的 struct 暴露了 `IsAdmin bool` 字段, +请求体 `{"IsAdmin": true}` 就会设置它。请使用专用 DTO,并显式映射: +::: + +```go +type UserDTO struct { + Name string `json:"name" form:"name" query:"name"` + Email string `json:"email" form:"email" query:"email"` +} + +e.POST("/users", func(c *echo.Context) error { + var dto UserDTO + if err := c.Bind(&dto); err != nil { + return c.String(http.StatusBadRequest, "bad request") + } + user := User{Name: dto.Name, Email: dto.Email, IsAdmin: false} + executeSomeBusinessLogic(user) + return c.JSON(http.StatusOK, user) +}) +``` + +## 流式绑定 + +如果要从单一来源进行显式且类型安全的绑定,请使用流式 binder。它们会链式配置并执行, +同时收集错误: + +```go +// /api/search?active=true&id=1&id=2&id=3&length=25 +var opts struct { + IDs []int64 + Active bool +} +length := int64(50) + +err := echo.QueryParamsBinder(c). + Int64("length", &length). + Int64s("id", &opts.IDs). + Bool("active", &opts.Active). + BindError() // first error, if any +``` + +可用的 binder:`echo.QueryParamsBinder(c)`、`echo.PathValuesBinder(c)`、 +`echo.FormFieldBinder(c)`。用 `BindError()`(第一个错误)或 `BindErrors()`(所有错误) +结束链式调用。`FailFast(false)` 会运行完整链;默认启用快速失败。 + +每个支持的类型都提供 `Type(...)`、`MustType(...)`、`Types(...)`(切片)和 +`MustTypes(...)` 方法,例如 `Int64`、`MustInt64`、`Int64s`。使用 +`BindWithDelimiter("id", &dest, ",")` 可拆分用逗号连接的值。 + +## 自定义 binder + +通过 `Echo#Binder` 注册自定义 binder: + +```go +type CustomBinder struct{} + +func (cb *CustomBinder) Bind(c *echo.Context, i any) error { + db := new(echo.DefaultBinder) + if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType { + return err + } + // custom logic here + return nil +} + +e.Binder = &CustomBinder{} +``` diff --git a/site/src/content/docs/zh-cn/guide/context.md b/site/src/content/docs/zh-cn/guide/context.md new file mode 100644 index 00000000..01491d15 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/context.md @@ -0,0 +1,77 @@ +--- +title: 上下文 +description: 每个请求对应的对象,携带请求、响应、参数和辅助方法。 +sidebar: + order: 4 +--- + +`echo.Context` 表示当前 HTTP 请求的上下文。每个处理函数和中间件都会收到它的指针 +(`*echo.Context`),其中包含请求和响应、路径参数、已绑定数据,以及用于构建响应的辅助方法。 + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## 读取输入 + +```go +id := c.Param("id") // path parameter +q := c.QueryParam("q") // query string value +all := c.QueryParams() // url.Values of all query params +name := c.FormValue("name") // form field (URL + body) +ua := c.Request().Header.Get(echo.HeaderUserAgent) +``` + +也有对应的 `*Or` 辅助方法,在值不存在时返回默认值,例如 +`c.ParamOr("id", "0")`、`c.QueryParamOr("page", "1")`、`c.FormValueOr(...)`。 + +## 写入响应 + +```go +c.String(http.StatusOK, "plain text") +c.JSON(http.StatusOK, payload) +c.JSONPretty(http.StatusOK, payload, " ") +c.HTML(http.StatusOK, "hi") +c.XML(http.StatusOK, payload) +c.Blob(http.StatusOK, "application/pdf", bytes) +c.Stream(http.StatusOK, "application/octet-stream", reader) +c.NoContent(http.StatusNoContent) +c.Redirect(http.StatusFound, "/elsewhere") +``` + +## 文件 + +```go +c.File("public/report.pdf") // serve a file +c.Attachment("invoice.pdf", "inv.pdf") // prompt download +c.Inline("photo.png", "photo.png") // render inline +``` + +## 每个请求的存储 + +使用 `Get`/`Set` 在中间件和处理函数之间共享数据: + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +也可以通过泛型辅助方法进行带类型访问: + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## 绑定和验证 + +`c.Bind()` 会把请求数据解析到 struct 中;参见[绑定](/zh-cn/guide/binding/)。 + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/zh-cn/guide/cookies.md b/site/src/content/docs/zh-cn/guide/cookies.md new file mode 100644 index 00000000..d7b8f6b2 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/cookies.md @@ -0,0 +1,70 @@ +--- +title: Cookie +description: 使用标准 http.Cookie 类型创建、读取和列出 HTTP Cookie。 +sidebar: + order: 11 +--- + +Cookie 是服务器发送给浏览器的一小段数据,浏览器会保存它,并在后续请求中发回。 +Cookie 让网站可以记住购物车、认证状态或之前输入的表单值等有状态信息。 + +Echo 使用 Go 标准库的 `http.Cookie` 类型,在处理函数中通过 `echo.Context` +添加和获取 Cookie。 + +## Cookie 属性 + +| 属性 | 可选 | +| ---------- | ---- | +| `Name` | 否 | +| `Value` | 否 | +| `Path` | 是 | +| `Domain` | 是 | +| `Expires` | 是 | +| `Secure` | 是 | +| `HttpOnly` | 是 | + +## 创建 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") +} +``` + +- 使用 `new(http.Cookie)` 创建 Cookie。 +- 在 `http.Cookie` 字段上设置属性。 +- 调用 `c.SetCookie(cookie)`,向响应添加 `Set-Cookie` header。 + +## 读取 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") +} +``` + +- 使用 `c.Cookie("username")` 按名称读取 Cookie。 +- 通过 `http.Cookie` 字段访问其属性。 + +## 读取所有 Cookie + +```go +func readAllCookies(c *echo.Context) error { + for _, cookie := range c.Cookies() { + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + } + return c.String(http.StatusOK, "read all the cookies") +} +``` diff --git a/site/src/content/docs/zh-cn/guide/customization.md b/site/src/content/docs/zh-cn/guide/customization.md new file mode 100644 index 00000000..6af00293 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/customization.md @@ -0,0 +1,61 @@ +--- +title: 自定义 +description: 自定义 Echo 的 logger、validator、binder、renderer、serializer 和错误处理。 +sidebar: + order: 12 +--- + +Echo 在 `Echo` 实例上暴露了一组字段,让你可以用自己的实现替换内置行为。 + +## 日志 + +`Echo#Logger` 写入结构化日志。默认处理函数会把 JSON 输出到 `os.Stdout`。 + +### 自定义 logger + +logger 是一个 `*slog.Logger`,因此你可以注册任何 `slog` 处理函数: + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` 注册用于请求负载验证的 validator。 + +[了解更多](/zh-cn/guide/request/#validate-data) + +## 自定义 binder + +`Echo#Binder` 注册用于绑定请求负载的自定义 binder。 + +[了解更多](/zh-cn/guide/binding/#custom-binder) + +## 自定义 JSON serializer + +`Echo#JSONSerializer` 注册自定义 JSON serializer。参见 +[json.go](https://github.com/labstack/echo/blob/master/json.go) 中的 +`DefaultJSONSerializer`。 + +## Renderer + +`Echo#Renderer` 注册用于模板渲染的 renderer。 + +[了解更多](/zh-cn/guide/templates/) + +## HTTP 错误处理函数 + +`Echo#HTTPErrorHandler` 注册自定义 HTTP 错误处理函数。 + +[了解更多](/zh-cn/guide/error-handling/) + +## 路由回调 + +`Echo#OnAddRoute` 注册一个回调,每当新路由被添加到路由器时都会调用。 + +## IP 提取器 + +`Echo#IPExtractor` 控制如何确定真实客户端 IP 地址。要可靠且安全地获取它, +你的应用必须了解整个基础设施。 + +[了解更多](/zh-cn/guide/ip-address/) diff --git a/site/src/content/docs/zh-cn/guide/error-handling.md b/site/src/content/docs/zh-cn/guide/error-handling.md new file mode 100644 index 00000000..e662ad69 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/error-handling.md @@ -0,0 +1,81 @@ +--- +title: 错误处理 +description: 通过从处理函数和中间件返回错误来集中处理 HTTP 错误。 +sidebar: + order: 6 +--- + +Echo 提倡**集中式**错误处理:处理函数和中间件返回 `error`,由单一错误处理函数将其转换为 +HTTP 响应。这样可以把日志记录和响应格式化放在同一个位置。 + +返回普通 `error` 或 `*echo.HTTPError`: + +```go +e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + if !authenticated(c) { + // invalid credentials → abort with 401 + return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials") + } + return next(c) + } +}) +``` + +没有消息的 `echo.NewHTTPError(code)` 会使用状态文本(例如 `"Unauthorized"`)。 +Echo 还提供 `echo.ErrBadRequest`、`echo.ErrNotFound` 和 `echo.ErrUnauthorized` +等哨兵错误。 + +## 默认错误处理函数 + +Echo 的默认处理函数以 JSON 响应: + +```json +{ "message": "error connecting to redis" } +``` + +普通 `error` 会变成 `500 Internal Server Error`(在暴露错误运行时会包含原始消息)。 +`*HTTPError` 会使用自身的状态码和消息。 + +## 自定义错误处理函数 + +通过 `e.HTTPErrorHandler` 设置你自己的处理函数,这适用于错误页面、通知, +或把错误发送到集中式系统。 + +使用 `echo.UnwrapResponse()` 检查响应是否已经发送,并通过 `echo.HTTPStatusCoder` +在错误链中查找状态码: + +```go +func customHTTPErrorHandler(c *echo.Context, err error) { + if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + if resp.Committed { + return // already sent by a handler/middleware + } + } + + code := http.StatusInternalServerError + var sc echo.HTTPStatusCoder + if errors.As(err, &sc) { + if tmp := sc.StatusCode(); tmp != 0 { + code = tmp + } + } + + var cErr error + if c.Request().Method == http.MethodHead { + cErr = c.NoContent(code) + } else { + cErr = c.File(fmt.Sprintf("%d.html", code)) // e.g. 404.html, 500.html + } + if cErr != nil { + c.Logger().Error("failed to send error page", "error", errors.Join(err, cErr)) + } +} + +e.HTTPErrorHandler = customHTTPErrorHandler +``` + +:::tip +除了 logger 之外(或同时),也可以从集中处理函数把错误转发到 Sentry、Elasticsearch +或 Splunk 等外部服务。 +::: diff --git a/site/src/content/docs/zh-cn/guide/installation.md b/site/src/content/docs/zh-cn/guide/installation.md new file mode 100644 index 00000000..7e14b927 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: 安装 +description: 将 Echo v5 添加到你的 Go module。 +sidebar: + order: 2 +--- + +Echo 以 Go module 的形式发布:`github.com/labstack/echo/v5`。 + +## 要求 + +Echo v5 需要 **Go 1.25 或更新版本**。 + +```bash +go version +``` + +## 添加到项目 + +在现有 module 中: + +```bash +go get github.com/labstack/echo/v5 +``` + +或者启动一个新 module: + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +在代码中导入它: + +```go +import "github.com/labstack/echo/v5" +``` + +## 版本 + +| 版本 | 导入路径 | 状态 | +| ---- | ----------------------------- | ---- | +| **v5** | `github.com/labstack/echo/v5` | 当前 | +| v4 | `github.com/labstack/echo/v4` | LTS(维护) | + +:::note +Echo 遵循[语义化导入版本](https://go.dev/blog/v2-go-modules): +主版本是导入路径的一部分,因此 v4 和 v5 可以在迁移期间共存。 +::: + +## 保持最新 + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/zh-cn/guide/ip-address.md b/site/src/content/docs/zh-cn/guide/ip-address.md new file mode 100644 index 00000000..2e74e20f --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/ip-address.md @@ -0,0 +1,105 @@ +--- +title: IP 地址 +description: 在代理之后安全地获取真实客户端 IP 地址。 +sidebar: + order: 14 +--- + +IP 地址在 HTTP 中扮演基础角色,它用于访问控制、审计、基于地理位置的分析等。 +Echo 暴露 `Context#RealIP()` 来获取它。 + +获取_真实_客户端 IP 并不简单,尤其是当 L7 代理位于应用前方时。在这种情况下, +真实 IP 必须由代理通过 HTTP 转发给你的应用,但你不能无条件信任 HTTP header, +否则就可能被欺骗。**这是一个安全风险。** + +要可靠且安全地获取 IP,你的应用必须了解整个基础设施。在 Echo 中,你通过 +`Echo#IPExtractor` 配置这一点。 + +:::caution +如果没有显式设置 `Echo#IPExtractor`,Echo 会回退到旧行为,而这不是安全的默认值。 +::: + +从两个问题开始,找到正确方法: + +1. 你的应用前面是否有任何 HTTP(L7)代理?这包括云负载均衡器(如 AWS ALB 或 GCP HTTP LB) + 和开源代理(如 Nginx、Envoy 或 Istio ingress gateway)。 +2. 如果有,这些代理使用哪个 HTTP header 将客户端 IP 传递给应用? + +## 情况 1:没有代理 + +如果没有代理(应用直接面向互联网),唯一可以信任的地址来自网络层。每个 HTTP header 都不可信, +因为客户端可以完全控制它们。 + +使用 `echo.ExtractIPDirect()`: + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## 情况 2:使用 X-Forwarded-For header 的代理 + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +是转发客户端 IP 最常见的 header。每经过一跳,代理都会把请求 IP 追加到 header 末尾。 + +```text + ┌──────────┐ ┌──────────┐ ┌──────────┐ +───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ + │ (IP: b) │ │ (IP: c) │ │ │ + └──────────┘ └──────────┘ └──────────┘ + +Case 1. +XFF: "" "a" "a, b" + ~~~~~~ +Case 2. +XFF: "x" "x, a" "x, a, b" + ~~~~~~~~~ + ↑ What your app will see +``` + +在这种情况下,应从右侧开始取得**第一个不可信的 IP 读数**。永远不要取最左边的第一个, +因为客户端控制它。这里“可信”表示你确定该 IP 属于你的基础设施。在上面的例子中, +如果 `b` 和 `c` 可信,则两种情况下客户端 IP 都是 `a`,绝不是 `x`。 + +使用 `ExtractIPFromXFFHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +默认情况下,它信任内部 IP 地址,包括 loopback、link-local unicast、private-use, +以及来自 [RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890)、 +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291) 和 +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193) 的 unique local 地址。 +可用 `TrustOption` 控制这一点: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader( + echo.TrustLoopback(false), // e.g. IPv4 starting with 127. + echo.TrustLinkLocal(false), // e.g. IPv4 starting with 169.254. + echo.TrustPrivateNet(false), // e.g. IPv4 starting with 10. or 192.168. + echo.TrustIPRange(lbIPRange), +) +``` + +## 情况 3:使用 X-Real-IP header 的代理 + +`X-Real-IP` 是另一个用于转发客户端 IP 的 header,但与 XFF 不同,它只携带单个地址。 + +如果你的代理设置了这个 header,请使用 `ExtractIPFromRealIPHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +与 XFF 一样,它默认信任内部 IP 地址,并接受相同的 `TrustOption`。 + +:::danger +**永远不要忘记**配置最外层代理(基础设施边缘的代理)**不要透传传入的 header**。 +否则客户端可以伪造它们,从而打开欺诈之门。 +::: + +## 默认行为 + +默认情况下,Echo 会同时考虑第一个 XFF header、X-Real-IP header 和网络层 IP。 + +正如本文应当说明的那样,这并不是好的选择。它仅为了向后兼容而保留为默认值。 diff --git a/site/src/content/docs/zh-cn/guide/quickstart.md b/site/src/content/docs/zh-cn/guide/quickstart.md new file mode 100644 index 00000000..a83dd242 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/quickstart.md @@ -0,0 +1,75 @@ +--- +title: 快速开始 +description: 在五分钟内构建可用于生产的 Echo API。 +sidebar: + order: 1 +--- + +Echo 是一个高性能、极简的 Go Web 框架。本指南会让服务器在五分钟内运行起来。 + +## 要求 + +Echo 需要 **Go 1.25 或更新版本**。检查你的版本: + +```bash +go version +``` + +## 安装 + +创建一个 module 并添加 Echo: + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +创建 `main.go`: + +```go +package main + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"message": "Hello, World!"}) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +运行它: + +```bash +go run main.go +``` + +你的服务器已经在 `http://localhost:1323` 上运行。Echo 的路由器在每条路由分发请求时实现 +**零动态内存分配**。 + +:::tip[Ask Echo] +遇到问题?按下右下角的 **Ask Echo** 按钮并询问 +*"How do I add JWT auth?"*,答案会直接来自这些文档。 +::: + +## 下一步 + +- [路由](/zh-cn/guide/routing/):静态、参数化和通配符路由。 +- [上下文](/zh-cn/guide/context/):每个请求的请求/响应对象。 +- [绑定](/zh-cn/guide/binding/):将请求数据解析到带类型的 struct 中。 diff --git a/site/src/content/docs/zh-cn/guide/request.md b/site/src/content/docs/zh-cn/guide/request.md new file mode 100644 index 00000000..2bb90b05 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/request.md @@ -0,0 +1,168 @@ +--- +title: 请求 +description: 从请求中获取表单、查询和路径数据,并验证它。 +sidebar: + order: 7 +--- + +处理函数通过 `echo.Context` 读取请求数据。Echo 可以按名称分别获取值, +把它们绑定到 struct 中(参见[绑定](/zh-cn/guide/binding/)),并把验证交给你注册的 validator。 + +## 获取数据 + +### 表单数据 + +使用 `Context#FormValue(name string)` 按名称获取表单字段: + +```go +e.POST("/form", func(c *echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +对于 `string` 以外的类型,请使用泛型函数 `echo.FormValue[T]`: + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +测试: + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +要绑定自定义数据类型,请实现 `echo.BindUnmarshaler` 接口: + +```go +type Timestamp time.Time + +func (t *Timestamp) UnmarshalParam(src string) error { + ts, err := time.Parse(time.RFC3339, src) + if err != nil { + return err + } + *t = Timestamp(ts) + return nil +} +``` + +### 查询参数 + +使用 `Context#QueryParam(name string)` 按名称获取查询参数: + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +对于 `string` 以外的类型,请使用泛型函数 `echo.QueryParam[T]`: + +```go +age, err := echo.QueryParam[int](c, "age") +if err != nil { + return err +} +``` + +```sh +curl -X GET "http://localhost:1323?name=Joe&age=30" +``` + +### 路径参数 + +使用 `Context#Param(name string)` 按名称获取已注册的路径参数: + +```go +e.GET("/users/:name", func(c *echo.Context) error { + name := c.Param("name") + return c.String(http.StatusOK, name) +}) +``` + +对于 `string` 以外的类型,请使用泛型函数 `echo.PathParam[T]`: + +```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 +``` + +### 绑定数据 + +Echo 也可以把请求数据绑定到原生 Go struct 和变量中。参见 +[绑定](/zh-cn/guide/binding/)。 + +## 验证数据 + +Echo 没有内置数据验证。你可以通过 `Echo#Validator` 注册自定义 validator,并使用 +[go-playground/validator](https://github.com/go-playground/validator) 这样的第三方库。 + +下面的示例会验证一个已绑定的 struct: + +```go +package main + +import ( + "net/http" + + "github.com/go-playground/validator/v10" // go get github.com/go-playground/validator/v10 + "github.com/labstack/echo/v5" +) + +type CustomValidator struct { + validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i any) error { + if err := cv.validator.Struct(i); err != nil { + // Optionally return the error to let each route control the status code. + return echo.ErrBadRequest.Wrap(err) + } + return nil +} + +type User struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` +} + +func main() { + e := echo.New() + e.Validator = &CustomValidator{validator: validator.New()} + + e.POST("/users", func(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + if err := c.Validate(u); err != nil { + return err + } + return c.JSON(http.StatusOK, u) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +```sh +curl -X POST http://localhost:1323/users \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe","email":"joe@invalid-domain"}' +{"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"} +``` diff --git a/site/src/content/docs/zh-cn/guide/response.md b/site/src/content/docs/zh-cn/guide/response.md new file mode 100644 index 00000000..05d920ff --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/response.md @@ -0,0 +1,316 @@ +--- +title: 响应 +description: 发送字符串、HTML、JSON、XML、文件、流、重定向和响应钩子。 +sidebar: + order: 8 +--- + +处理函数通过 `echo.Context` 写入响应。每个辅助方法都会为你设置合适的 +`Content-Type` 和状态码。 + +## 发送字符串 + +`Context#String(code int, s string)` 会发送带状态码的纯文本响应。 + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## 发送 HTML + +`Context#HTML(code int, html string)` 会发送带状态码的简单 HTML 响应。 +要动态生成 HTML,请参见[模板](/zh-cn/guide/templates/)。 + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### 发送 HTML blob + +`Context#HTMLBlob(code int, b []byte)` 会发送带状态码的 HTML blob。 +它适合与输出 `[]byte` 的模板引擎一起使用。 + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## 渲染模板 + +参见[模板](/zh-cn/guide/templates/)。 + +## 发送 JSON + +`Context#JSON(code int, i any)` 会把 Go 值编码为 JSON,并带状态码发送。 + +```go +type User struct { + Name string `json:"name" xml:"name"` + Email string `json:"email" xml:"email"` +} + +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSON(http.StatusOK, u) +} +``` + +### 流式发送 JSON + +`Context#JSON()` 内部使用 `json.Marshal`,对于大型负载可能效率不高。 +这种情况下,请直接流式发送 JSON: + +```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)` 会发送格式化后的 JSON 响应。 +缩进可以是空格或制表符。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSONPretty(http.StatusOK, u, " ") +} +``` + +```json +{ + "email": "jon@labstack.com", + "name": "Jon" +} +``` + +### JSON blob + +`Context#JSONBlob(code int, b []byte)` 会直接发送预编码的 JSON blob, +例如来自数据库的数据。 + +```go +func(c *echo.Context) error { + encodedJSON := []byte{} // Encoded JSON from an external source. + return c.JSONBlob(http.StatusOK, encodedJSON) +} +``` + +## 发送 JSONP + +`Context#JSONP(code int, callback string, i any)` 会把 Go 值编码为 JSON, +并作为包装在给定 callback 中的 JSONP 负载发送。 + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +参见 [JSONP cookbook](/zh-cn/cookbook/jsonp/)。 + +## 发送 XML + +`Context#XML(code int, i any)` 会把 Go 值编码为 XML,并带状态码发送。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XML(http.StatusOK, u) +} +``` + +### 流式发送 XML + +`Context#XML` 内部使用 `xml.Marshal`,对于大型负载可能效率不高。 +这种情况下,请直接流式发送 XML: + +```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)` 会发送格式化后的 XML 响应。 +缩进可以是空格或制表符。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XMLPretty(http.StatusOK, u, " ") +} +``` + +```xml + + + Jon + jon@labstack.com + +``` + +:::tip +你也可以在请求 URL 查询字符串中追加 `pretty`,让 `Context#XML()` 输出格式化后的 XML。 + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### XML blob + +`Context#XMLBlob(code int, b []byte)` 会直接发送预编码的 XML blob, +例如来自数据库的数据。 + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## 发送文件 + +`Context#File(file string)` 会把文件内容作为响应发送。它会设置正确的内容类型, +并自动处理缓存。 + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## 发送附件 + +`Context#Attachment(file, name string)` 类似于 `File()`,但会使用 +`Content-Disposition: attachment` 和给定名称发送文件。 + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## 内联发送 + +`Context#Inline(file, name string)` 类似于 `File()`,但会使用 +`Content-Disposition: inline` 和给定名称发送文件。 + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## 发送 blob + +`Context#Blob(code int, contentType string, b []byte)` 会使用给定内容类型和状态码发送任意数据。 + +```go +func(c *echo.Context) error { + data := []byte(`0306703,0035866,NO_ACTION,06/19/2006 +0086003,"0005866",UPDATED,06/19/2006`) + return c.Blob(http.StatusOK, "text/csv", data) +} +``` + +## 发送流 + +`Context#Stream(code int, contentType string, r io.Reader)` 会使用给定内容类型、 +`io.Reader` 和状态码发送任意数据流。 + +```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) +} +``` + +## 发送无内容响应 + +`Context#NoContent(code int)` 会发送带状态码的空响应体。 + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## 重定向请求 + +`Context#Redirect(code int, url string)` 会使用给定 URL 和状态码重定向请求。 + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## 钩子 + +### 响应前 + +`Response#Before(func())` 注册一个函数,在响应写入之前运行。 + +### 响应后 + +`Response#After(func())` 注册一个函数,在响应写入之后运行。如果 `Content-Length` +未知,则不会运行任何 after 函数。 + +```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 +你可以注册多个 `Before` 和 `After` 函数。 +::: diff --git a/site/src/content/docs/zh-cn/guide/routing.md b/site/src/content/docs/zh-cn/guide/routing.md new file mode 100644 index 00000000..770afc94 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/routing.md @@ -0,0 +1,75 @@ +--- +title: 路由 +description: 在 Echo 零分配 radix tree 上将请求 URL 匹配到处理函数。 +sidebar: + order: 3 +--- + +Echo 优化过的路由器使用 radix tree 将请求 URL 匹配到处理函数,实现 +**零动态内存分配**和智能路由优先级。 + +## 注册路由 + +在 `Echo` 实例上使用 HTTP 方法辅助函数。每个函数都接受一个路径模式和一个 +`HandlerFunc`(`func(c *echo.Context) error`),并可选接收路由级中间件。 + +```go +e := echo.New() + +e.GET("/users/:id", getUser) // named parameter +e.POST("/users", createUser) +e.PUT("/users/:id", updateUser) +e.DELETE("/users/:id", deleteUser) +e.GET("/static/*", serveFiles) // wildcard +``` + +`Any` 会为所有支持的方法注册处理函数,`Match` 则用于一组指定方法: + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## 匹配类型 + +| 模式 | 类型 | 匹配示例 | +| ------------------ | -------- | -------------------------- | +| `/users/profile` | 静态 | `/users/profile` | +| `/users/:id` | 参数 | `/users/42` | +| `/static/*` | 通配符 | `/static/css/app.css` | + +:::note +优先级是**静态 → 参数 → 通配符**,因此 `/users/profile` 总是优先于 +`/users/:id`,而后者优先于 `/users/*`。 +::: + +## 路径参数 + +使用 `c.Param()` 从上下文读取命名参数(或使用 `c.ParamOr()` 提供默认值): + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +通配符片段可通过 `*` 参数访问: + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## 路由组 + +使用 `e.Group()` 将共享前缀和中间件的路由分组: + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +路由组可以嵌套,以组合更大的路由树。 diff --git a/site/src/content/docs/zh-cn/guide/static-files.md b/site/src/content/docs/zh-cn/guide/static-files.md new file mode 100644 index 00000000..a8e10dba --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/static-files.md @@ -0,0 +1,85 @@ +--- +title: 提供静态文件 +description: 使用 Echo 提供图片、JavaScript、CSS、字体和其他资源。 +sidebar: + order: 9 +--- + +Echo 可以从文件系统或嵌入式文件系统提供图片、JavaScript、CSS、PDF、字体等静态资源。 + +## 默认文件系统 + +Echo 使用 `os.DirFS(".")` 作为默认文件系统,其根目录是当前工作目录。 +要更改它,请设置 `Echo#Filesystem` 字段: + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## 使用 Static 中间件 + +参见 [Static 中间件](/zh-cn/middleware/static/)。 + +## 使用 Echo#Static() + +`Echo#Static(prefix, root string)` 会注册一条路由,从给定根目录按路径前缀提供静态文件。 + +从 `assets` 下通过 `/static/*` 提供任意文件。对 `/static/js/main.js` 的请求会提供 +`assets/js/main.js`: + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +从 `assets` 下通过 `/*` 提供任意文件。对 `/js/main.js` 的请求会提供 +`assets/js/main.js`: + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## 使用 Echo#StaticFS() + +静态文件可以从任何 `fs.FS` 提供,包括 `embed.FS`。使用 `echo.MustSubFS`, +使提供的文件以正确子目录为根;`embed.FS` 会把其子目录作为自己的条目包含进来。 + +```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) + } +} +``` + +## 使用 Echo#File() + +`Echo#File(path, file string)` 会注册一条路由,用于提供单个静态文件。 + +从 `public/index.html` 提供索引页: + +```go +e.File("/", "public/index.html") +``` + +从 `app/assets/favicon.ico` 提供 favicon: + +```go +e := echo.New() +e.Filesystem = os.DirFS("/") +e.File("/favicon.ico", "app/assets/favicon.ico") // The file path must not have a leading slash. +``` + +:::caution +文件路径中前导 `/` 不适用于大多数 `fs.FS` 实现。请使用相对路径。 +::: diff --git a/site/src/content/docs/zh-cn/guide/templates.md b/site/src/content/docs/zh-cn/guide/templates.md new file mode 100644 index 00000000..4602ac3d --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/templates.md @@ -0,0 +1,131 @@ +--- +title: 模板 +description: 通过注册 renderer 使用任意引擎渲染 HTML 模板。 +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` 会用数据渲染模板,并发送带状态码的 +`text/html` 响应。通过设置 `Echo#Renderer` 注册 renderer,这让你可以使用任何模板引擎。 + +## 渲染 + +下面的示例使用 Go 的 `html/template`。 + +使用默认模板 renderer: + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +或者自己实现 `echo.Renderer` 接口: + +```go +type Template struct { + templates *template.Template +} + +func (t *Template) Render(c *echo.Context, w io.Writer, name string, data any) error { + return t.templates.ExecuteTemplate(w, name, data) +} +``` + +1. 预编译模板。 + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. 注册 renderer。 + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. 在处理函数中渲染模板。 + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## 高级:从模板调用 Echo + +有时从模板内部调用 `Echo#Reverse` 来生成 URI 很有用。Go 的 `html/template` +并不特别适合这样做,但可以通过两种方式实现:为传给模板的每个对象提供一个公共方法, +或者传入 `map[string]any` 并在自定义 renderer 中扩展它。后者更加灵活。 +下面是一个完整示例。 + +`template.html`: + +```html + + +

Hello {{index . "name"}}

+ +

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

+ + +``` + +`server.go`: + +```go +package main + +import ( + "html/template" + "io" + "net/http" + + "github.com/labstack/echo/v5" +) + +// TemplateRenderer is a custom html/template renderer for Echo. +type TemplateRenderer struct { + templates *template.Template +} + +// Render renders a template document. +func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error { + // Add global methods if the data is a map. + if viewContext, isMap := data.(map[string]any); isMap { + viewContext["reverse"] = c.RouteInfo().Reverse + } + + return t.templates.ExecuteTemplate(w, name, data) +} + +func main() { + e := echo.New() + e.Renderer = &TemplateRenderer{ + templates: template.Must(template.ParseGlob("main/*.html")), + } + + e.GET("/something/:name", func(c *echo.Context) error { + return c.Render(http.StatusOK, "template.html", map[string]any{ + "name": "Dolly!", + }) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("shutting down the server", "error", err) + } +} +``` diff --git a/site/src/content/docs/zh-cn/guide/testing.md b/site/src/content/docs/zh-cn/guide/testing.md new file mode 100644 index 00000000..9c003139 --- /dev/null +++ b/site/src/content/docs/zh-cn/guide/testing.md @@ -0,0 +1,262 @@ +--- +title: 测试 +description: 使用 httptest 和 echotest 辅助方法测试处理函数和中间件。 +sidebar: + order: 13 +--- + +Echo 处理函数和中间件都是围绕 `echo.Context` 的普通函数,因此可以直接使用标准 +`net/http/httptest` 包测试。`echotest` 包提供了可减少样板代码的辅助方法。 + +## 测试处理函数 + +考虑两个处理函数: + +**CreateUser**:`POST /users` + +- 接受 JSON 负载。 +- 成功时返回 `201 Created`。 +- 出错时返回 `500 Internal Server Error`。 + +**GetUser**:`GET /users/:email` + +- 成功时返回 `200 OK`。 +- 如果用户不存在,返回 `404 Not Found`;否则返回 `500 Internal Server Error`。 + +`handler.go`: + +```go +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v5" +) + +type ( + User struct { + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + } + handler struct { + db map[string]*User + } +) + +func (h *handler) createUser(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + return c.JSON(http.StatusCreated, u) +} + +func (h *handler) getUser(c *echo.Context) error { + email := c.Param("email") + user := h.db[email] + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + return c.JSON(http.StatusOK, user) +} +``` + +`handler_test.go`: + +```go +package handler + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/echotest" + "github.com/stretchr/testify/assert" +) + +var ( + mockDB = map[string]*User{ + "jon@labstack.com": {Name: "Jon Snow", Email: "jon@labstack.com"}, + } + userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}` +) + +func TestCreateUser(t *testing.T) { + // Setup + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### 使用 echotest 辅助方法 + +`echotest.ContextConfig` 会根据请求的声明式描述构建上下文(和 recorder): + +```go +// Same test as above, using echotest. +func TestCreateUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} + +// Even shorter, using ServeWithHandler. +func TestCreateUserWithServeHandler(t *testing.T) { + h := &handler{mockDB} + + rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ServeWithHandler(t, h.createUser) + + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) +} + +func TestGetUser(t *testing.T) { + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }, + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.getUser(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### 使用表单负载 + +```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) +``` + +使用 `echotest` 构建 multipart 表单负载: + +```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) +} +``` + +### 设置路径参数 + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### 设置查询参数 + +```go +// import "net/url" +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) +``` + +## 测试中间件 + +```go +func TestMiddleware(t *testing.T) { + handler := func(c *echo.Context) error { + return c.JSON(http.StatusTeapot, fmt.Sprintf("email: %s", c.Param("email"))) + } + middleware := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("user_id", int64(1234)) + return next(c) + } + } + + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{{Name: "email", Value: "jon@labstack.com"}}, + }.ToContextRecorder(t) + + if err := middleware(handler)(c); err != nil { + t.Fatal(err) + } + + // Check that the middleware set the value. + userID, err := echo.ContextGet[int64](c, "user_id") + assert.NoError(t, err) + assert.Equal(t, int64(1234), userID) + + // Check that the handler returned the correct response. + assert.Equal(t, http.StatusTeapot, rec.Code) +} +``` + +:::tip +更多示例请参见 Echo 源码中的 +[中间件测试用例](https://github.com/labstack/echo/tree/master/middleware)。 +::: From 9c9cd5f2f06cb25a8a8e708a1d1b0368c2034d4f Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Mon, 15 Jun 2026 21:59:54 -0700 Subject: [PATCH 3/4] docs(i18n): middleware (zh-cn, ja) --- .../content/docs/ja/middleware/basic-auth.md | 74 +++++ .../content/docs/ja/middleware/body-dump.md | 72 +++++ .../content/docs/ja/middleware/body-limit.md | 47 +++ .../content/docs/ja/middleware/casbin-auth.md | 201 +++++++++++++ .../docs/ja/middleware/context-timeout.md | 47 +++ site/src/content/docs/ja/middleware/cors.md | 118 ++++++++ site/src/content/docs/ja/middleware/csrf.md | 174 +++++++++++ .../content/docs/ja/middleware/decompress.md | 58 ++++ site/src/content/docs/ja/middleware/gzip.md | 69 +++++ site/src/content/docs/ja/middleware/jwt.md | 126 ++++++++ .../content/docs/ja/middleware/key-auth.md | 91 ++++++ site/src/content/docs/ja/middleware/logger.md | 236 +++++++++++++++ .../docs/ja/middleware/method-override.md | 52 ++++ .../docs/ja/middleware/open-telemetry.md | 81 +++++ .../content/docs/ja/middleware/prometheus.md | 284 ++++++++++++++++++ site/src/content/docs/ja/middleware/proxy.md | 134 +++++++++ .../docs/ja/middleware/rate-limiter.md | 108 +++++++ .../src/content/docs/ja/middleware/recover.md | 59 ++++ .../content/docs/ja/middleware/redirect.md | 100 ++++++ .../content/docs/ja/middleware/request-id.md | 89 ++++++ .../src/content/docs/ja/middleware/rewrite.md | 87 ++++++ site/src/content/docs/ja/middleware/secure.md | 113 +++++++ .../src/content/docs/ja/middleware/session.md | 187 ++++++++++++ site/src/content/docs/ja/middleware/static.md | 141 +++++++++ .../docs/ja/middleware/trailing-slash.md | 64 ++++ .../docs/zh-cn/middleware/basic-auth.md | 74 +++++ .../docs/zh-cn/middleware/body-dump.md | 70 +++++ .../docs/zh-cn/middleware/body-limit.md | 47 +++ .../docs/zh-cn/middleware/casbin-auth.md | 201 +++++++++++++ .../docs/zh-cn/middleware/context-timeout.md | 47 +++ .../src/content/docs/zh-cn/middleware/cors.md | 117 ++++++++ .../src/content/docs/zh-cn/middleware/csrf.md | 173 +++++++++++ .../docs/zh-cn/middleware/decompress.md | 57 ++++ .../src/content/docs/zh-cn/middleware/gzip.md | 69 +++++ site/src/content/docs/zh-cn/middleware/jwt.md | 126 ++++++++ .../content/docs/zh-cn/middleware/key-auth.md | 91 ++++++ .../content/docs/zh-cn/middleware/logger.md | 234 +++++++++++++++ .../docs/zh-cn/middleware/method-override.md | 51 ++++ .../docs/zh-cn/middleware/open-telemetry.md | 80 +++++ .../docs/zh-cn/middleware/prometheus.md | 284 ++++++++++++++++++ .../content/docs/zh-cn/middleware/proxy.md | 132 ++++++++ .../docs/zh-cn/middleware/rate-limiter.md | 106 +++++++ .../content/docs/zh-cn/middleware/recover.md | 59 ++++ .../content/docs/zh-cn/middleware/redirect.md | 100 ++++++ .../docs/zh-cn/middleware/request-id.md | 89 ++++++ .../content/docs/zh-cn/middleware/rewrite.md | 85 ++++++ .../content/docs/zh-cn/middleware/secure.md | 112 +++++++ .../content/docs/zh-cn/middleware/session.md | 185 ++++++++++++ .../content/docs/zh-cn/middleware/static.md | 141 +++++++++ .../docs/zh-cn/middleware/trailing-slash.md | 64 ++++ 50 files changed, 5606 insertions(+) create mode 100644 site/src/content/docs/ja/middleware/basic-auth.md create mode 100644 site/src/content/docs/ja/middleware/body-dump.md create mode 100644 site/src/content/docs/ja/middleware/body-limit.md create mode 100644 site/src/content/docs/ja/middleware/casbin-auth.md create mode 100644 site/src/content/docs/ja/middleware/context-timeout.md create mode 100644 site/src/content/docs/ja/middleware/cors.md create mode 100644 site/src/content/docs/ja/middleware/csrf.md create mode 100644 site/src/content/docs/ja/middleware/decompress.md create mode 100644 site/src/content/docs/ja/middleware/gzip.md create mode 100644 site/src/content/docs/ja/middleware/jwt.md create mode 100644 site/src/content/docs/ja/middleware/key-auth.md create mode 100644 site/src/content/docs/ja/middleware/logger.md create mode 100644 site/src/content/docs/ja/middleware/method-override.md create mode 100644 site/src/content/docs/ja/middleware/open-telemetry.md create mode 100644 site/src/content/docs/ja/middleware/prometheus.md create mode 100644 site/src/content/docs/ja/middleware/proxy.md create mode 100644 site/src/content/docs/ja/middleware/rate-limiter.md create mode 100644 site/src/content/docs/ja/middleware/recover.md create mode 100644 site/src/content/docs/ja/middleware/redirect.md create mode 100644 site/src/content/docs/ja/middleware/request-id.md create mode 100644 site/src/content/docs/ja/middleware/rewrite.md create mode 100644 site/src/content/docs/ja/middleware/secure.md create mode 100644 site/src/content/docs/ja/middleware/session.md create mode 100644 site/src/content/docs/ja/middleware/static.md create mode 100644 site/src/content/docs/ja/middleware/trailing-slash.md create mode 100644 site/src/content/docs/zh-cn/middleware/basic-auth.md create mode 100644 site/src/content/docs/zh-cn/middleware/body-dump.md create mode 100644 site/src/content/docs/zh-cn/middleware/body-limit.md create mode 100644 site/src/content/docs/zh-cn/middleware/casbin-auth.md create mode 100644 site/src/content/docs/zh-cn/middleware/context-timeout.md create mode 100644 site/src/content/docs/zh-cn/middleware/cors.md create mode 100644 site/src/content/docs/zh-cn/middleware/csrf.md create mode 100644 site/src/content/docs/zh-cn/middleware/decompress.md create mode 100644 site/src/content/docs/zh-cn/middleware/gzip.md create mode 100644 site/src/content/docs/zh-cn/middleware/jwt.md create mode 100644 site/src/content/docs/zh-cn/middleware/key-auth.md create mode 100644 site/src/content/docs/zh-cn/middleware/logger.md create mode 100644 site/src/content/docs/zh-cn/middleware/method-override.md create mode 100644 site/src/content/docs/zh-cn/middleware/open-telemetry.md create mode 100644 site/src/content/docs/zh-cn/middleware/prometheus.md create mode 100644 site/src/content/docs/zh-cn/middleware/proxy.md create mode 100644 site/src/content/docs/zh-cn/middleware/rate-limiter.md create mode 100644 site/src/content/docs/zh-cn/middleware/recover.md create mode 100644 site/src/content/docs/zh-cn/middleware/redirect.md create mode 100644 site/src/content/docs/zh-cn/middleware/request-id.md create mode 100644 site/src/content/docs/zh-cn/middleware/rewrite.md create mode 100644 site/src/content/docs/zh-cn/middleware/secure.md create mode 100644 site/src/content/docs/zh-cn/middleware/session.md create mode 100644 site/src/content/docs/zh-cn/middleware/static.md create mode 100644 site/src/content/docs/zh-cn/middleware/trailing-slash.md diff --git a/site/src/content/docs/ja/middleware/basic-auth.md b/site/src/content/docs/ja/middleware/basic-auth.md new file mode 100644 index 00000000..40173d18 --- /dev/null +++ b/site/src/content/docs/ja/middleware/basic-auth.md @@ -0,0 +1,74 @@ +--- +title: Basic Auth +description: ユーザー名とパスワードの認証情報を検証する HTTP Basic 認証ミドルウェアです。 +sidebar: + order: 1 +--- + +Basic Auth ミドルウェアは HTTP Basic 認証を提供します。 + +- 有効な認証情報の場合、次のハンドラを呼び出します。 +- 認証情報がない、または無効な場合は `401 Unauthorized` レスポンスを送信します。 + +## 使い方 + +```go +e.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (bool, error) { + // Use a constant time comparison to prevent timing attacks. + if subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 { + return true, nil + } + return false, nil +})) +``` + +## カスタム設定 + +```go +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) +``` + +## 設定 + +```go +type BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator validates the credentials. If the request contains multiple basic + // auth headers, it is called once for each header until the first valid result. + // Required. + Validator BasicAuthValidator + + // Realm is the realm attribute of the WWW-Authenticate header. + // Default value "Restricted". + Realm string + + // AllowedCheckLimit sets how many headers are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + // Default value 1. + AllowedCheckLimit uint +} +``` + +`Validator` のシグネチャは次のとおりです。 + +```go +type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error) +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: "Restricted", +} +``` + +:::caution[セキュリティ] +タイミング攻撃を防ぐため、認証情報の比較には必ず `subtle.ConstantTimeCompare` を使ってください。 +::: diff --git a/site/src/content/docs/ja/middleware/body-dump.md b/site/src/content/docs/ja/middleware/body-dump.md new file mode 100644 index 00000000..cc579a6f --- /dev/null +++ b/site/src/content/docs/ja/middleware/body-dump.md @@ -0,0 +1,72 @@ +--- +title: ボディダンプ +description: リクエストとレスポンスのペイロードをキャプチャし、ログ記録やデバッグ用のハンドラへ渡します。 +sidebar: + order: 2 +--- + +Body Dump ミドルウェアは、リクエストとレスポンスのペイロードをキャプチャして登録済みハンドラへ渡します。 +一般的にはデバッグやログ記録に使います。 + +:::caution +ファイルのアップロードやダウンロードなど、大きなペイロードには Body Dump を避けてください。 +そのようなルートで使う必要がある場合は、skipper 関数に例外を追加してください。 +::: + +## 使い方 + +```go +e := echo.New() +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) { + // Handle the request and response bodies. +})) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) +``` + +## 設定 + +```go +type BodyDumpConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Handler receives the request and response payloads and the handler error, if any. + // Required. + Handler BodyDumpHandler + + // MaxRequestBytes limits how much of the request body to dump. If the request body + // exceeds this limit, only the first MaxRequestBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxRequestBytes int64 + + // MaxResponseBytes limits how much of the response body to dump. If the response body + // exceeds this limit, only the first MaxResponseBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxResponseBytes int64 +} +``` + +`Handler` のシグネチャは次のとおりです。 + +```go +type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error) +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset (Handler is required). +BodyDumpConfig{ + Skipper: DefaultSkipper, + MaxRequestBytes: 5 * MB, + MaxResponseBytes: 5 * MB, +} +``` diff --git a/site/src/content/docs/ja/middleware/body-limit.md b/site/src/content/docs/ja/middleware/body-limit.md new file mode 100644 index 00000000..ba377d6b --- /dev/null +++ b/site/src/content/docs/ja/middleware/body-limit.md @@ -0,0 +1,47 @@ +--- +title: ボディ制限 +description: ボディが設定済みの最大サイズを超えるリクエストを拒否します。 +sidebar: + order: 3 +--- + +Body Limit ミドルウェアはリクエストボディに許可される最大サイズを設定します。 +サイズが設定値を超えた場合、`413 Request Entity Too Large` レスポンスを送信します。 + +この制限は `Content-Length` リクエスト header と実際に読み取られた内容の両方に適用されるため、 +偽装された header に対しても耐性があります。制限値はバイト単位で指定します。 + +## 使い方 + +```go +e := echo.New() +e.Use(middleware.BodyLimit(2_097_152)) // 2 MB +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) +``` + +## 設定 + +```go +type BodyLimitConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // LimitBytes is the maximum allowed size in bytes for a request body. + LimitBytes int64 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset (Limit is required). +BodyLimitConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/ja/middleware/casbin-auth.md b/site/src/content/docs/ja/middleware/casbin-auth.md new file mode 100644 index 00000000..5b18fe8f --- /dev/null +++ b/site/src/content/docs/ja/middleware/casbin-auth.md @@ -0,0 +1,201 @@ +--- +title: Casbin 認可 +description: 小さなカスタムミドルウェアで Casbin アクセス制御ライブラリを使い、リクエストを認可します。 +sidebar: + order: 4 +--- + +[Casbin](https://github.com/casbin/casbin) は、Go 向けの強力で効率的なオープンソース +アクセス制御ライブラリです。多くのモデルで認可を強制できます。 + +- ACL(Access Control List) +- スーパーユーザー付き ACL +- ユーザーなし ACL:認証やユーザーログインがないシステムに有用 +- リソースなし ACL:個別リソースではなく、リソース種別(例:`write-article`、`read-log`)を対象にする +- RBAC(Role-Based Access Control) +- リソースロール付き RBAC:ユーザーとリソースの両方がロールを持てる +- ドメイン/テナント付き RBAC:ユーザーがドメイン/テナントごとに異なるロールセットを持てる +- ABAC(Attribute-Based Access Control) +- RESTful +- Deny-override:allow と deny の両方のルールをサポートし、deny が allow を上書きする + +詳細は [API overview](https://casbin.org/docs/api-overview) と +[Casbin documentation](https://casbin.org/docs/) を参照してください。 + +## 依存関係 + +```bash +go get github.com/casbin/casbin/v3 +``` + +```go +import ( + "github.com/casbin/casbin/v3" +) +``` + +## 実装 + +Echo には Casbin ミドルウェアは同梱されていません。この連携は Casbin enforcer の小さなラッパーです。 + +```go +// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/). +func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + username, err := userGetter(c) + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { + return echo.ErrInternalServerError.Wrap(err) + } else if !pass { + return echo.NewHTTPError(http.StatusForbidden, "access denied") + } + return next(c) + } + } +} +``` + +## 例 + +Casbin モデルファイル `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 == "*") +``` + +Casbin ポリシーファイル `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 +``` + +認証と認可は別の関心事です。JWT や Basic Auth など別のミドルウェアでユーザーを認証し、 +Casbin がリクエストを認可できるよう `userGetter` を渡します。 + +### JWT と使う + +```go +e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication +jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return "", err + } + return token.Claims.GetSubject() +} +e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization +``` + +次で試します。 + +```bash +curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" +``` + +### Basic Auth と使う + +```go +// BasicAuth middleware does authentication +e.Use(middleware.BasicAuth(func(c *echo.Context, user, password string) (bool, error) { + return subtle.ConstantTimeCompare([]byte(user), []byte("alice")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("password")) == 1, nil +})) +basicAuthUser := func(c *echo.Context) (string, error) { // Basic auth user getter for Casbin authorization + username, _, _ := c.Request().BasicAuth() // password is verified by the BasicAuth middleware above + return username, nil +} +e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization +``` + +次で試します。 + +```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 +``` + +### Casbin + JWT の完全な例 + +```go +package main + +import ( + "log/slog" + "net/http" + + "github.com/casbin/casbin/v3" + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" +) + +// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/). +func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + username, err := userGetter(c) + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { + return echo.ErrInternalServerError.Wrap(err) + } else if !pass { + return echo.NewHTTPError(http.StatusForbidden, "access denied") + } + return next(c) + } + } +} + +func main() { + e := echo.New() + + ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") + if err != nil { + slog.Error("failed to initialize Casbin enforcer", "error", err) + } + + e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication + jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return "", err + } + return token.Claims.GetSubject() + } + e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization + + e.GET("/*", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/ja/middleware/context-timeout.md b/site/src/content/docs/ja/middleware/context-timeout.md new file mode 100644 index 00000000..9656a5ba --- /dev/null +++ b/site/src/content/docs/ja/middleware/context-timeout.md @@ -0,0 +1,47 @@ +--- +title: コンテキストタイムアウト +description: リクエストコンテキストにタイムアウトを適用し、コンテキスト対応の処理を早期に返せるようにします。 +sidebar: + order: 5 +--- + +Context Timeout ミドルウェアは、定義済みの期間内でリクエストコンテキストにタイムアウトを適用し、 +期限を超えたらコンテキスト対応メソッドが早期に返れるようにします。 + +## 使い方 + +```go +e.Use(middleware.ContextTimeout(60 * time.Second)) +``` + +## カスタム設定 + +```go +e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ + Timeout: 60 * time.Second, +})) +``` + +## 設定 + +```go +type ContextTimeoutConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // ErrorHandler is a function invoked when an error arises during middleware execution. + ErrorHandler func(c *echo.Context, err error) error + + // Timeout configures the timeout for the middleware. + Timeout time.Duration +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset (Timeout is required). +ContextTimeoutConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/ja/middleware/cors.md b/site/src/content/docs/ja/middleware/cors.md new file mode 100644 index 00000000..65bbb97c --- /dev/null +++ b/site/src/content/docs/ja/middleware/cors.md @@ -0,0 +1,118 @@ +--- +title: CORS +description: 安全なクロスドメインアクセス制御のための Cross-Origin Resource Sharing ミドルウェアです。 +sidebar: + order: 6 +--- + +CORS ミドルウェアは [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol) 仕様を実装します。 +CORS は Web サーバーにクロスドメインアクセス制御を提供し、安全なクロスドメインデータ転送を可能にします。 + +## 使い方 + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## カスタム設定 + +```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}, +})) +``` + +## 設定 + +```go +type CORSConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // AllowOrigins determines the value of the Access-Control-Allow-Origin response + // header, defining the list of origins that may access the resource. + // + // An origin consists of: scheme + "://" + host + optional ":" + port. + // A wildcard may be used, but it must be set explicitly as []string{"*"}. + // Example: `https://example.com`, `http://example.com:8080`, `*`. + // + // Security: use extreme caution when handling the origin and carefully validate any + // logic. Attackers may register hostile domain names. See + // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // Mandatory. + AllowOrigins []string + + // UnsafeAllowOriginFunc is an optional custom function to validate the origin. It + // takes the origin and returns the allowed origin, whether it is allowed, and an + // error (returned immediately by the handler). If set, AllowOrigins is ignored. + // + // Security: use extreme caution when handling the origin. Attackers may register + // hostile (sub)domain names. + // + // Sub-domain check example: + // UnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) { + // if strings.HasSuffix(origin, ".example.com") { + // return origin, true, nil + // } + // return "", false, nil + // } + // + // Optional. + UnsafeAllowOriginFunc func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error) + + // AllowMethods determines the value of the Access-Control-Allow-Methods response + // header, used in response to a preflight request. + // + // Optional. Defaults to GET, HEAD, PUT, PATCH, POST, DELETE. If left empty, the + // middleware fills the preflight Access-Control-Allow-Methods header from the + // `Allow` header that the router set into the context. + AllowMethods []string + + // AllowHeaders determines the value of the Access-Control-Allow-Headers response + // header, indicating which HTTP headers can be used in the actual request. + // + // Optional. Defaults to an empty list. + AllowHeaders []string + + // AllowCredentials determines the value of the Access-Control-Allow-Credentials + // response header, indicating whether the response can be exposed when the + // credentials mode is true. + // + // Optional. Default value false, in which case the header is not set. + // + // Security: avoid using AllowCredentials = true together with AllowOrigins = *. + AllowCredentials bool + + // ExposeHeaders determines the value of Access-Control-Expose-Headers, the list of + // headers clients are allowed to access. + // + // Optional. Default value []string{}, in which case the header is not set. + ExposeHeaders []string + + // MaxAge determines the value of the Access-Control-Max-Age response header, how long + // (in seconds) the results of a preflight request can be cached. The header is set + // only if MaxAge != 0; a negative value sends "0", instructing browsers not to cache. + // + // Optional. Default value 0 — the header is not sent. + MaxAge int +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +CORSConfig{ + Skipper: DefaultSkipper, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, +} +``` + +:::caution[セキュリティ] +`AllowCredentials = true` とワイルドカードの `AllowOrigins` を組み合わせてはいけません。 +動的な origin 検証が必要な場合は `UnsafeAllowOriginFunc` を使い、慎重に検証してください。 +攻撃者が悪意ある(サブ)ドメイン名を登録する可能性があります。 +::: diff --git a/site/src/content/docs/ja/middleware/csrf.md b/site/src/content/docs/ja/middleware/csrf.md new file mode 100644 index 00000000..25af2235 --- /dev/null +++ b/site/src/content/docs/ja/middleware/csrf.md @@ -0,0 +1,174 @@ +--- +title: CSRF +description: Sec-Fetch-Site メタデータと token 検証を使った Cross-Site Request Forgery 防御です。 +sidebar: + order: 7 +--- + +Cross-Site Request Forgery(CSRF、"sea-surf" と発音されることもある、または XSRF)は、 +Web サイトが信頼するユーザーから未承認のコマンドが送信される悪意ある攻撃の一種です。 + +## 使い方 + +```go +e.Use(middleware.CSRF()) +``` + +## 仕組み + +CSRF ミドルウェアは、現代的な多層防御として +[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) +header をサポートします。 +[CSRF protection](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers) +のために、従来の token ベース機構と並行して OWASP 推奨の Fetch Metadata API を実装しています。 + +現代的なブラウザーはすべてのリクエストに `Sec-Fetch-Site` header を自動送信し、 +リクエスト元とターゲットの関係を示します。ミドルウェアはこれを使ってセキュリティ判断を行います。 + +- **`same-origin`** または **`none`**:許可(完全な同一 origin、またはユーザーの直接ナビゲーション) +- **`same-site`**:token 検証にフォールバック(例:サブドメインからメインドメイン) +- **`cross-site`**:安全でないメソッド(POST、PUT、DELETE、PATCH)ではデフォルトで `403` エラーによりブロック + +この header を送信しないブラウザー(古いブラウザー)では、従来の token ベース CSRF 防御へシームレスに +フォールバックします。 + +`Sec-Fetch-Site` の挙動は 2 つのオプションで調整できます。 + +- `TrustedOrigins []string`:cross-site リクエストで特定 origin を許可リスト化(OAuth callback、webhook などに有用) +- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)`:same-site/cross-site 検証用のカスタムロジック + +```go +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + // Allow OAuth callbacks from a trusted provider. + TrustedOrigins: []string{"https://oauth-provider.com"}, + + // Custom validation for same-site/cross-site requests. + AllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) { + // Your custom authorization logic here. + return validateCustomAuth(c), nil + // return true, err // blocks the request with an error + // return true, nil // allows the request through + // return false, nil // falls back to legacy token logic + }, +})) +``` + +## token ベース防御 + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +上の例は `X-XSRF-TOKEN` リクエスト header から CSRF token を抽出します。 + +代わりに Cookie から token を読む場合: + +```go +middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "cookie:_csrf", + CookiePath: "/", + CookieDomain: "example.com", + CookieSecure: true, + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, +}) +``` + +## CSRF token へのアクセス + +- **サーバー側**:token は `ContextKey` の下でコンテキストから利用でき、テンプレート経由でクライアントへ渡せます。 +- **クライアント側**:token は CSRF Cookie から読み取れます。 + +## 設定 + +```go +type CSRFConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // TrustedOrigins permits any request with a `Sec-Fetch-Site` header whose `Origin` + // header exactly matches one of the listed values. Values should be formatted as + // the Origin header: "scheme://host[:port]". + TrustedOrigins []string + + // AllowSecFetchSiteFunc allows custom behaviour for `Sec-Fetch-Site` requests that + // are about to fail with a CSRF error, to be allowed or replaced with a custom + // error. Applies to `same-site` and `cross-site` values. + AllowSecFetchSiteFunc func(c *echo.Context) (bool, error) + + // TokenLength is the length of the generated token. + // Optional. Default value 32. + TokenLength uint8 + + // TokenLookup is a string in the form ":" or + // ":,:" used to extract the token from the request. + // Optional. Default value "header:X-CSRF-Token". + // Possible values: + // - "header:" or "header::" + // - "query:" + // - "form:" + // Multiple sources example: "header:X-CSRF-Token,query:csrf". + TokenLookup string `yaml:"token_lookup"` + + // Generator defines a function to generate the token. + // Optional. Defaults to randomString(TokenLength). + Generator func() string + + // ContextKey is the key under which the generated CSRF token is stored in the context. + // Optional. Default value "csrf". + ContextKey string + + // CookieName is the name of the CSRF cookie that stores the token. + // Optional. Default value "_csrf". + CookieName string + + // CookieDomain is the domain of the CSRF cookie. + // Optional. Default value none. + CookieDomain string + + // CookiePath is the path of the CSRF cookie. + // Optional. Default value none. + CookiePath string + + // CookieMaxAge is the max age (in seconds) of the CSRF cookie. + // Optional. Default value 86400 (24h). + CookieMaxAge int + + // CookieSecure indicates whether the CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool + + // CookieHTTPOnly indicates whether the CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // CookieSameSite indicates the SameSite mode of the CSRF cookie. + // Optional. Default value SameSiteDefaultMode. + CookieSameSite http.SameSite + + // ErrorHandler defines a function that returns custom errors. + ErrorHandler func(c *echo.Context, err error) error +} +``` + +### デフォルト設定 + +```go +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, +} +``` + +## 完全な例 + +完全に実行可能な例は +[echox cookbook](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go) にあります。 diff --git a/site/src/content/docs/ja/middleware/decompress.md b/site/src/content/docs/ja/middleware/decompress.md new file mode 100644 index 00000000..1c8bfeb8 --- /dev/null +++ b/site/src/content/docs/ja/middleware/decompress.md @@ -0,0 +1,58 @@ +--- +title: 解凍 +description: gzip エンコードされたリクエストボディを透過的に解凍します。 +sidebar: + order: 8 +--- + +Decompress ミドルウェアは、`Content-Encoding` header が `gzip` に設定されている場合に +HTTP リクエストボディを解凍します。 + +:::note +ボディはメモリ内で解凍され、リクエストのライフタイム中(およびガベージコレクションまで)保持されます。 +::: + +## 使い方 + +```go +e.Use(middleware.Decompress()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ + Skipper: middleware.DefaultSkipper, +})) +``` + +## 設定 + +```go +type DecompressConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // GzipDecompressPool provides the sync.Pool used to create and store gzip readers. + GzipDecompressPool Decompressor + + // MaxDecompressedSize limits the maximum size of the decompressed request body in + // bytes. If the decompressed body exceeds this limit, the middleware returns an + // HTTP 413 error. This prevents zip-bomb attacks where a small compressed payload + // decompresses to a huge size. + // Default: 100 * MB (104,857,600 bytes). Set to -1 to disable limits (not recommended). + MaxDecompressedSize int64 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +DecompressConfig{ + Skipper: DefaultSkipper, + GzipDecompressPool: &DefaultGzipDecompressPool{}, + MaxDecompressedSize: 100 * MB, +} +``` diff --git a/site/src/content/docs/ja/middleware/gzip.md b/site/src/content/docs/ja/middleware/gzip.md new file mode 100644 index 00000000..2ab2ae44 --- /dev/null +++ b/site/src/content/docs/ja/middleware/gzip.md @@ -0,0 +1,69 @@ +--- +title: Gzip +description: gzip 圧縮方式で HTTP レスポンスを圧縮します。 +sidebar: + order: 9 +--- + +Gzip ミドルウェアは gzip 圧縮方式を使って HTTP レスポンスを圧縮します。 + +## 使い方 + +```go +e.Use(middleware.Gzip()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +:::tip +skipper を渡すと、特定の URL で gzip を無効化できます。 +::: + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Skipper: func(c *echo.Context) bool { + return strings.Contains(c.Path(), "metrics") // change "metrics" to your own path + }, +})) +``` + +## 設定 + +```go +type GzipConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Level is the gzip compression level. + // Optional. Default value -1. + Level int + + // MinLength is the length threshold before gzip compression is applied. + // Optional. Default value 0. + // + // Most of the time the default is fine. Compressing a short response might increase + // the transmitted data because of gzip's format overhead, and compression consumes + // CPU and time on both server and client. Depending on your use case such a + // threshold can be useful. + MinLength int +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + MinLength: 0, +} +``` diff --git a/site/src/content/docs/ja/middleware/jwt.md b/site/src/content/docs/ja/middleware/jwt.md new file mode 100644 index 00000000..b1a393c8 --- /dev/null +++ b/site/src/content/docs/ja/middleware/jwt.md @@ -0,0 +1,126 @@ +--- +title: JWT +description: echo-jwt module が提供する JSON Web Token 認証ミドルウェアです。 +sidebar: + order: 10 +--- + +JWT ミドルウェアは JSON Web Token(JWT)認証を提供します。これは別 module にあります: +[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt)。 + +挙動: + +- 有効な token の場合、コンテキストにユーザーを設定して次のハンドラを呼び出します。 +- 無効な token の場合、`401 Unauthorized` レスポンスを送信します。 +- `Authorization` header がない、または無効な場合、`400 Bad Request` レスポンスを送信します。 + +## 依存関係 + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## 使い方 + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## カスタム設定 + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## 設定 + +```go +type Config struct { + // Skipper defines a function to skip middleware. + Skipper middleware.Skipper + + // BeforeFunc defines a function which is executed just before the middleware. + BeforeFunc middleware.BeforeFunc + + // SuccessHandler defines a function executed for a valid token. If it returns an + // error, the middleware stops the handler chain and returns that error. + SuccessHandler func(c *echo.Context) error + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) + // or invalid key, and may be used to define a custom JWT error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users; the handler can set a default + // public JWT token value in the request and continue. + ErrorHandler func(c *echo.Context, err error) error + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool + + // ContextKey is the key under which user information from the token is stored in the context. + // Optional. Default value "user". + ContextKey string + + // SigningKey is the signing key used to validate the token. One of the three options + // to provide a token validation key. Order of precedence: user-defined KeyFunc, + // SigningKeys, then SigningKey. + // Required if neither a user-defined KeyFunc nor SigningKeys is provided. + SigningKey any + + // SigningKeys is a map of signing keys to validate tokens using the kid field. One of + // the three options to provide a token validation key. + // Required if neither a user-defined KeyFunc nor SigningKey is provided. + SigningKeys map[string]any + + // SigningMethod is the signing method used to check the token's signing algorithm. + // Not checked when a user-defined KeyFunc is provided. + // Optional. Default value HS256. + SigningMethod string + + // KeyFunc supplies the public key for token validation. It must verify the signing + // algorithm and select the proper key. Useful when tokens are issued by an external + // party. When provided, SigningKey, SigningKeys and SigningMethod are ignored. + // One of the three options to provide a token validation key, and not used if a + // custom ParseTokenFunc is set. + KeyFunc jwt.Keyfunc + + // TokenLookup is a string in the form ":" or + // ":,:" used to extract the token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" or "header::" + // trims a static prefix from the extracted value. For JWT tokens with + // `Authorization: Bearer `, the prefix to cut is `Bearer ` (note the space). + // If the prefix is empty, the whole value is returned. + // - "query:" + // - "param:" + // - "cookie:" + // - "form:" + // Multiple sources example: "header:Authorization:Bearer ,cookie:myowncookie". + TokenLookup string + + // TokenLookupFuncs is a list of user-defined functions that extract the JWT token + // from the context. One of two options to provide a token extractor. Order of + // precedence: TokenLookupFuncs, then TokenLookup. Both may be provided. + TokenLookupFuncs []middleware.ValuesExtractor + + // ParseTokenFunc parses the token from the given auth string, returning an error when + // parsing fails or the token is invalid. + // Defaults to an implementation using github.com/golang-jwt/jwt. + ParseTokenFunc func(c *echo.Context, auth string) (any, error) + + // NewClaimsFunc returns the extendable claims defining token content. Used by the + // default ParseTokenFunc; not used if a custom ParseTokenFunc is set. + // Optional. Defaults to a function returning jwt.MapClaims. + NewClaimsFunc func(c *echo.Context) jwt.Claims +} +``` + +## 例 + +完全な例は [JWT cookbook](/ja/cookbook/jwt/) を参照してください。 diff --git a/site/src/content/docs/ja/middleware/key-auth.md b/site/src/content/docs/ja/middleware/key-auth.md new file mode 100644 index 00000000..596625d4 --- /dev/null +++ b/site/src/content/docs/ja/middleware/key-auth.md @@ -0,0 +1,91 @@ +--- +title: Key Auth +description: header、query、form、Cookie から API key を検証する key ベース認証ミドルウェアです。 +sidebar: + order: 11 +--- + +Key Auth ミドルウェアは key ベース認証を提供します。 + +- 有効な key の場合、次のハンドラを呼び出します。 +- 無効な key の場合、`401 Unauthorized` レスポンスを送信します。 +- key がない場合、`400 Bad Request` レスポンスを送信します。 + +## 使い方 + +```go +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil +})) +``` + +## カスタム設定 + +```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 + }, +})) +``` + +## 設定 + +```go +type KeyAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // KeyLookup is a string in the form ":" or + // ":,:" used to extract the key from the request. + // Optional. Default value "header:Authorization:Bearer ". + // Possible values: + // - "header:" or "header::" + // trims a static prefix from the extracted value. For + // `Authorization: Basic `, the prefix to remove is `Basic `. + // - "query:" + // - "form:" + // - "cookie:" + // Multiple sources example: "header:Authorization,header:X-Api-Key". + KeyLookup string + + // AllowedCheckLimit sets how many KeyLookup values are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + AllowedCheckLimit uint + + // Validator validates the key. + // Required. + Validator KeyAuthValidator + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) or + // invalid key, and may be used to define a custom error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users. + ErrorHandler KeyAuthErrorHandler + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool +} +``` + +`Validator` のシグネチャは次のとおりです。 + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### デフォルト設定 + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/ja/middleware/logger.md b/site/src/content/docs/ja/middleware/logger.md new file mode 100644 index 00000000..2c4656e7 --- /dev/null +++ b/site/src/content/docs/ja/middleware/logger.md @@ -0,0 +1,236 @@ +--- +title: リクエストロガー +description: 構造化ログライブラリと統合できる、完全にカスタマイズ可能なリクエストログです。 +sidebar: + order: 12 +--- + +`RequestLogger` ミドルウェアは各 HTTP リクエストの情報をログに記録します。 +何をどのように記録するかを完全にカスタマイズできるため、サードパーティの +(構造化ログ)ライブラリとの利用に適しています。 + +logger が抽出できる値は、`RequestLoggerConfig` の bool フィールドと slice フィールドで制御されます。 +フィールドを有効化する(例:`LogStatus: true`)と、その値が `LogValuesFunc` に渡される +`RequestLoggerValues` に設定されます。 + +```go +type RequestLoggerConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // BeforeNextFunc is called before the next middleware or handler in the chain. + BeforeNextFunc func(c *echo.Context) + + // LogValuesFunc is called with the values extracted by the logger from the + // request/response. + // Mandatory. + LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error + + // HandleError instructs the logger to call the global error handler when the next + // middleware/handler returns an error. A side effect is that the response is then + // committed and sent, so middlewares up the chain can no longer change the status + // code or body. + HandleError bool + + // LogLatency records the duration of the rest of the handler chain (the next(c) call). + LogLatency bool + // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). + LogProtocol bool + // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. + LogRemoteIP bool + // LogHost extracts the request host value (for example example.com). + LogHost bool + // LogMethod extracts the request method (for example GET). + LogMethod bool + // LogURI extracts the request URI (for example /list?lang=en&page=1). + LogURI bool + // LogURIPath extracts the request URI path part (for example /list). + LogURIPath bool + // LogRoutePath extracts the route path the request matched (for example /user/:id). + LogRoutePath bool + // LogRequestID extracts the request ID from the X-Request-ID request header, or the + // response if the request did not have a value. + LogRequestID bool + // LogReferer extracts the request referer value. + LogReferer bool + // LogUserAgent extracts the request user agent value. + LogUserAgent bool + // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, + // the status code is taken from it. + LogStatus bool + // LogError extracts the error returned from the handler chain. + LogError bool + // LogContentLength extracts the Content-Length header value. Note: this can differ + // from the actual request body size as it may be spoofed. + LogContentLength bool + // LogResponseSize extracts the response content length. Note: when used with Gzip + // middleware this value may not always be correct. + LogResponseSize bool + // LogHeaders extracts the given list of request headers. A slice of values is logged + // per header since a request can contain more than one. Names are canonicalized with + // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). + LogHeaders []string + // LogQueryParams extracts the given list of query parameters from the request URI. A + // slice of values is logged per name since a request can repeat a parameter. + LogQueryParams []string + // LogFormValues extracts the given list of form values from the request body and URI. + // A slice of values is logged per name since a request can repeat a value. + LogFormValues []string +} +``` + +## 例 + +### fmt.Printf + +```go +skipper := func(c *echo.Context) bool { + // Skip the health check endpoint. + return c.Request().URL.Path == "/health" +} +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + Skipper: skipper, + BeforeNextFunc: func(c *echo.Context) { + c.Set("customValueFromContext", 42) + }, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + value, _ := c.Get("customValueFromContext").(int) + fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) + return nil + }, +})) +``` + +出力例: + +```text +REQUEST: uri: /hello, status: 200, custom-value: 42 +``` + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +```go +logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogError: true, + HandleError: true, // forwards the error to the global error handler so it can pick the status code + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + ) + } else { + logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.String("err", v.Error.Error()), + ) + } + return nil + }, +})) +``` + +出力例: + +```text +{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} +``` + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +```go +logger := zerolog.New(os.Stdout) +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger.Info(). + Str("URI", v.URI). + Int("status", v.Status). + Msg("request") + return nil + }, +})) +``` + +出力例: + +```text +{"level":"info","URI":"/hello","status":200,"message":"request"} +``` + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +```go +logger, _ := zap.NewProduction() +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger.Info("request", + zap.String("URI", v.URI), + zap.Int("status", v.Status), + ) + return nil + }, +})) +``` + +出力例: + +```text +{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} +``` + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +```go +log := logrus.New() +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { + log.WithFields(logrus.Fields{ + "URI": values.URI, + "status": values.Status, + }).Info("request") + return nil + }, +})) +``` + +出力例: + +```text +time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 +``` + +## トラブルシューティング + +### panic: missing LogValuesFunc callback function for request logger middleware + +この panic は、必須の `LogValuesFunc` callback が未設定のときに発生します。 +`LogValuesFunc` シグネチャに一致する関数を定義し、設定に割り当ててください。 + +```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, +})) +``` + +### ログ内のパラメーターが空 + +`LogValuesFunc` 内で `v.URI` や `v.Status` などが空の場合、対応する抽出フラグ +(`LogStatus`、`LogURI` など)が設定で `true` になっているか確認してください。 +各値は、そのフラグが有効な場合にのみ設定されます。 diff --git a/site/src/content/docs/ja/middleware/method-override.md b/site/src/content/docs/ja/middleware/method-override.md new file mode 100644 index 00000000..f47fec0e --- /dev/null +++ b/site/src/content/docs/ja/middleware/method-override.md @@ -0,0 +1,52 @@ +--- +title: メソッドオーバーライド +description: header、form、query の値で POST リクエストの HTTP メソッドを上書きします。 +sidebar: + order: 13 +--- + +Method Override ミドルウェアは、リクエストから上書きされたメソッドを読み取り、 +元のメソッドの代わりに使います。 + +:::note +セキュリティ上の理由により、上書きできるのは `POST` メソッドだけです。 +::: + +## 使い方 + +```go +e.Pre(middleware.MethodOverride()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +メソッドは `MethodFromHeader`、`MethodFromForm`、`MethodFromQuery` から取得できます。 + +## 設定 + +```go +type MethodOverrideConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Getter is a function that gets the overridden method from the request. + // Optional. Default value MethodFromHeader(echo.HeaderXHTTPMethodOverride). + Getter MethodOverrideGetter +} +``` + +### デフォルト設定 + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/site/src/content/docs/ja/middleware/open-telemetry.md b/site/src/content/docs/ja/middleware/open-telemetry.md new file mode 100644 index 00000000..cef06a72 --- /dev/null +++ b/site/src/content/docs/ja/middleware/open-telemetry.md @@ -0,0 +1,81 @@ +--- +title: OpenTelemetry +description: Echo の HTTP リクエスト向け OpenTelemetry インストルメンテーションです。 +sidebar: + order: 14 +--- + +[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) は、HTTP リクエスト向けの +OpenTelemetry インストルメンテーションを提供するミドルウェアです。 + +OpenTelemetry は、クラウドネイティブアプリケーション向けのインストルメンテーションを提供する +オープンソースツール群です。 + +- [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/) + +## 使い方 + +Go modules で OpenTelemetry ミドルウェア依存関係を追加します。 + +```bash +go get github.com/labstack/echo-opentelemetry +``` + +ミドルウェアと OpenTelemetry trace API を import します。 + +```go +import ( + echootel "github.com/labstack/echo-opentelemetry" + "go.opentelemetry.io/otel/trace" +) +``` + +完全な設定で登録します。 + +```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, +})) +``` + +設定オプションについては [`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28) struct を参照してください。 + +サーバー名だけを指定する簡略形式でミドルウェアを追加します。 + +```go +e.Use(echootel.NewMiddleware("app.example.com")) +``` + +設定オプション付きでミドルウェアを追加します。 + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + TracerProvider: tp, +})) +``` + +Echo コンテキストから tracer を取得します。 + +```go +tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) +``` + +## 例 + +[example](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) は +metrics と spans を stdout にエクスポートしますが、任意の exporter(OTLP など)を使えます。 +[OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters) ドキュメントを参照してください。 diff --git a/site/src/content/docs/ja/middleware/prometheus.md b/site/src/content/docs/ja/middleware/prometheus.md new file mode 100644 index 00000000..663ca94b --- /dev/null +++ b/site/src/content/docs/ja/middleware/prometheus.md @@ -0,0 +1,284 @@ +--- +title: Prometheus +description: Echo の HTTP リクエスト向け Prometheus metrics を生成します。 +sidebar: + order: 15 +--- + +[Echo Prometheus](https://github.com/labstack/echo-prometheus) ミドルウェアは、HTTP リクエスト向けの +Prometheus metrics を生成します。 + +## 使い方 + +必要な module を追加します。 + +```bash +go get github.com/labstack/echo-prometheus +``` + +Prometheus ミドルウェアと、収集した metrics を提供するルートを追加します。 + +```go +e := echo.New() +e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics +e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics +``` + +## 例 + +metrics を収集する同じサーバーから metrics を提供します。 + +```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) + } +} +``` + +別ポートで metrics を提供します。 + +```go +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + + go func() { + metrics := echo.New() // this Echo will run on separate port 8081 + metrics.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + if err := metrics.Start(":8081"); err != nil { + e.Logger.Error("failed to start metrics server", "error", err) + } + }() + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +出力例(最初の例): + +```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 +... +``` + +## カスタム設定 + +### カスタム Prometheus metrics を提供する + +Prometheus のデフォルト registry でカスタム metrics を使います。 + +```go +package main + +import ( + "log" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + customCounter := prometheus.NewCounter( // create a new counter metric + prometheus.CounterOpts{ + Name: "custom_requests_total", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", + }, + ) + if err := prometheus.Register(customCounter); err != nil { // register the counter with the default registry + log.Fatal(err) + } + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + AfterNext: func(c *echo.Context, err error) { + customCounter.Inc() // increment the counter after every request + }, + })) + e.GET("/metrics", echoprometheus.NewHandler()) // register a route to serve gathered metrics + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +または独自の registry を作成し、そこにカスタム metrics を登録します。 + +```go +package main + +import ( + "log" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + customRegistry := prometheus.NewRegistry() // create a custom registry for your custom metrics + customCounter := prometheus.NewCounter( // create a new counter metric + prometheus.CounterOpts{ + Name: "custom_requests_total", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", + }, + ) + if err := customRegistry.Register(customCounter); err != nil { // register the counter with the custom registry + log.Fatal(err) + } + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + AfterNext: func(c *echo.Context, err error) { + customCounter.Inc() // increment the counter after every request + }, + Registerer: customRegistry, // use the custom registry instead of the default Prometheus registry + })) + e.GET("/metrics", echoprometheus.NewHandlerWithConfig(echoprometheus.HandlerConfig{Gatherer: customRegistry})) // serve metrics from the custom registry + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### URL をスキップする + +skipper を渡すと、特定 URL の metrics 生成を避けられます。 + +```go +package main + +import ( + "net/http" + "strings" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + mwConfig := echoprometheus.MiddlewareConfig{ + Skipper: func(c *echo.Context) bool { + return strings.HasPrefix(c.Path(), "/testurl") + }, // does not gather metrics on routes starting with `/testurl` + } + e.Use(echoprometheus.NewMiddlewareWithConfig(mwConfig)) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## 複雑なシナリオ + +デフォルトの `echoprometheus` metric 定義を変更します。 + +```go +package main + +import ( + "net/http" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + // Labels of default metrics can be modified or added with the `LabelFuncs` function. + LabelFuncs: map[string]echoprometheus.LabelValueFunc{ + "scheme": func(c *echo.Context, err error) string { // additional custom label + return c.Scheme() + }, + "host": func(c *echo.Context, err error) string { // overrides the default 'host' label value + return "y_" + c.Request().Host + }, + }, + // The `echoprometheus` middleware registers the following metrics by default: + // - Histogram: request_duration_seconds + // - Histogram: response_size_bytes + // - Histogram: request_size_bytes + // - Counter: requests_total + // which can be modified with the `HistogramOptsFunc` and `CounterOptsFunc` functions. + HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { + if opts.Name == "request_duration_seconds" { + opts.Buckets = []float64{1000.0, 10_000.0, 100_000.0, 1_000_000.0} // 1KB, 10KB, 100KB, 1MB + } + return opts + }, + CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts { + if opts.Name == "requests_total" { + opts.ConstLabels = prometheus.Labels{"my_const": "123"} + } + return opts + }, + })) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/ja/middleware/proxy.md b/site/src/content/docs/ja/middleware/proxy.md new file mode 100644 index 00000000..163d4c28 --- /dev/null +++ b/site/src/content/docs/ja/middleware/proxy.md @@ -0,0 +1,134 @@ +--- +title: プロキシ +description: 負荷分散付きの HTTP と WebSocket のリバースプロキシミドルウェアです。 +sidebar: + order: 16 +--- + +Proxy は HTTP/WebSocket リバースプロキシミドルウェアを提供します。設定された負荷分散方式を使って、 +リクエストを上流サーバーへ転送します。 + +## 使い方 + +```go +url1, err := url.Parse("http://localhost:8081") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +url2, err := url.Parse("http://localhost:8082") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ + { + URL: url1, + }, + { + URL: url2, + }, +}))) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## 設定 + +```go +type ProxyConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Balancer defines a load balancing technique. + // Required. + Balancer ProxyBalancer + + // RetryCount defines the number of times a failed proxied request should be retried + // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried. + RetryCount int + + // RetryFilter defines a function used to determine if a failed request to a + // ProxyTarget should be retried. The RetryFilter will only be called when the number + // of previous retries is less than RetryCount. If the function returns true, the + // request will be retried. The provided error indicates the reason for the request + // failure. When the ProxyTarget is unavailable, the error will be an instance of + // echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error + // will indicate an internal error in the Proxy middleware. When a RetryFilter is not + // specified, all requests that fail with http.StatusBadGateway will be retried. A custom + // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is + // only called when the request to the target fails, or an internal error in the Proxy + // middleware has occurred. Successful requests that return a non-200 response code cannot + // be retried. + RetryFilter func(c *echo.Context, e error) bool + + // ErrorHandler defines a function which can be used to return custom errors from + // the Proxy middleware. ErrorHandler is only invoked when there has been + // either an internal error in the Proxy middleware or the ProxyTarget is + // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked + // when a ProxyTarget returns a non-200 response. In these cases, the response + // is already written so errors cannot be modified. ErrorHandler is only + // invoked after all retry attempts have been exhausted. + ErrorHandler func(c *echo.Context, err error) error + + // Rewrite defines URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Examples: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rewrite map[string]string + + // RegexRewrite defines rewrite rules using regexp.Regexp with captures. + // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. + // Example: + // "^/old/[0.9]+/": "/new", + // "^/api/.+?/(.*)": "/v2/$1", + RegexRewrite map[*regexp.Regexp]string + + // Context key to store selected ProxyTarget into context. + // Optional. Default value "target". + ContextKey string + + // To customize the transport to remote. + // Examples: If custom TLS certificates are required. + Transport http.RoundTripper + + // ModifyResponse defines function to modify response from ProxyTarget. + ModifyResponse func(*http.Response) error +} +``` + +### デフォルト設定 + +| 名前 | 値 | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### 正規表現ベースのルール + +プロキシリクエストの高度な書き換えでは、正規表現でルールを定義することもできます。 +通常のキャプチャグループは `()` で定義し、書き換え後のパスでインデックス +(`$1`、`$2` など)により参照できます。 + +`RegexRewrite` と通常の `Rewrite` ルールは組み合わせられます。 + +```go +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ + Balancer: rrb, + Rewrite: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRewrite: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` + +完全な例は[リバースプロキシ](/ja/cookbook/reverse-proxy/) cookbook を参照してください。 diff --git a/site/src/content/docs/ja/middleware/rate-limiter.md b/site/src/content/docs/ja/middleware/rate-limiter.md new file mode 100644 index 00000000..584ed20a --- /dev/null +++ b/site/src/content/docs/ja/middleware/rate-limiter.md @@ -0,0 +1,108 @@ +--- +title: レートリミッター +description: 特定の IP または識別子からのリクエスト数を一定期間内で制限します。 +sidebar: + order: 17 +--- + +`RateLimiter` は、特定の IP または識別子からサーバーへ送信されるリクエスト数を一定期間内で +制限するレートリミッターミドルウェアを提供します。 + +デフォルトでは、インメモリ store がリクエストを追跡します。デフォルトのインメモリ実装は +正確性に重点を置いており、大量の同時リクエストや多数の異なる識別子(>16k)には最適ではない場合があります。 + +## 使い方 + +アプリケーションにレート制限を追加するには、`RateLimiter` ミドルウェアを追加します。 +次の例では、デフォルトのインメモリ store を使ってアプリケーションを 20 requests/sec に制限します。 + +```go +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) +``` + +:::note +指定した rate が浮動小数点数の場合、`Burst` は rate を切り捨てた値として扱われます。 +::: + +## カスタム設定 + +```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)) +``` + +### エラー + +```go +var ( + // ErrRateLimitExceeded denotes an error raised when the rate limit is exceeded. + ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") + // ErrExtractorError denotes an error raised when the extractor function is unsuccessful. + ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier") +) +``` + +:::tip +独自の store を実装するには、`RateLimiterStore` インターフェイスを満たして +`RateLimiterConfig` に渡してください。 +::: + +## 設定 + +```go +type RateLimiterConfig struct { + Skipper Skipper + BeforeFunc BeforeFunc + // IdentifierExtractor uses echo.Context to extract the identifier for a visitor. + IdentifierExtractor Extractor + // Store defines a store for the rate limiter. + Store RateLimiterStore + // ErrorHandler provides a handler to be called when IdentifierExtractor returns a non-nil error. + ErrorHandler func(c *echo.Context, err error) error + // DenyHandler provides a handler to be called when RateLimiter denies access. + DenyHandler func(c *echo.Context, identifier string, err error) error +} +``` + +### デフォルト設定 + +```go +// DefaultRateLimiterConfig defines default values for RateLimiterConfig. +var DefaultRateLimiterConfig = RateLimiterConfig{ + Skipper: DefaultSkipper, + IdentifierExtractor: func(c *echo.Context) (string, error) { + id := c.RealIP() + return id, nil + }, + ErrorHandler: func(c *echo.Context, err error) error { + return &echo.HTTPError{ + Code: ErrExtractorError.Code, + Message: ErrExtractorError.Message, + Internal: err, + } + }, + DenyHandler: func(c *echo.Context, identifier string, err error) error { + return &echo.HTTPError{ + Code: ErrRateLimitExceeded.Code, + Message: ErrRateLimitExceeded.Message, + Internal: err, + } + }, +} +``` diff --git a/site/src/content/docs/ja/middleware/recover.md b/site/src/content/docs/ja/middleware/recover.md new file mode 100644 index 00000000..08160d00 --- /dev/null +++ b/site/src/content/docs/ja/middleware/recover.md @@ -0,0 +1,59 @@ +--- +title: Recover +description: チェーン内のどこで起きた panic からも復旧し、一元化されたエラーハンドラへ委譲します。 +sidebar: + order: 18 +--- + +Recover ミドルウェアは、チェーン内のどこで発生した panic からも復旧し、スタックトレースを出力して、 +一元化された [HTTPErrorHandler](/ja/guide/customization/#http-error-handler) に制御を渡します。 + +## 使い方 + +```go +e.Use(middleware.Recover()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +上の例では `StackSize` を 1 KB にし、`DisableStackAll` と `DisablePrintStack` はデフォルト値を使います。 + +## 設定 + +```go +type RecoverConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Size of the stack to be printed. + // Optional. Default value 4KB. + StackSize int + + // DisableStackAll disables formatting stack traces of all other goroutines + // into the buffer after the trace for the current goroutine. + // Optional. Default value false. + DisableStackAll bool + + // DisablePrintStack disables printing the stack trace. + // Optional. Default value false. + DisablePrintStack bool +} +``` + +### デフォルト設定 + +```go +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/site/src/content/docs/ja/middleware/redirect.md b/site/src/content/docs/ja/middleware/redirect.md new file mode 100644 index 00000000..918c2dd8 --- /dev/null +++ b/site/src/content/docs/ja/middleware/redirect.md @@ -0,0 +1,100 @@ +--- +title: リダイレクト +description: HTTP/HTTPS と www/non-www のバリエーション間でリクエストをリダイレクトします。 +sidebar: + order: 19 +--- + +## HTTPS リダイレクト + +HTTPS redirect ミドルウェアは HTTP リクエストを HTTPS へリダイレクトします。 +たとえば `http://labstack.com` は `https://labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPS WWW リダイレクト + +HTTPS WWW redirect は HTTP リクエストを www HTTPS へリダイレクトします。 +たとえば `http://labstack.com` は `https://www.labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPS NonWWW リダイレクト + +HTTPS NonWWW redirect は HTTP リクエストを non-www HTTPS へリダイレクトします。 +たとえば `http://www.labstack.com` は `https://labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.HTTPSNonWWWRedirect()) +``` + +## WWW リダイレクト + +WWW redirect は non-www リクエストを www へリダイレクトします。 +たとえば `http://labstack.com` は `http://www.labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWW リダイレクト + +NonWWW redirect は www リクエストを non-www へリダイレクトします。 +たとえば `http://www.labstack.com` は `http://labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +上の例は、HTTP リクエストをステータスコード `307 - StatusTemporaryRedirect` で HTTPS へリダイレクトします。 + +## 設定 + +```go +type RedirectConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional. Default value http.StatusMovedPermanently. + Code int +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/site/src/content/docs/ja/middleware/request-id.md b/site/src/content/docs/ja/middleware/request-id.md new file mode 100644 index 00000000..331fb759 --- /dev/null +++ b/site/src/content/docs/ja/middleware/request-id.md @@ -0,0 +1,89 @@ +--- +title: リクエスト ID +description: 各リクエストに一意の ID を生成します。 +sidebar: + order: 20 +--- + +Request ID ミドルウェアはリクエストに一意の ID を生成します。 + +## 使い方 + +```go +e.Use(middleware.RequestID()) +``` + +例: + +```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) + } +} +``` + +## カスタム設定 + +```go +e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ + Generator: func() string { + return customGenerator() + }, +})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, // random 32-character string + TargetHeader: echo.HeaderXRequestID, +} +``` + +## ID を設定する + +リクエスト元は `X-Request-ID` header で ID を設定できます。 + +### リクエスト + +```sh +curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param" +``` + +### ログ + +```js +{"time":"2017-11-13T20:26:28.6438003+01:00","id":"3","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/?my=param","my":"param","status":200, "latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":13} +``` diff --git a/site/src/content/docs/ja/middleware/rewrite.md b/site/src/content/docs/ja/middleware/rewrite.md new file mode 100644 index 00000000..68ba1e1e --- /dev/null +++ b/site/src/content/docs/ja/middleware/rewrite.md @@ -0,0 +1,87 @@ +--- +title: リライト +description: 設定されたルールに基づいて URL パスを書き換えます。 +sidebar: + order: 21 +--- + +Rewrite ミドルウェアは、指定されたルールに基づいて URL パスを書き換えます。 +後方互換性を保つ場合や、より短く分かりやすいリンクを作る場合に役立ちます。 + +## 使い方 + +```go +e.Pre(middleware.Rewrite(map[string]string{ + "/old": "/new", + "/api/*": "/$1", + "/js/*": "/public/javascripts/$1", + "/users/*/orders/*": "/user/$1/order/$2", +})) +``` + +アスタリスクでキャプチャされた値は、`$1`、`$2` などのインデックスで取得できます。 +各アスタリスクは非貪欲です(キャプチャグループ `(.*?)` に変換されます)。 +複数のアスタリスクを使う場合、末尾の `*` は残りのパスにマッチします。 + +:::caution +Rewrite ミドルウェアは、ルーターより前に実行されるよう `Echo#Pre()` で登録してください。 +::: + +## カスタム設定 + +```go +e := echo.New() +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) +``` + +## 設定 + +```go +type RewriteConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + // Required. + Rules map[string]string + + // RegexRules defines the URL path rewrite rules using regexp.Regexp with captures. + // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. + // Example: + // "^/old/[0.9]+/": "/new", + // "^/api/.+?/(.*)": "/v2/$1", + RegexRules map[*regexp.Regexp]string +} +``` + +デフォルト設定: + +| 名前 | 値 | +| ------- | -------------- | +| Skipper | DefaultSkipper | + +### 正規表現ベースのルール + +パスの高度な書き換えでは、正規表現でルールを定義することもできます。 +通常のキャプチャグループは `()` で定義し、書き換え後のパスでインデックス +(`$1`、`$2` など)により参照できます。 + +`RegexRules` と通常の `Rules` は組み合わせられます。 + +```go +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{ + Rules: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRules: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` diff --git a/site/src/content/docs/ja/middleware/secure.md b/site/src/content/docs/ja/middleware/secure.md new file mode 100644 index 00000000..12121cb7 --- /dev/null +++ b/site/src/content/docs/ja/middleware/secure.md @@ -0,0 +1,113 @@ +--- +title: セキュア +description: XSS、コンテンツスニッフィング、クリックジャッキング、その他のインジェクション攻撃を防ぎます。 +sidebar: + order: 22 +--- + +Secure ミドルウェアは、クロスサイトスクリプティング(XSS)、コンテンツタイプスニッフィング、 +クリックジャッキング、安全でない接続、その他のコードインジェクション攻撃への防御を提供します。 + +## 使い方 + +```go +e.Use(middleware.Secure()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +:::note +空の `XSSProtection`、`ContentTypeNosniff`、`XFrameOptions`、または +`ContentSecurityPolicy` を渡すと、その防御が無効になります。 +::: + +## 設定 + +```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 ,