From af934b7db725a2829b29c1de482fc9af19192981 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Tue, 16 Jun 2026 12:33:45 -0700 Subject: [PATCH] docs: document slice binding (#170) and add JWT timing-attack caveat (#100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #170 — Struct-tag binding to a slice field (repeated query/path/form/header values collected into []T) was undocumented. Add a "Slices" subsection to the binding guide with a `[]string` + repeated-query-param example, across all five locales (translated heading/lead-in, identical code block). #100 — The JWT cookbook login handler compared hard-coded credentials with a plain `!=` and only a "// Throws unauthorized error" comment. Replace it with a caveat noting it's demo-only and that production code should verify a hashed password using a constant-time compare to avoid timing attacks (all locales). Co-Authored-By: Claude Opus 4.8 (1M context) --- site/src/content/docs/cookbook/jwt.md | 4 +++- site/src/content/docs/es/cookbook/jwt.md | 4 +++- site/src/content/docs/es/guide/binding.md | 18 ++++++++++++++++++ site/src/content/docs/guide/binding.md | 18 ++++++++++++++++++ site/src/content/docs/ja/cookbook/jwt.md | 4 +++- site/src/content/docs/ja/guide/binding.md | 18 ++++++++++++++++++ site/src/content/docs/pt-br/cookbook/jwt.md | 4 +++- site/src/content/docs/pt-br/guide/binding.md | 18 ++++++++++++++++++ site/src/content/docs/zh-cn/cookbook/jwt.md | 4 +++- site/src/content/docs/zh-cn/guide/binding.md | 17 +++++++++++++++++ 10 files changed, 104 insertions(+), 5 deletions(-) diff --git a/site/src/content/docs/cookbook/jwt.md b/site/src/content/docs/cookbook/jwt.md index c4f126f1..c0abb058 100644 --- a/site/src/content/docs/cookbook/jwt.md +++ b/site/src/content/docs/cookbook/jwt.md @@ -47,7 +47,9 @@ func login(c *echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") - // Throws unauthorized error + // Demo only: hard-coded credentials compared in plain text. In production, + // look up the user and verify a hashed password with a constant-time compare + // (e.g. golang.org/x/crypto/bcrypt's CompareHashAndPassword) to avoid timing attacks. if username != "jon" || password != "shhh!" { return echo.ErrUnauthorized } diff --git a/site/src/content/docs/es/cookbook/jwt.md b/site/src/content/docs/es/cookbook/jwt.md index e746c3af..a8792aeb 100644 --- a/site/src/content/docs/es/cookbook/jwt.md +++ b/site/src/content/docs/es/cookbook/jwt.md @@ -47,7 +47,9 @@ func login(c *echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") - // Throws unauthorized error + // Demo only: hard-coded credentials compared in plain text. In production, + // look up the user and verify a hashed password with a constant-time compare + // (e.g. golang.org/x/crypto/bcrypt's CompareHashAndPassword) to avoid timing attacks. if username != "jon" || password != "shhh!" { return echo.ErrUnauthorized } diff --git a/site/src/content/docs/es/guide/binding.md b/site/src/content/docs/es/guide/binding.md index 49309a63..135b49ed 100644 --- a/site/src/content/docs/es/guide/binding.md +++ b/site/src/content/docs/es/guide/binding.md @@ -44,6 +44,24 @@ if err := c.Bind(&user); err != nil { Los campos de path, query, header y form requieren un **tag explícito**. JSON y XML usan el nombre del campo del struct cuando se omite el tag, igual que la biblioteca estándar. +### Slices + +Los valores repetidos de query, path, formulario o header se enlazan a un campo +slice, que recoge cada aparición: + +```go +// GET /search?tag=go&tag=web&tag=api +type Filter struct { + Tags []string `query:"tag"` +} + +var f Filter +if err := c.Bind(&f); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +// f.Tags == []string{"go", "web", "api"} +``` + ### Content types del body Al decodificar el body del request, el header `Content-Type` selecciona el decoder: diff --git a/site/src/content/docs/guide/binding.md b/site/src/content/docs/guide/binding.md index 894cd411..e06b6034 100644 --- a/site/src/content/docs/guide/binding.md +++ b/site/src/content/docs/guide/binding.md @@ -44,6 +44,24 @@ if err := c.Bind(&user); err != nil { Path, query, header, and form fields require an **explicit tag**. JSON and XML fall back to the struct field name when the tag is omitted, matching the standard library. +### Slices + +Repeated query, path, form, or header values bind to a slice field — the field +collects every occurrence: + +```go +// GET /search?tag=go&tag=web&tag=api +type Filter struct { + Tags []string `query:"tag"` +} + +var f Filter +if err := c.Bind(&f); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +// f.Tags == []string{"go", "web", "api"} +``` + ### Body content types When decoding the request body, the `Content-Type` header selects the decoder: diff --git a/site/src/content/docs/ja/cookbook/jwt.md b/site/src/content/docs/ja/cookbook/jwt.md index 95e6bf84..18cd4c6d 100644 --- a/site/src/content/docs/ja/cookbook/jwt.md +++ b/site/src/content/docs/ja/cookbook/jwt.md @@ -46,7 +46,9 @@ func login(c *echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") - // Throws unauthorized error + // Demo only: hard-coded credentials compared in plain text. In production, + // look up the user and verify a hashed password with a constant-time compare + // (e.g. golang.org/x/crypto/bcrypt's CompareHashAndPassword) to avoid timing attacks. if username != "jon" || password != "shhh!" { return echo.ErrUnauthorized } diff --git a/site/src/content/docs/ja/guide/binding.md b/site/src/content/docs/ja/guide/binding.md index da9283b5..a05d92b5 100644 --- a/site/src/content/docs/ja/guide/binding.md +++ b/site/src/content/docs/ja/guide/binding.md @@ -46,6 +46,24 @@ if err := c.Bind(&user); err != nil { JSON と XML はタグが省略された場合、標準ライブラリと同じように struct フィールド名へ フォールバックします。 +### スライス + +繰り返し指定されたクエリ・パス・フォーム・ヘッダーの値はスライスフィールドにバインドされ、 +すべての出現が収集されます: + +```go +// GET /search?tag=go&tag=web&tag=api +type Filter struct { + Tags []string `query:"tag"` +} + +var f Filter +if err := c.Bind(&f); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +// f.Tags == []string{"go", "web", "api"} +``` + ### ボディのコンテンツタイプ リクエストボディをデコードするときは、`Content-Type` header によってデコーダーが選ばれます。 diff --git a/site/src/content/docs/pt-br/cookbook/jwt.md b/site/src/content/docs/pt-br/cookbook/jwt.md index f26536cf..b30d6aa7 100644 --- a/site/src/content/docs/pt-br/cookbook/jwt.md +++ b/site/src/content/docs/pt-br/cookbook/jwt.md @@ -47,7 +47,9 @@ func login(c *echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") - // Throws unauthorized error + // Demo only: hard-coded credentials compared in plain text. In production, + // look up the user and verify a hashed password with a constant-time compare + // (e.g. golang.org/x/crypto/bcrypt's CompareHashAndPassword) to avoid timing attacks. if username != "jon" || password != "shhh!" { return echo.ErrUnauthorized } diff --git a/site/src/content/docs/pt-br/guide/binding.md b/site/src/content/docs/pt-br/guide/binding.md index 36db4b54..d9989327 100644 --- a/site/src/content/docs/pt-br/guide/binding.md +++ b/site/src/content/docs/pt-br/guide/binding.md @@ -44,6 +44,24 @@ if err := c.Bind(&user); err != nil { Campos de path, query, header e form exigem uma **tag explícita**. JSON e XML usam o nome do campo da struct quando a tag é omitida, igual à biblioteca padrão. +### Slices + +Valores repetidos de query, path, formulário ou header são vinculados a um campo +slice, que coleta cada ocorrência: + +```go +// GET /search?tag=go&tag=web&tag=api +type Filter struct { + Tags []string `query:"tag"` +} + +var f Filter +if err := c.Bind(&f); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +// f.Tags == []string{"go", "web", "api"} +``` + ### Tipos de conteúdo do body Ao decodificar o body do request, o header `Content-Type` seleciona o decoder: diff --git a/site/src/content/docs/zh-cn/cookbook/jwt.md b/site/src/content/docs/zh-cn/cookbook/jwt.md index 633c69d5..204dacb8 100644 --- a/site/src/content/docs/zh-cn/cookbook/jwt.md +++ b/site/src/content/docs/zh-cn/cookbook/jwt.md @@ -46,7 +46,9 @@ func login(c *echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") - // Throws unauthorized error + // Demo only: hard-coded credentials compared in plain text. In production, + // look up the user and verify a hashed password with a constant-time compare + // (e.g. golang.org/x/crypto/bcrypt's CompareHashAndPassword) to avoid timing attacks. if username != "jon" || password != "shhh!" { return echo.ErrUnauthorized } diff --git a/site/src/content/docs/zh-cn/guide/binding.md b/site/src/content/docs/zh-cn/guide/binding.md index e6eb1ef7..25dc27c0 100644 --- a/site/src/content/docs/zh-cn/guide/binding.md +++ b/site/src/content/docs/zh-cn/guide/binding.md @@ -44,6 +44,23 @@ if err := c.Bind(&user); err != nil { 路径、查询、header 和表单字段都需要**显式标签**。JSON 和 XML 在省略标签时会回退到 struct 字段名,这与标准库的行为一致。 +### 切片 + +重复的查询、路径、表单或请求头值会绑定到切片字段——该字段会收集每一个值: + +```go +// GET /search?tag=go&tag=web&tag=api +type Filter struct { + Tags []string `query:"tag"` +} + +var f Filter +if err := c.Bind(&f); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +// f.Tags == []string{"go", "web", "api"} +``` + ### 请求体内容类型 解码请求体时,`Content-Type` header 会选择对应的解码器: