From 1f379483ce8aadfe88f4ddbf3cb0b46a21a018d6 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Mon, 15 Jun 2026 22:50:26 -0700 Subject: [PATCH 1/4] feat(docs): i18n config (es) --- site/astro.config.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 579e3a9d..9fac34e9 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -15,6 +15,7 @@ export default defineConfig({ root: { label: 'English', lang: 'en' }, 'zh-cn': { label: '简体中文', lang: 'zh-CN' }, ja: { label: '日本語', lang: 'ja' }, + es: { label: 'Español', lang: 'es' }, }, logo: { light: './src/assets/logo-light.svg', @@ -79,9 +80,9 @@ export default defineConfig({ // Autogenerated from the content dirs — new pages appear automatically, // ordered by each page's `sidebar.order` frontmatter. sidebar: [ - { 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' } }] }, + { label: 'Guide', translations: { 'zh-CN': '指南', ja: 'ガイド', es: 'Guía' }, items: [{ autogenerate: { directory: 'guide' } }] }, + { label: 'Middleware', translations: { 'zh-CN': '中间件', ja: 'ミドルウェア', es: 'Middleware' }, items: [{ autogenerate: { directory: 'middleware' } }] }, + { label: 'Cookbook', translations: { 'zh-CN': '示例', ja: 'クックブック', es: 'Recetario' }, items: [{ autogenerate: { directory: 'cookbook' } }] }, ], // tune the built-in code theme toward our terminal palette expressiveCode: { themes: ['github-dark', 'github-light'] }, From 34ea72a2383153bd11f63d74480b91b2df75051f Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Tue, 16 Jun 2026 06:16:36 -0700 Subject: [PATCH 2/4] docs(i18n): guide (es) --- site/src/content/docs/es/guide/binding.md | 150 +++++++++ site/src/content/docs/es/guide/context.md | 78 +++++ site/src/content/docs/es/guide/cookies.md | 72 ++++ .../content/docs/es/guide/customization.md | 62 ++++ .../content/docs/es/guide/error-handling.md | 82 +++++ .../src/content/docs/es/guide/installation.md | 57 ++++ site/src/content/docs/es/guide/ip-address.md | 116 +++++++ site/src/content/docs/es/guide/quickstart.md | 76 +++++ site/src/content/docs/es/guide/request.md | 170 ++++++++++ site/src/content/docs/es/guide/response.md | 318 ++++++++++++++++++ site/src/content/docs/es/guide/routing.md | 75 +++++ .../src/content/docs/es/guide/static-files.md | 89 +++++ site/src/content/docs/es/guide/templates.md | 132 ++++++++ site/src/content/docs/es/guide/testing.md | 264 +++++++++++++++ 14 files changed, 1741 insertions(+) create mode 100644 site/src/content/docs/es/guide/binding.md create mode 100644 site/src/content/docs/es/guide/context.md create mode 100644 site/src/content/docs/es/guide/cookies.md create mode 100644 site/src/content/docs/es/guide/customization.md create mode 100644 site/src/content/docs/es/guide/error-handling.md create mode 100644 site/src/content/docs/es/guide/installation.md create mode 100644 site/src/content/docs/es/guide/ip-address.md create mode 100644 site/src/content/docs/es/guide/quickstart.md create mode 100644 site/src/content/docs/es/guide/request.md create mode 100644 site/src/content/docs/es/guide/response.md create mode 100644 site/src/content/docs/es/guide/routing.md create mode 100644 site/src/content/docs/es/guide/static-files.md create mode 100644 site/src/content/docs/es/guide/templates.md create mode 100644 site/src/content/docs/es/guide/testing.md diff --git a/site/src/content/docs/es/guide/binding.md b/site/src/content/docs/es/guide/binding.md new file mode 100644 index 00000000..49309a63 --- /dev/null +++ b/site/src/content/docs/es/guide/binding.md @@ -0,0 +1,150 @@ +--- +title: Binding +description: Analiza datos de request en structs Go tipados desde path, query, header y body. +sidebar: + order: 5 +--- + +Analizar datos de request es una parte crucial de una aplicación web. En Echo esto se llama +_binding_, y puede leer desde cuatro partes de un request HTTP: + +- Parámetros de path de URL +- Parámetros de query de URL +- Headers +- Body del request + +## Binding con tags de struct + +Define un struct con tags que especifican el origen de datos y la clave, y luego llama a `c.Bind()` +con un puntero a él. Aquí el parámetro de query `id` se vincula al campo `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") +} +``` + +### Orígenes de datos + +| Tag | Origen | +| -------- | ------ | +| `query` | Parámetro de query | +| `param` | Parámetro de path | +| `header` | Valor de header | +| `form` | Datos de formulario (query + body) | +| `json` | Body del request (`encoding/json`) | +| `xml` | Body del request (`encoding/xml`) | + +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. + +### Content types del body + +Al decodificar el body del request, el header `Content-Type` selecciona el decoder: + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### Múltiples orígenes y precedencia + +Un campo puede declarar varios orígenes. Los datos se vinculan en este orden, y cada paso +sobrescribe el anterior: + +1. Parámetros de path +2. Parámetros de query (solo GET / DELETE) +3. Body del request + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### Binding directo desde un origen + +```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 +Los headers **no** están incluidos por `c.Bind()`. Vincúlalos directamente con `echo.BindHeaders`. +::: + +:::caution[Seguridad] +No vincules directamente a structs de negocio. Si un struct vinculado expone un campo `IsAdmin bool`, +un body de request `{"IsAdmin": true}` lo establecería. Usa un DTO dedicado y mapéalo explícitamente: +::: + +```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) +}) +``` + +## Binding fluido + +Para binding explícito y type-safe desde un único origen, usa los binders fluidos. Encadenan +la configuración y la ejecutan, recopilando errores: + +```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 +``` + +Binders disponibles: `echo.QueryParamsBinder(c)`, `echo.PathValuesBinder(c)`, +`echo.FormFieldBinder(c)`. Termina una cadena con `BindError()` (primer error) o +`BindErrors()` (todos los errores). `FailFast(false)` ejecuta toda la cadena; está activado por defecto. + +Cada tipo soportado ofrece métodos `Type(...)`, `MustType(...)`, `Types(...)` (slices) y +`MustTypes(...)`, por ejemplo `Int64`, `MustInt64`, `Int64s`. Usa +`BindWithDelimiter("id", &dest, ",")` para separar valores unidos por comas. + +## Binder personalizado + +Registra un binder personalizado mediante `Echo#Binder`: + +```go +type CustomBinder struct{} + +func (cb *CustomBinder) Bind(c *echo.Context, i any) error { + db := new(echo.DefaultBinder) + if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType { + return err + } + // custom logic here + return nil +} + +e.Binder = &CustomBinder{} +``` diff --git a/site/src/content/docs/es/guide/context.md b/site/src/content/docs/es/guide/context.md new file mode 100644 index 00000000..731fe64f --- /dev/null +++ b/site/src/content/docs/es/guide/context.md @@ -0,0 +1,78 @@ +--- +title: Context +description: El objeto por request que transporta request, response, params y helpers. +sidebar: + order: 4 +--- + +`echo.Context` representa el contexto del request HTTP actual. Se pasa un puntero a él +(`*echo.Context`) a cada handler y middleware, y transporta el request y response, +parámetros de path, datos vinculados y helpers para construir responses. + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## Leer entrada + +```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) +``` + +Hay helpers `*Or` equivalentes que devuelven un valor por defecto cuando el valor no está presente: +`c.ParamOr("id", "0")`, `c.QueryParamOr("page", "1")`, `c.FormValueOr(...)`. + +## Escribir responses + +```go +c.String(http.StatusOK, "plain text") +c.JSON(http.StatusOK, payload) +c.JSONPretty(http.StatusOK, payload, " ") +c.HTML(http.StatusOK, "hi") +c.XML(http.StatusOK, payload) +c.Blob(http.StatusOK, "application/pdf", bytes) +c.Stream(http.StatusOK, "application/octet-stream", reader) +c.NoContent(http.StatusNoContent) +c.Redirect(http.StatusFound, "/elsewhere") +``` + +## Archivos + +```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 +``` + +## Almacenamiento por request + +Comparte datos entre middleware y handlers con `Get`/`Set`: + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +El acceso tipado está disponible mediante los helpers genéricos: + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## Binding y validación + +`c.Bind()` analiza datos del request en un struct; consulta [Binding](/es/guide/binding/). + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/es/guide/cookies.md b/site/src/content/docs/es/guide/cookies.md new file mode 100644 index 00000000..857ad549 --- /dev/null +++ b/site/src/content/docs/es/guide/cookies.md @@ -0,0 +1,72 @@ +--- +title: Cookies +description: Crea, lee y lista HTTP cookies usando el tipo estándar http.Cookie. +sidebar: + order: 11 +--- + +Una cookie es una pequeña pieza de datos que un servidor envía al navegador, que el navegador +almacena y vuelve a enviar en requests posteriores. Las cookies permiten que los sitios web +recuerden información con estado, como un carrito de compras, el estado de autenticación o +valores de formularios ingresados previamente. + +Echo usa el tipo estándar `http.Cookie` de Go para agregar y obtener cookies desde +`echo.Context` en un handler. + +## Atributos de Cookie + +| Atributo | Opcional | +| ---------- | -------- | +| `Name` | No | +| `Value` | No | +| `Path` | Sí | +| `Domain` | Sí | +| `Expires` | Sí | +| `Secure` | Sí | +| `HttpOnly` | Sí | + +## Crear una 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") +} +``` + +- Crea la cookie con `new(http.Cookie)`. +- Establece atributos en los campos de `http.Cookie`. +- Llama a `c.SetCookie(cookie)` para agregar un header `Set-Cookie` a la response. + +## Leer una 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") +} +``` + +- Lee una cookie por nombre con `c.Cookie("username")`. +- Accede a sus atributos mediante los campos de `http.Cookie`. + +## Leer todas las cookies + +```go +func readAllCookies(c *echo.Context) error { + for _, cookie := range c.Cookies() { + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + } + return c.String(http.StatusOK, "read all the cookies") +} +``` diff --git a/site/src/content/docs/es/guide/customization.md b/site/src/content/docs/es/guide/customization.md new file mode 100644 index 00000000..e9759c52 --- /dev/null +++ b/site/src/content/docs/es/guide/customization.md @@ -0,0 +1,62 @@ +--- +title: Personalización +description: Personaliza el logger, validator, binder, renderer, serializer y manejo de errores de Echo. +sidebar: + order: 12 +--- + +Echo expone un conjunto de campos en la instancia `Echo` que te permiten reemplazar el +comportamiento integrado con tus propias implementaciones. + +## Logging + +`Echo#Logger` escribe logs estructurados. El handler por defecto emite JSON a `os.Stdout`. + +### Logger personalizado + +El logger es un `*slog.Logger`, por lo que puedes registrar cualquier handler de `slog`: + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` registra un validator para validar payloads de request. + +[Aprende más](/es/guide/request/#validate-data) + +## Binder personalizado + +`Echo#Binder` registra un binder personalizado para binding de payloads de request. + +[Aprende más](/es/guide/binding/#custom-binder) + +## Serializer JSON personalizado + +`Echo#JSONSerializer` registra un serializer JSON personalizado. Consulta `DefaultJSONSerializer` +en [json.go](https://github.com/labstack/echo/blob/master/json.go). + +## Renderer + +`Echo#Renderer` registra un renderer para renderizado de templates. + +[Aprende más](/es/guide/templates/) + +## Handler de errores HTTP + +`Echo#HTTPErrorHandler` registra un handler de errores HTTP personalizado. + +[Aprende más](/es/guide/error-handling/) + +## Callback de ruta + +`Echo#OnAddRoute` registra un callback que se invoca cada vez que se agrega una nueva ruta al +router. + +## Extractor de IP + +`Echo#IPExtractor` controla cómo se determina la dirección IP real del cliente. Para +obtenerla de forma fiable y segura, tu aplicación debe conocer toda su infraestructura. + +[Aprende más](/es/guide/ip-address/) diff --git a/site/src/content/docs/es/guide/error-handling.md b/site/src/content/docs/es/guide/error-handling.md new file mode 100644 index 00000000..170b2622 --- /dev/null +++ b/site/src/content/docs/es/guide/error-handling.md @@ -0,0 +1,82 @@ +--- +title: Manejo de errores +description: Manejo centralizado de errores HTTP devolviendo errores desde handlers y middleware. +sidebar: + order: 6 +--- + +Echo promueve el manejo **centralizado** de errores: los handlers y middleware devuelven un +`error`, y un único handler de errores lo convierte en una response HTTP. Esto mantiene el logging +y el formato de responses en un solo lugar. + +Devuelve un `error` plano o un `*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)` sin mensaje usa el texto del estado (por ejemplo, `"Unauthorized"`). +Echo también incluye errores sentinel como `echo.ErrBadRequest`, `echo.ErrNotFound` y +`echo.ErrUnauthorized`. + +## Handler de errores por defecto + +El handler por defecto de Echo responde en JSON: + +```json +{ "message": "error connecting to redis" } +``` + +Un `error` plano se convierte en `500 Internal Server Error` (el mensaje original se incluye +cuando se ejecuta con errores expuestos). Un `*HTTPError` usa su código de estado y mensaje. + +## Handler de errores personalizado + +Define el tuyo mediante `e.HTTPErrorHandler`; es útil para páginas de error, notificaciones o +enviar errores a un sistema centralizado. + +Comprueba si la response ya se envió con `echo.UnwrapResponse()`, y encuentra un código de +estado en la cadena de errores mediante `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 +En lugar del logger, o además de él, reenvía errores a un servicio externo como +Sentry, Elasticsearch o Splunk desde el handler central. +::: diff --git a/site/src/content/docs/es/guide/installation.md b/site/src/content/docs/es/guide/installation.md new file mode 100644 index 00000000..9d3483fd --- /dev/null +++ b/site/src/content/docs/es/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: Instalación +description: Agrega Echo v5 a tu módulo Go. +sidebar: + order: 2 +--- + +Echo se distribuye como un módulo Go: `github.com/labstack/echo/v5`. + +## Requisitos + +Echo v5 requiere **Go 1.25 o posterior**. + +```bash +go version +``` + +## Agregar a un proyecto + +Dentro de un módulo existente: + +```bash +go get github.com/labstack/echo/v5 +``` + +O inicia un módulo nuevo: + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +Impórtalo en tu código: + +```go +import "github.com/labstack/echo/v5" +``` + +## Versiones + +| Versión | Import path | Estado | +| ------- | ------------------------------- | ------ | +| **v5** | `github.com/labstack/echo/v5` | Actual | +| v4 | `github.com/labstack/echo/v4` | LTS (mantenimiento) | + +:::note +Echo sigue [semantic import versioning](https://go.dev/blog/v2-go-modules): la +versión major forma parte del import path, por lo que v4 y v5 pueden coexistir durante una migración. +::: + +## Mantenerse actualizado + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/es/guide/ip-address.md b/site/src/content/docs/es/guide/ip-address.md new file mode 100644 index 00000000..8b160976 --- /dev/null +++ b/site/src/content/docs/es/guide/ip-address.md @@ -0,0 +1,116 @@ +--- +title: Dirección IP +description: Obtén de forma segura la dirección IP real del cliente detrás de proxies. +sidebar: + order: 14 +--- + +La dirección IP cumple un papel fundamental en HTTP: se usa para control de acceso, +auditoría, análisis geográfico y más. Echo expone `Context#RealIP()` para obtenerla. + +Obtener la IP _real_ del cliente no es trivial, especialmente cuando hay proxies L7 delante +de tu aplicación. En ese caso, la IP real debe transmitirse por HTTP desde los proxies hacia +tu app, pero no debes confiar incondicionalmente en headers HTTP, o corres el riesgo de ser +engañado. **Esto es un riesgo de seguridad.** + +Para obtener la IP de forma fiable y segura, tu aplicación debe conocer toda su infraestructura. +En Echo, esto se configura mediante `Echo#IPExtractor`. + +:::caution +Si no estableces `Echo#IPExtractor` explícitamente, Echo vuelve al comportamiento legacy, +que no es un valor por defecto seguro. +::: + +Empieza con dos preguntas para encontrar el enfoque correcto: + +1. ¿Pones algún proxy HTTP (L7) delante de la aplicación? Esto incluye load balancers cloud + (como AWS ALB o GCP HTTP LB) y proxies open-source (como Nginx, Envoy o un Istio ingress gateway). +2. Si es así, ¿qué header HTTP usan tus proxies para pasar la IP del cliente a la + aplicación? + +## Caso 1: Sin proxy + +Si no hay proxy (la app mira directamente a internet), la única dirección en la que puedes +confiar es la de la capa de red. Todos los headers HTTP son no confiables porque los clientes +tienen control total sobre ellos. + +Usa `echo.ExtractIPDirect()`: + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## Caso 2: Proxies que usan el header X-Forwarded-For + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +es el header más común para retransmitir IPs de cliente. En cada salto, el proxy agrega la +IP del request al final del 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 +``` + +En este caso, toma la **primera lectura de IP no confiable desde la derecha**. Nunca tomes +la primera desde la izquierda, ya que el cliente la controla. Aquí "confiable" significa que +estás seguro de que la IP pertenece a tu infraestructura. En el ejemplo anterior, si `b` y +`c` son confiables, la IP del cliente es `a` en ambos casos, nunca `x`. + +Usa `ExtractIPFromXFFHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +Por defecto confía en direcciones IP internas: loopback, link-local unicast, +private-use y direcciones locales únicas de +[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890), +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291) y +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193). Contrólalo con `TrustOption`s: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader( + echo.TrustLoopback(false), // e.g. IPv4 starting with 127. + echo.TrustLinkLocal(false), // e.g. IPv4 starting with 169.254. + echo.TrustPrivateNet(false), // e.g. IPv4 starting with 10. or 192.168. + echo.TrustIPRange(lbIPRange), +) +``` + +## Caso 3: Proxies que usan el header X-Real-IP + +`X-Real-IP` es otro header para retransmitir la IP del cliente, pero a diferencia de XFF +contiene solo una dirección. + +Si tus proxies establecen este header, usa `ExtractIPFromRealIPHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +Al igual que con XFF, confía en direcciones IP internas por defecto y acepta los mismos +`TrustOption`s. + +:::danger +**Nunca olvides** configurar el proxy más externo (en el edge de tu infraestructura) para que +**no deje pasar headers entrantes**. De lo contrario, un cliente puede falsificarlos y abrir +la puerta al fraude. +::: + +## Comportamiento por defecto + +Por defecto, Echo considera al mismo tiempo el primer header XFF, el header X-Real-IP y la IP +de la capa de red. + +Como este artículo debería dejar claro, esa no es una buena elección. Sigue siendo el valor +por defecto solo por compatibilidad hacia atrás. diff --git a/site/src/content/docs/es/guide/quickstart.md b/site/src/content/docs/es/guide/quickstart.md new file mode 100644 index 00000000..a5c39793 --- /dev/null +++ b/site/src/content/docs/es/guide/quickstart.md @@ -0,0 +1,76 @@ +--- +title: Inicio rápido +description: Crea una API Echo lista para producción en menos de cinco minutos. +sidebar: + order: 1 +--- + +Echo es un framework web Go minimalista y de alto rendimiento. Esta guía pone un servidor +en marcha en menos de cinco minutos. + +## Requisitos + +Echo requiere **Go 1.25 o posterior**. Comprueba tu versión: + +```bash +go version +``` + +## Instalar + +Crea un módulo y agrega Echo: + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +Crea `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) + } +} +``` + +Ejecútalo: + +```bash +go run main.go +``` + +Tu servidor está disponible en `http://localhost:1323`. El router de Echo despacha requests +con **cero asignaciones dinámicas de memoria** por ruta. + +:::tip[Ask Echo] +¿Atascado? Presiona el botón **Ask Echo** (abajo a la derecha) y pregunta +*"How do I add JWT auth?"*: las respuestas salen directamente de esta documentación. +::: + +## Próximos pasos + +- [Routing](/es/guide/routing/): rutas estáticas, parametrizadas y wildcard. +- [Context](/es/guide/context/): el objeto request/response por request. +- [Binding](/es/guide/binding/): analiza datos de request en structs tipados. diff --git a/site/src/content/docs/es/guide/request.md b/site/src/content/docs/es/guide/request.md new file mode 100644 index 00000000..2e17ed2d --- /dev/null +++ b/site/src/content/docs/es/guide/request.md @@ -0,0 +1,170 @@ +--- +title: Request +description: Obtén datos de form, query y path desde un request, y valídalos. +sidebar: + order: 7 +--- + +Un handler lee datos del request a través de `echo.Context`. Echo puede obtener valores +individualmente por nombre, vincularlos a structs (consulta [Binding](/es/guide/binding/)) y +delegar la validación a un validator que registres. + +## Obtener datos + +### Datos de formulario + +Obtén un campo de formulario por nombre con `Context#FormValue(name string)`: + +```go +e.POST("/form", func(c *echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +Para tipos distintos de `string`, usa la función genérica `echo.FormValue[T]`: + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +Prueba con: + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +Para vincular un tipo de datos personalizado, implementa la interfaz `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 +} +``` + +### Parámetros de query + +Obtén un parámetro de query por nombre con `Context#QueryParam(name string)`: + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +Para tipos distintos de `string`, usa la función genérica `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" +``` + +### Parámetros de path + +Obtén un parámetro de path registrado por nombre con `Context#Param(name string)`: + +```go +e.GET("/users/:name", func(c *echo.Context) error { + name := c.Param("name") + return c.String(http.StatusOK, name) +}) +``` + +Para tipos distintos de `string`, usa la función genérica `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 +``` + +### Binding de datos + +Echo también puede vincular datos de request a structs y variables nativas de Go. Consulta +[Binding](/es/guide/binding/). + +## Validar datos + +Echo no incluye validación de datos integrada. Puedes registrar un validator personalizado mediante +`Echo#Validator` y usar una biblioteca de terceros como +[go-playground/validator](https://github.com/go-playground/validator). + +El ejemplo siguiente valida un struct vinculado: + +```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/es/guide/response.md b/site/src/content/docs/es/guide/response.md new file mode 100644 index 00000000..17938d4e --- /dev/null +++ b/site/src/content/docs/es/guide/response.md @@ -0,0 +1,318 @@ +--- +title: Response +description: Envía strings, HTML, JSON, XML, archivos, streams, redirects y hooks de response. +sidebar: + order: 8 +--- + +Un handler escribe su response a través de `echo.Context`. Cada helper establece por ti el +`Content-Type` adecuado y el código de estado. + +## Enviar string + +`Context#String(code int, s string)` envía una response de texto plano con un código de estado. + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## Enviar HTML + +`Context#HTML(code int, html string)` envía una response HTML simple con un código de estado. +Para generar HTML dinámicamente, consulta [Templates](/es/guide/templates/). + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### Enviar blob HTML + +`Context#HTMLBlob(code int, b []byte)` envía un blob HTML con un código de estado. Es útil +con un template engine que produce `[]byte`. + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## Renderizar template + +Consulta [Templates](/es/guide/templates/). + +## Enviar JSON + +`Context#JSON(code int, i any)` codifica un valor Go como JSON y lo envía con un código de estado. + +```go +type User struct { + Name string `json:"name" xml:"name"` + Email string `json:"email" xml:"email"` +} + +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSON(http.StatusOK, u) +} +``` + +### Stream JSON + +`Context#JSON()` usa `json.Marshal` internamente, lo que puede ser ineficiente para payloads +grandes. En ese caso, transmite el JSON directamente: + +```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)` envía una response JSON con formato bonito. +La indentación puede ser espacios o tabs. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSONPretty(http.StatusOK, u, " ") +} +``` + +```json +{ + "email": "jon@labstack.com", + "name": "Jon" +} +``` + +### Blob JSON + +`Context#JSONBlob(code int, b []byte)` envía directamente un blob JSON precodificado, por +ejemplo desde una base de datos. + +```go +func(c *echo.Context) error { + encodedJSON := []byte{} // Encoded JSON from an external source. + return c.JSONBlob(http.StatusOK, encodedJSON) +} +``` + +## Enviar JSONP + +`Context#JSONP(code int, callback string, i any)` codifica un valor Go como JSON y lo envía +como payload JSONP envuelto en el callback dado. + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +Consulta el [recetario de JSONP](/es/cookbook/jsonp/). + +## Enviar XML + +`Context#XML(code int, i any)` codifica un valor Go como XML y lo envía con un código de estado. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XML(http.StatusOK, u) +} +``` + +### Stream XML + +`Context#XML` usa `xml.Marshal` internamente, lo que puede ser ineficiente para payloads +grandes. En ese caso, transmite el XML directamente: + +```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)` envía una response XML con formato bonito. +La indentación puede ser espacios o tabs. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XMLPretty(http.StatusOK, u, " ") +} +``` + +```xml + + + Jon + jon@labstack.com + +``` + +:::tip +También puedes hacer que `Context#XML()` produzca XML con formato bonito agregando `pretty` +al query string de la URL del request. + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### Blob XML + +`Context#XMLBlob(code int, b []byte)` envía directamente un blob XML precodificado, por +ejemplo desde una base de datos. + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## Enviar archivo + +`Context#File(file string)` envía el contenido de un archivo como response. Establece +el content type correcto y maneja el caching automáticamente. + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## Enviar attachment + +`Context#Attachment(file, name string)` es como `File()`, pero envía el archivo con +`Content-Disposition: attachment` y el nombre dado. + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## Enviar inline + +`Context#Inline(file, name string)` es como `File()`, pero envía el archivo con +`Content-Disposition: inline` y el nombre dado. + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## Enviar blob + +`Context#Blob(code int, contentType string, b []byte)` envía datos arbitrarios con un +content type y código de estado dados. + +```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) +} +``` + +## Enviar stream + +`Context#Stream(code int, contentType string, r io.Reader)` envía un stream de datos +arbitrario con un content type, `io.Reader` y código de estado dados. + +```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) +} +``` + +## Enviar sin contenido + +`Context#NoContent(code int)` envía un body vacío con un código de estado. + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## Redirigir request + +`Context#Redirect(code int, url string)` redirige el request a la URL dada con un código de estado. + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## Hooks + +### Antes de la response + +`Response#Before(func())` registra una función que se ejecuta justo antes de escribir la response. + +### Después de la response + +`Response#After(func())` registra una función que se ejecuta justo después de escribir la response. +Si `Content-Length` es desconocido, no se ejecuta ninguna función 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 +Puedes registrar varias funciones `Before` y `After`. +::: diff --git a/site/src/content/docs/es/guide/routing.md b/site/src/content/docs/es/guide/routing.md new file mode 100644 index 00000000..bf08c5e7 --- /dev/null +++ b/site/src/content/docs/es/guide/routing.md @@ -0,0 +1,75 @@ +--- +title: Routing +description: Hace coincidir URLs de request con handlers en el árbol radix sin asignaciones de Echo. +sidebar: + order: 3 +--- + +El router optimizado de Echo hace coincidir URLs de request con handlers usando un árbol radix con +**cero asignaciones dinámicas de memoria** y priorización inteligente de rutas. + +## Registrar rutas + +Usa los helpers de métodos HTTP en la instancia `Echo`. Cada uno recibe un patrón de path y una +`HandlerFunc` (`func(c *echo.Context) error`), con middleware opcional a nivel de ruta. + +```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` registra un handler para todos los métodos soportados, y `Match` para un conjunto específico: + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## Tipos de coincidencia + +| Patrón | Tipo | Ejemplo de coincidencia | +| ------------------ | -------- | -------------------------- | +| `/users/profile` | Static | `/users/profile` | +| `/users/:id` | Param | `/users/42` | +| `/static/*` | Wildcard | `/static/css/app.css` | + +:::note +La prioridad es **static → param → wildcard**, por lo que `/users/profile` siempre gana sobre +`/users/:id`, que a su vez gana sobre `/users/*`. +::: + +## Parámetros de path + +Lee parámetros nombrados desde el contexto con `c.Param()` (o `c.ParamOr()` para un valor por defecto): + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +El segmento wildcard está disponible como el parámetro `*`: + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## Grupos + +Agrupa rutas que comparten un prefijo y middleware con `e.Group()`: + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +Los grupos pueden anidarse para componer árboles de rutas más grandes. diff --git a/site/src/content/docs/es/guide/static-files.md b/site/src/content/docs/es/guide/static-files.md new file mode 100644 index 00000000..bc326d7c --- /dev/null +++ b/site/src/content/docs/es/guide/static-files.md @@ -0,0 +1,89 @@ +--- +title: Servir archivos estáticos +description: Sirve imágenes, JavaScript, CSS, fuentes y otros assets con Echo. +sidebar: + order: 9 +--- + +Echo puede servir assets estáticos como imágenes, JavaScript, CSS, PDFs y fuentes desde +el filesystem o desde un filesystem embebido. + +## Filesystem por defecto + +Echo usa `os.DirFS(".")` como filesystem por defecto, con raíz en el directorio de trabajo +actual. Para cambiarlo, establece el campo `Echo#Filesystem`: + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## Usar el middleware Static + +Consulta [middleware Static](/es/middleware/static/). + +## Usar Echo#Static() + +`Echo#Static(prefix, root string)` registra una ruta que sirve archivos estáticos bajo +un prefijo de path desde el directorio raíz dado. + +Sirve cualquier archivo de `assets` bajo `/static/*`. Un request a `/static/js/main.js` +sirve `assets/js/main.js`: + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +Sirve cualquier archivo de `assets` bajo `/*`. Un request a `/js/main.js` sirve +`assets/js/main.js`: + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## Usar Echo#StaticFS() + +Los archivos estáticos se pueden servir desde cualquier `fs.FS`, incluido un `embed.FS`. Usa +`echo.MustSubFS` para que los archivos servidos tengan raíz en el subdirectorio correcto: un +`embed.FS` incluye sus subdirectorios como entradas propias. + +```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) + } +} +``` + +## Usar Echo#File() + +`Echo#File(path, file string)` registra una ruta que sirve un único archivo estático. + +Sirve una página index desde `public/index.html`: + +```go +e.File("/", "public/index.html") +``` + +Sirve un favicon desde `app/assets/favicon.ico`: + +```go +e := echo.New() +e.Filesystem = os.DirFS("/") +e.File("/favicon.ico", "app/assets/favicon.ico") // The file path must not have a leading slash. +``` + +:::caution +Un `/` inicial en el path del archivo no funciona con la mayoría de implementaciones de `fs.FS`. +Usa un path relativo. +::: diff --git a/site/src/content/docs/es/guide/templates.md b/site/src/content/docs/es/guide/templates.md new file mode 100644 index 00000000..e5cb290d --- /dev/null +++ b/site/src/content/docs/es/guide/templates.md @@ -0,0 +1,132 @@ +--- +title: Templates +description: Renderiza templates HTML con cualquier engine registrando un renderer. +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` renderiza un template con datos +y envía una response `text/html` con un código de estado. Registra un renderer estableciendo +`Echo#Renderer`, lo que te permite usar cualquier template engine. + +## Renderizado + +El ejemplo siguiente usa `html/template` de Go. + +Usa el renderer de templates por defecto: + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +O implementa tú mismo la interfaz `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. Precompila los templates. + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. Registra el renderer. + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. Renderiza un template dentro del handler. + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## Avanzado: llamar a Echo desde templates + +A veces es útil generar URIs desde un template llamando a `Echo#Reverse`. `html/template` +de Go no es ideal para esto, pero se puede hacer de dos formas: proporcionando un método común +en cada objeto que se pasa a los templates, o pasando un `map[string]any` y ampliándolo en el +renderer personalizado. La segunda opción es más flexible. Aquí tienes un ejemplo completo. + +`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/es/guide/testing.md b/site/src/content/docs/es/guide/testing.md new file mode 100644 index 00000000..b71db7e1 --- /dev/null +++ b/site/src/content/docs/es/guide/testing.md @@ -0,0 +1,264 @@ +--- +title: Testing +description: Prueba handlers y middleware con httptest y los helpers de echotest. +sidebar: + order: 13 +--- + +Los handlers y middleware de Echo son funciones simples sobre un `echo.Context`, por lo que son +fáciles de probar con el paquete estándar `net/http/httptest`. El paquete `echotest` ofrece +helpers que reducen el código repetitivo. + +## Probar un handler + +Considera dos handlers: + +**CreateUser**: `POST /users` + +- Acepta un payload JSON. +- Devuelve `201 Created` si tiene éxito. +- Devuelve `500 Internal Server Error` si hay error. + +**GetUser**: `GET /users/:email` + +- Devuelve `200 OK` si tiene éxito. +- Devuelve `404 Not Found` si el usuario no existe; de lo contrario, `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()) + } +} +``` + +### Usar los helpers de echotest + +`echotest.ContextConfig` crea un contexto (y recorder) a partir de una descripción +declarativa del request: + +```go +// Same test as above, using echotest. +func TestCreateUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} + +// Even shorter, using ServeWithHandler. +func TestCreateUserWithServeHandler(t *testing.T) { + h := &handler{mockDB} + + rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ServeWithHandler(t, h.createUser) + + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) +} + +func TestGetUser(t *testing.T) { + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }, + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.getUser(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### Usar un payload de formulario + +```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) +``` + +Un payload de formulario multipart con `echotest`: + +```go +func TestContext_MultipartForm(t *testing.T) { + testConf := echotest.ContextConfig{ + MultipartForm: &echotest.MultipartForm{ + Fields: map[string]string{ + "key": "value", + }, + Files: []echotest.MultipartFormFile{ + { + Fieldname: "file", + Filename: "test.json", + Content: echotest.LoadBytes(t, "testdata/test.json"), + }, + }, + }, + } + c := testConf.ToContext(t) + + assert.Equal(t, "value", c.FormValue("key")) + assert.Equal(t, http.MethodPost, c.Request().Method) + assert.Equal(t, true, strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "multipart/form-data; boundary=")) + + fv, err := c.FormFile("file") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "test.json", fv.Filename) +} +``` + +### Establecer parámetros de path + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### Establecer parámetros de query + +```go +// import "net/url" +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) +``` + +## Probar middleware + +```go +func TestMiddleware(t *testing.T) { + handler := func(c *echo.Context) error { + return c.JSON(http.StatusTeapot, fmt.Sprintf("email: %s", c.Param("email"))) + } + middleware := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("user_id", int64(1234)) + return next(c) + } + } + + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{{Name: "email", Value: "jon@labstack.com"}}, + }.ToContextRecorder(t) + + if err := middleware(handler)(c); err != nil { + t.Fatal(err) + } + + // Check that the middleware set the value. + userID, err := echo.ContextGet[int64](c, "user_id") + assert.NoError(t, err) + assert.Equal(t, int64(1234), userID) + + // Check that the handler returned the correct response. + assert.Equal(t, http.StatusTeapot, rec.Code) +} +``` + +:::tip +Para más ejemplos, consulta los [casos de prueba de middleware](https://github.com/labstack/echo/tree/master/middleware) +en el código fuente de Echo. +::: From 1c52f4b814e35a7e878e0ef2e8938ab9a156191e Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Tue, 16 Jun 2026 06:35:58 -0700 Subject: [PATCH 3/4] docs(i18n): middleware (es) --- .../content/docs/es/middleware/basic-auth.md | 74 +++++ .../content/docs/es/middleware/body-dump.md | 72 +++++ .../content/docs/es/middleware/body-limit.md | 47 +++ .../content/docs/es/middleware/casbin-auth.md | 203 +++++++++++++ .../docs/es/middleware/context-timeout.md | 47 +++ site/src/content/docs/es/middleware/cors.md | 118 ++++++++ site/src/content/docs/es/middleware/csrf.md | 177 +++++++++++ .../content/docs/es/middleware/decompress.md | 59 ++++ site/src/content/docs/es/middleware/gzip.md | 69 +++++ site/src/content/docs/es/middleware/jwt.md | 126 ++++++++ .../content/docs/es/middleware/key-auth.md | 91 ++++++ site/src/content/docs/es/middleware/logger.md | 236 +++++++++++++++ .../docs/es/middleware/method-override.md | 52 ++++ .../docs/es/middleware/open-telemetry.md | 82 +++++ .../content/docs/es/middleware/prometheus.md | 284 ++++++++++++++++++ site/src/content/docs/es/middleware/proxy.md | 134 +++++++++ .../docs/es/middleware/rate-limiter.md | 109 +++++++ .../src/content/docs/es/middleware/recover.md | 61 ++++ .../content/docs/es/middleware/redirect.md | 101 +++++++ .../content/docs/es/middleware/request-id.md | 89 ++++++ .../src/content/docs/es/middleware/rewrite.md | 87 ++++++ site/src/content/docs/es/middleware/secure.md | 113 +++++++ .../src/content/docs/es/middleware/session.md | 189 ++++++++++++ site/src/content/docs/es/middleware/static.md | 142 +++++++++ .../docs/es/middleware/trailing-slash.md | 65 ++++ 25 files changed, 2827 insertions(+) create mode 100644 site/src/content/docs/es/middleware/basic-auth.md create mode 100644 site/src/content/docs/es/middleware/body-dump.md create mode 100644 site/src/content/docs/es/middleware/body-limit.md create mode 100644 site/src/content/docs/es/middleware/casbin-auth.md create mode 100644 site/src/content/docs/es/middleware/context-timeout.md create mode 100644 site/src/content/docs/es/middleware/cors.md create mode 100644 site/src/content/docs/es/middleware/csrf.md create mode 100644 site/src/content/docs/es/middleware/decompress.md create mode 100644 site/src/content/docs/es/middleware/gzip.md create mode 100644 site/src/content/docs/es/middleware/jwt.md create mode 100644 site/src/content/docs/es/middleware/key-auth.md create mode 100644 site/src/content/docs/es/middleware/logger.md create mode 100644 site/src/content/docs/es/middleware/method-override.md create mode 100644 site/src/content/docs/es/middleware/open-telemetry.md create mode 100644 site/src/content/docs/es/middleware/prometheus.md create mode 100644 site/src/content/docs/es/middleware/proxy.md create mode 100644 site/src/content/docs/es/middleware/rate-limiter.md create mode 100644 site/src/content/docs/es/middleware/recover.md create mode 100644 site/src/content/docs/es/middleware/redirect.md create mode 100644 site/src/content/docs/es/middleware/request-id.md create mode 100644 site/src/content/docs/es/middleware/rewrite.md create mode 100644 site/src/content/docs/es/middleware/secure.md create mode 100644 site/src/content/docs/es/middleware/session.md create mode 100644 site/src/content/docs/es/middleware/static.md create mode 100644 site/src/content/docs/es/middleware/trailing-slash.md diff --git a/site/src/content/docs/es/middleware/basic-auth.md b/site/src/content/docs/es/middleware/basic-auth.md new file mode 100644 index 00000000..49dc36dc --- /dev/null +++ b/site/src/content/docs/es/middleware/basic-auth.md @@ -0,0 +1,74 @@ +--- +title: Basic Auth +description: Middleware de autenticación HTTP Basic que valida credenciales de username y password. +sidebar: + order: 1 +--- + +El middleware Basic Auth proporciona autenticación HTTP basic. + +- Para credenciales válidas llama al siguiente handler. +- Para credenciales ausentes o inválidas, envía una response `401 Unauthorized`. + +## Uso + +```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 +})) +``` + +## Configuración personalizada + +```go +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) +``` + +## Configuración + +```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` tiene esta firma: + +```go +type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error) +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: "Restricted", +} +``` + +:::caution[Seguridad] +Compara siempre las credenciales con `subtle.ConstantTimeCompare` para prevenir timing attacks. +::: diff --git a/site/src/content/docs/es/middleware/body-dump.md b/site/src/content/docs/es/middleware/body-dump.md new file mode 100644 index 00000000..d500f95a --- /dev/null +++ b/site/src/content/docs/es/middleware/body-dump.md @@ -0,0 +1,72 @@ +--- +title: Body Dump +description: Captura payloads de request y response y pásalos a un handler para logging o debugging. +sidebar: + order: 2 +--- + +El middleware Body Dump captura los payloads de request y response y los pasa a un +handler registrado. Generalmente se usa para debugging o logging. + +:::caution +Evita Body Dump para payloads grandes, como uploads o downloads de archivos. Si debes usarlo +en esas rutas, agrega una excepción en la función skipper. +::: + +## Uso + +```go +e := echo.New() +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) { + // Handle the request and response bodies. +})) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) +``` + +## Configuración + +```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` tiene esta firma: + +```go +type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error) +``` + +### Configuración por defecto + +```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/es/middleware/body-limit.md b/site/src/content/docs/es/middleware/body-limit.md new file mode 100644 index 00000000..128246e9 --- /dev/null +++ b/site/src/content/docs/es/middleware/body-limit.md @@ -0,0 +1,47 @@ +--- +title: Body Limit +description: Rechaza requests cuyo body supera un tamaño máximo configurado. +sidebar: + order: 3 +--- + +El middleware Body Limit establece el tamaño máximo permitido para un body de request. Si el tamaño +supera el límite configurado, envía una response `413 Request Entity Too Large`. + +El límite se aplica tanto al header de request `Content-Length` como al contenido real leído, +lo que lo hace resistente a headers falsificados. El límite se especifica en bytes. + +## Uso + +```go +e := echo.New() +e.Use(middleware.BodyLimit(2_097_152)) // 2 MB +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset (Limit is required). +BodyLimitConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/es/middleware/casbin-auth.md b/site/src/content/docs/es/middleware/casbin-auth.md new file mode 100644 index 00000000..d90c98f2 --- /dev/null +++ b/site/src/content/docs/es/middleware/casbin-auth.md @@ -0,0 +1,203 @@ +--- +title: Casbin Auth +description: Autoriza requests con la biblioteca de control de acceso Casbin usando un middleware personalizado pequeño. +sidebar: + order: 4 +--- + +[Casbin](https://github.com/casbin/casbin) es una biblioteca de control de acceso open-source +potente y eficiente para Go. Soporta aplicar autorización en muchos modelos: + +- ACL (Access Control List) +- ACL con superuser +- ACL sin usuarios, útil para sistemas sin autenticación o log-ins de usuario +- ACL sin recursos: apunta a un tipo de recurso (por ejemplo `write-article`, `read-log`) en lugar de a uno individual +- RBAC (Role-Based Access Control) +- RBAC con roles de recursos: tanto usuarios como recursos pueden tener roles +- RBAC con dominios/tenants: los usuarios pueden tener conjuntos de roles distintos por dominio/tenant +- ABAC (Attribute-Based Access Control) +- RESTful +- Deny-override: se soportan reglas allow y deny; deny sobrescribe allow + +Consulta el [resumen de API](https://casbin.org/docs/api-overview) y la +[documentación de Casbin](https://casbin.org/docs/) para más detalles. + +## Dependencias + +```bash +go get github.com/casbin/casbin/v3 +``` + +```go +import ( + "github.com/casbin/casbin/v3" +) +``` + +## Implementación + +Echo no incluye un middleware Casbin; la integración es un wrapper pequeño alrededor del +enforcer de Casbin: + +```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) + } + } +} +``` + +## Ejemplo + +Crea un archivo de modelo 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 == "*") +``` + +Crea un archivo de policy 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 +``` + +La autenticación y la autorización son responsabilidades separadas. Autentica al usuario con +otro middleware (como JWT o Basic Auth), y luego proporciona un `userGetter` para que Casbin +pueda autorizar el request. + +### Con 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 +``` + +Pruébalo con: + +```bash +curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" +``` + +### Con 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 +``` + +Pruébalo con: + +```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 +``` + +### Ejemplo completo de 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/es/middleware/context-timeout.md b/site/src/content/docs/es/middleware/context-timeout.md new file mode 100644 index 00000000..e75ff5e1 --- /dev/null +++ b/site/src/content/docs/es/middleware/context-timeout.md @@ -0,0 +1,47 @@ +--- +title: Context Timeout +description: Aplica un timeout al contexto del request para que las operaciones conscientes del contexto puedan retornar antes. +sidebar: + order: 5 +--- + +El middleware Context Timeout aplica un timeout al contexto del request dentro de un periodo +predefinido, para que los métodos conscientes del contexto puedan retornar antes cuando se supera el deadline. + +## Uso + +```go +e.Use(middleware.ContextTimeout(60 * time.Second)) +``` + +## Configuración personalizada + +```go +e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ + Timeout: 60 * time.Second, +})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset (Timeout is required). +ContextTimeoutConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/es/middleware/cors.md b/site/src/content/docs/es/middleware/cors.md new file mode 100644 index 00000000..ce6c57b6 --- /dev/null +++ b/site/src/content/docs/es/middleware/cors.md @@ -0,0 +1,118 @@ +--- +title: CORS +description: Middleware Cross-Origin Resource Sharing para control de acceso seguro entre dominios. +sidebar: + order: 6 +--- + +El middleware CORS implementa la especificación [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol). +CORS da a los servidores web controles de acceso entre dominios, lo que permite transferencias de datos seguras entre dominios. + +## Uso + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## Configuración personalizada + +```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}, +})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```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[Seguridad] +Nunca combines `AllowCredentials = true` con un wildcard `AllowOrigins`. Cuando necesites +validación dinámica de origin, usa `UnsafeAllowOriginFunc` y valida con cuidado: +los atacantes pueden registrar nombres de (sub)dominio hostiles. +::: diff --git a/site/src/content/docs/es/middleware/csrf.md b/site/src/content/docs/es/middleware/csrf.md new file mode 100644 index 00000000..411e4863 --- /dev/null +++ b/site/src/content/docs/es/middleware/csrf.md @@ -0,0 +1,177 @@ +--- +title: CSRF +description: Protección contra Cross-Site Request Forgery usando metadatos Sec-Fetch-Site y validación de tokens. +sidebar: + order: 7 +--- + +Cross-Site Request Forgery (CSRF, a veces pronunciado "sea-surf", o XSRF) es un tipo de +exploit malicioso en el que se transmiten comandos no autorizados desde un usuario en el que +un sitio web confía. + +## Uso + +```go +e.Use(middleware.CSRF()) +``` + +## Cómo funciona + +El middleware CSRF soporta el header +[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) +como un enfoque moderno de defensa en profundidad para la +[protección CSRF](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers), +implementando la Fetch Metadata API recomendada por OWASP junto al mecanismo tradicional +basado en tokens. + +Los navegadores modernos envían automáticamente el header `Sec-Fetch-Site` con cada request, +indicando la relación entre el origin del request y el destino. El middleware usa esto para +tomar una decisión de seguridad: + +- **`same-origin`** o **`none`**: permitido (coincidencia exacta de origin o navegación directa del usuario) +- **`same-site`**: vuelve a la validación por token (por ejemplo, de subdominio a dominio principal) +- **`cross-site`**: bloqueado por defecto con un error `403` para métodos inseguros (POST, PUT, DELETE, PATCH) + +Para navegadores que no envían este header (navegadores más antiguos), el middleware vuelve +sin interrupciones a la protección CSRF tradicional basada en tokens. + +Dos opciones ajustan el comportamiento de `Sec-Fetch-Site`: + +- `TrustedOrigins []string`: allowlist de origins específicos para requests cross-site (útil para callbacks OAuth, webhooks) +- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)`: lógica personalizada para validación 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 + }, +})) +``` + +## Protección basada en tokens + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +El ejemplo anterior extrae el token CSRF del header de request `X-XSRF-TOKEN`. + +Leer el token desde una cookie en su lugar: + +```go +middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "cookie:_csrf", + CookiePath: "/", + CookieDomain: "example.com", + CookieSecure: true, + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, +}) +``` + +## Acceder al token CSRF + +- **Server-side**: el token está disponible desde el contexto bajo `ContextKey` y se puede pasar al cliente mediante un template. +- **Client-side**: el token se puede leer desde la cookie CSRF. + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, +} +``` + +## Ejemplo completo + +Hay un ejemplo completo y ejecutable disponible en el +[recetario de echox](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go). diff --git a/site/src/content/docs/es/middleware/decompress.md b/site/src/content/docs/es/middleware/decompress.md new file mode 100644 index 00000000..f2ad7211 --- /dev/null +++ b/site/src/content/docs/es/middleware/decompress.md @@ -0,0 +1,59 @@ +--- +title: Decompress +description: Descomprime de forma transparente bodies de request codificados con gzip. +sidebar: + order: 8 +--- + +El middleware Decompress descomprime el body del request HTTP cuando el header +`Content-Encoding` está establecido en `gzip`. + +:::note +El body se descomprime en memoria y permanece allí durante la vida del request (y hasta +la garbage collection). +::: + +## Uso + +```go +e.Use(middleware.Decompress()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ + Skipper: middleware.DefaultSkipper, +})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +DecompressConfig{ + Skipper: DefaultSkipper, + GzipDecompressPool: &DefaultGzipDecompressPool{}, + MaxDecompressedSize: 100 * MB, +} +``` diff --git a/site/src/content/docs/es/middleware/gzip.md b/site/src/content/docs/es/middleware/gzip.md new file mode 100644 index 00000000..9ec16acf --- /dev/null +++ b/site/src/content/docs/es/middleware/gzip.md @@ -0,0 +1,69 @@ +--- +title: Gzip +description: Comprime responses HTTP con el esquema de compresión gzip. +sidebar: + order: 9 +--- + +El middleware Gzip comprime la response HTTP usando el esquema de compresión gzip. + +## Uso + +```go +e.Use(middleware.Gzip()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +:::tip +Pasa un skipper para deshabilitar gzip en ciertas URLs. +::: + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Skipper: func(c *echo.Context) bool { + return strings.Contains(c.Path(), "metrics") // change "metrics" to your own path + }, +})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + MinLength: 0, +} +``` diff --git a/site/src/content/docs/es/middleware/jwt.md b/site/src/content/docs/es/middleware/jwt.md new file mode 100644 index 00000000..97cbbd8f --- /dev/null +++ b/site/src/content/docs/es/middleware/jwt.md @@ -0,0 +1,126 @@ +--- +title: JWT +description: Middleware de autenticación JSON Web Token provisto por el módulo echo-jwt. +sidebar: + order: 10 +--- + +El middleware JWT proporciona autenticación JSON Web Token (JWT). Vive en un módulo separado: +[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt). + +Comportamiento: + +- Para un token válido, establece el usuario en el contexto y llama al siguiente handler. +- Para un token inválido, envía una response `401 Unauthorized`. +- Para un header `Authorization` ausente o inválido, envía una response `400 Bad Request`. + +## Dependencias + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## Uso + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## Configuración personalizada + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## Configuración + +```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 +} +``` + +## Ejemplo + +Consulta el [recetario de JWT](/es/cookbook/jwt/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/key-auth.md b/site/src/content/docs/es/middleware/key-auth.md new file mode 100644 index 00000000..39d0beb2 --- /dev/null +++ b/site/src/content/docs/es/middleware/key-auth.md @@ -0,0 +1,91 @@ +--- +title: Key Auth +description: Middleware de autenticación basada en clave que valida una API key desde header, query, form o cookie. +sidebar: + order: 11 +--- + +El middleware Key Auth proporciona autenticación basada en clave. + +- Para una clave válida llama al siguiente handler. +- Para una clave inválida, envía una response `401 Unauthorized`. +- Para una clave ausente, envía una response `400 Bad Request`. + +## Uso + +```go +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil +})) +``` + +## Configuración personalizada + +```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 + }, +})) +``` + +## Configuración + +```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` tiene esta firma: + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### Configuración por defecto + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/es/middleware/logger.md b/site/src/content/docs/es/middleware/logger.md new file mode 100644 index 00000000..4858cb7f --- /dev/null +++ b/site/src/content/docs/es/middleware/logger.md @@ -0,0 +1,236 @@ +--- +title: Request Logger +description: Logging de requests totalmente personalizable que se integra con bibliotecas de logging estructurado. +sidebar: + order: 12 +--- + +El middleware `RequestLogger` registra información sobre cada request HTTP. Te permite +personalizar por completo qué se registra y cómo, por lo que encaja bien con bibliotecas +de terceros de logging estructurado. + +Los valores que el logger puede extraer se controlan con los campos booleanos y slices de +`RequestLoggerConfig`. Habilita un campo (por ejemplo `LogStatus: true`) para que su valor +se rellene en `RequestLoggerValues`, que se pasa a tu `LogValuesFunc`. + +```go +type RequestLoggerConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // BeforeNextFunc is called before the next middleware or handler in the chain. + BeforeNextFunc func(c *echo.Context) + + // LogValuesFunc is called with the values extracted by the logger from the + // request/response. + // Mandatory. + LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error + + // HandleError instructs the logger to call the global error handler when the next + // middleware/handler returns an error. A side effect is that the response is then + // committed and sent, so middlewares up the chain can no longer change the status + // code or body. + HandleError bool + + // LogLatency records the duration of the rest of the handler chain (the next(c) call). + LogLatency bool + // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). + LogProtocol bool + // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. + LogRemoteIP bool + // LogHost extracts the request host value (for example example.com). + LogHost bool + // LogMethod extracts the request method (for example GET). + LogMethod bool + // LogURI extracts the request URI (for example /list?lang=en&page=1). + LogURI bool + // LogURIPath extracts the request URI path part (for example /list). + LogURIPath bool + // LogRoutePath extracts the route path the request matched (for example /user/:id). + LogRoutePath bool + // LogRequestID extracts the request ID from the X-Request-ID request header, or the + // response if the request did not have a value. + LogRequestID bool + // LogReferer extracts the request referer value. + LogReferer bool + // LogUserAgent extracts the request user agent value. + LogUserAgent bool + // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, + // the status code is taken from it. + LogStatus bool + // LogError extracts the error returned from the handler chain. + LogError bool + // LogContentLength extracts the Content-Length header value. Note: this can differ + // from the actual request body size as it may be spoofed. + LogContentLength bool + // LogResponseSize extracts the response content length. Note: when used with Gzip + // middleware this value may not always be correct. + LogResponseSize bool + // LogHeaders extracts the given list of request headers. A slice of values is logged + // per header since a request can contain more than one. Names are canonicalized with + // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). + LogHeaders []string + // LogQueryParams extracts the given list of query parameters from the request URI. A + // slice of values is logged per name since a request can repeat a parameter. + LogQueryParams []string + // LogFormValues extracts the given list of form values from the request body and URI. + // A slice of values is logged per name since a request can repeat a value. + LogFormValues []string +} +``` + +## Ejemplos + +### 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 + }, +})) +``` + +Salida de ejemplo: + +```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 + }, +})) +``` + +Salida de ejemplo: + +```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 + }, +})) +``` + +Salida de ejemplo: + +```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 + }, +})) +``` + +Salida de ejemplo: + +```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 + }, +})) +``` + +Salida de ejemplo: + +```text +time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 +``` + +## Solución de problemas + +### panic: missing LogValuesFunc callback function for request logger middleware + +Este panic ocurre cuando el callback obligatorio `LogValuesFunc` se deja sin configurar. +Define una función que coincida con la firma de `LogValuesFunc` y asígnala en la configuración: + +```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, +})) +``` + +### Los parámetros en los logs están vacíos + +Si valores como `v.URI` y `v.Status` están vacíos dentro de `LogValuesFunc`, comprueba que los +flags de extracción correspondientes (`LogStatus`, `LogURI`, etc.) estén establecidos en `true` +en la configuración. Cada valor solo se rellena cuando su flag está habilitado. diff --git a/site/src/content/docs/es/middleware/method-override.md b/site/src/content/docs/es/middleware/method-override.md new file mode 100644 index 00000000..22fbaa4a --- /dev/null +++ b/site/src/content/docs/es/middleware/method-override.md @@ -0,0 +1,52 @@ +--- +title: Method Override +description: Sobrescribe el método HTTP de un request POST mediante header, form o valor de query. +sidebar: + order: 13 +--- + +El middleware Method Override lee el método sobrescrito desde el request y lo usa +en lugar del método original. + +:::note +Por razones de seguridad, solo se puede sobrescribir el método `POST`. +::: + +## Uso + +```go +e.Pre(middleware.MethodOverride()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +El método puede obtenerse con `MethodFromHeader`, `MethodFromForm` o `MethodFromQuery`. + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/site/src/content/docs/es/middleware/open-telemetry.md b/site/src/content/docs/es/middleware/open-telemetry.md new file mode 100644 index 00000000..4981e981 --- /dev/null +++ b/site/src/content/docs/es/middleware/open-telemetry.md @@ -0,0 +1,82 @@ +--- +title: OpenTelemetry +description: Instrumentación OpenTelemetry para requests HTTP en Echo. +sidebar: + order: 14 +--- + +[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) es un middleware que +proporciona instrumentación OpenTelemetry para requests HTTP. + +OpenTelemetry es un conjunto de herramientas open-source que proporcionan instrumentación para +aplicaciones cloud-native. + +- [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/) + +## Uso + +Agrega la dependencia del middleware OpenTelemetry con Go modules: + +```bash +go get github.com/labstack/echo-opentelemetry +``` + +Importa el middleware y la API de tracing de OpenTelemetry: + +```go +import ( + echootel "github.com/labstack/echo-opentelemetry" + "go.opentelemetry.io/otel/trace" +) +``` + +Regístralo con configuración completa: + +```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, +})) +``` + +Para opciones de configuración, consulta el struct +[`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28). + +Agrega el middleware en forma simplificada proporcionando solo el nombre del servidor: + +```go +e.Use(echootel.NewMiddleware("app.example.com")) +``` + +Agrega el middleware con opciones de configuración: + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + TracerProvider: tp, +})) +``` + +Obtén el tracer desde el contexto de Echo: + +```go +tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) +``` + +## Ejemplo + +El [ejemplo](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) exporta +metrics y spans a stdout, pero puedes usar cualquier exporter (OTLP, etc.). Consulta la +documentación de [OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters). diff --git a/site/src/content/docs/es/middleware/prometheus.md b/site/src/content/docs/es/middleware/prometheus.md new file mode 100644 index 00000000..88398857 --- /dev/null +++ b/site/src/content/docs/es/middleware/prometheus.md @@ -0,0 +1,284 @@ +--- +title: Prometheus +description: Genera metrics Prometheus para requests HTTP en Echo. +sidebar: + order: 15 +--- + +El middleware [Echo Prometheus](https://github.com/labstack/echo-prometheus) genera metrics +Prometheus para requests HTTP. + +## Uso + +Agrega el módulo requerido: + +```bash +go get github.com/labstack/echo-prometheus +``` + +Agrega el middleware Prometheus y una ruta para servir las metrics recopiladas: + +```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 +``` + +## Ejemplos + +Servir metrics desde el mismo servidor que las recopila: + +```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) + } +} +``` + +Servir metrics en un puerto separado: + +```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) + } +} +``` + +Salida de ejemplo (para el primer ejemplo): + +```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 +... +``` + +## Configuración personalizada + +### Servir metrics Prometheus personalizadas + +Usa metrics personalizadas con el registry por defecto de Prometheus: + +```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) + } +} +``` + +O crea tu propio registry y registra metrics personalizadas con él: + +```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) + } +} +``` + +### Omitir URLs + +Se puede pasar un skipper para evitar generar metrics en ciertas URLs: + +```go +package main + +import ( + "net/http" + "strings" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + mwConfig := echoprometheus.MiddlewareConfig{ + Skipper: func(c *echo.Context) bool { + return strings.HasPrefix(c.Path(), "/testurl") + }, // does not gather metrics on routes starting with `/testurl` + } + e.Use(echoprometheus.NewMiddlewareWithConfig(mwConfig)) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Escenarios complejos + +Modificar las definiciones de metrics por defecto de `echoprometheus`: + +```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/es/middleware/proxy.md b/site/src/content/docs/es/middleware/proxy.md new file mode 100644 index 00000000..981fd079 --- /dev/null +++ b/site/src/content/docs/es/middleware/proxy.md @@ -0,0 +1,134 @@ +--- +title: Proxy +description: Middleware de reverse proxy HTTP y WebSocket con load balancing. +sidebar: + order: 16 +--- + +Proxy proporciona un middleware de reverse proxy HTTP/WebSocket. Reenvía un request a un +servidor upstream usando una técnica de load balancing configurada. + +## Uso + +```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, + }, +}))) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +| Nombre | Valor | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### Reglas basadas en regex + +Para rewriting avanzado de requests proxied, también se pueden definir reglas usando +expresiones regulares. Los grupos de captura normales se pueden definir con `()` y referenciar +por índice (`$1`, `$2`, ...) en el path reescrito. + +Las reglas `RegexRewrite` y `Rewrite` normales se pueden combinar. + +```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", + }, +})) +``` + +Consulta el recetario de [reverse proxy](/es/cookbook/reverse-proxy/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/rate-limiter.md b/site/src/content/docs/es/middleware/rate-limiter.md new file mode 100644 index 00000000..f0b3c5d6 --- /dev/null +++ b/site/src/content/docs/es/middleware/rate-limiter.md @@ -0,0 +1,109 @@ +--- +title: Rate Limiter +description: Limita el número de requests desde una IP o identificador particular dentro de un periodo. +sidebar: + order: 17 +--- + +`RateLimiter` proporciona un middleware de rate limiter que limita el número de requests enviados +al servidor desde una IP o identificador particular dentro de un periodo. + +Por defecto, un store en memoria lleva la cuenta de los requests. La implementación en memoria por +defecto se centra en la corrección y puede no ser la mejor opción para un número alto de requests +concurrentes o una gran cantidad de identificadores distintos (>16k). + +## Uso + +Para agregar un rate limit a tu aplicación, agrega el middleware `RateLimiter`. El ejemplo siguiente +limita la aplicación a 20 requests/sec usando el store en memoria por defecto: + +```go +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) +``` + +:::note +Si la tasa proporcionada es un número float, `Burst` se trata como el valor redondeado hacia abajo de la tasa. +::: + +## Configuración personalizada + +```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)) +``` + +### Errores + +```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 +Para implementar tu propio store, satisface la interfaz `RateLimiterStore` y pásalo a +`RateLimiterConfig`. +::: + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```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/es/middleware/recover.md b/site/src/content/docs/es/middleware/recover.md new file mode 100644 index 00000000..82d94e3e --- /dev/null +++ b/site/src/content/docs/es/middleware/recover.md @@ -0,0 +1,61 @@ +--- +title: Recover +description: Recupérate de panics en cualquier punto de la cadena y delega al handler centralizado de errores. +sidebar: + order: 18 +--- + +El middleware Recover se recupera de panics en cualquier punto de la cadena, imprime el stack trace y +pasa el control al +[HTTPErrorHandler](/es/guide/customization/#http-error-handler) centralizado. + +## Uso + +```go +e.Use(middleware.Recover()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +El ejemplo anterior usa un `StackSize` de 1 KB y valores por defecto para `DisableStackAll` y +`DisablePrintStack`. + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/site/src/content/docs/es/middleware/redirect.md b/site/src/content/docs/es/middleware/redirect.md new file mode 100644 index 00000000..77a401d9 --- /dev/null +++ b/site/src/content/docs/es/middleware/redirect.md @@ -0,0 +1,101 @@ +--- +title: Redirect +description: Redirige requests entre variantes HTTP/HTTPS y www/non-www. +sidebar: + order: 19 +--- + +## HTTPS Redirect + +El middleware HTTPS redirect redirige requests HTTP a HTTPS. Por ejemplo, +`http://labstack.com` se redirige a `https://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPS WWW Redirect + +HTTPS WWW redirect redirige requests HTTP a www HTTPS. Por ejemplo, +`http://labstack.com` se redirige a `https://www.labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPS NonWWW Redirect + +HTTPS NonWWW redirect redirige requests HTTP a non-www HTTPS. Por ejemplo, +`http://www.labstack.com` se redirige a `https://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSNonWWWRedirect()) +``` + +## WWW Redirect + +WWW redirect redirige requests non-www a www. Por ejemplo, `http://labstack.com` se +redirige a `http://www.labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWW Redirect + +NonWWW redirect redirige requests www a non-www. Por ejemplo, `http://www.labstack.com` se +redirige a `http://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +El ejemplo anterior redirige requests HTTP a HTTPS con el código de estado +`307 - StatusTemporaryRedirect`. + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/site/src/content/docs/es/middleware/request-id.md b/site/src/content/docs/es/middleware/request-id.md new file mode 100644 index 00000000..730297de --- /dev/null +++ b/site/src/content/docs/es/middleware/request-id.md @@ -0,0 +1,89 @@ +--- +title: Request ID +description: Genera un ID único para cada request. +sidebar: + order: 20 +--- + +El middleware Request ID genera un ID único para un request. + +## Uso + +```go +e.Use(middleware.RequestID()) +``` + +Ejemplo: + +```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) + } +} +``` + +## Configuración personalizada + +```go +e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ + Generator: func() string { + return customGenerator() + }, +})) +``` + +## Configuración + +```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 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, // random 32-character string + TargetHeader: echo.HeaderXRequestID, +} +``` + +## Establecer ID + +Puedes establecer el ID desde quien hace el request con el header `X-Request-ID`. + +### Request + +```sh +curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param" +``` + +### Log + +```js +{"time":"2017-11-13T20:26:28.6438003+01:00","id":"3","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/?my=param","my":"param","status":200, "latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":13} +``` diff --git a/site/src/content/docs/es/middleware/rewrite.md b/site/src/content/docs/es/middleware/rewrite.md new file mode 100644 index 00000000..88e7b773 --- /dev/null +++ b/site/src/content/docs/es/middleware/rewrite.md @@ -0,0 +1,87 @@ +--- +title: Rewrite +description: Reescribe el path de la URL según reglas configuradas. +sidebar: + order: 21 +--- + +El middleware Rewrite reescribe el path de la URL según las reglas proporcionadas. Es útil para +compatibilidad hacia atrás o para crear enlaces más limpios y descriptivos. + +## Uso + +```go +e.Pre(middleware.Rewrite(map[string]string{ + "/old": "/new", + "/api/*": "/$1", + "/js/*": "/public/javascripts/$1", + "/users/*/orders/*": "/user/$1/order/$2", +})) +``` + +Los valores capturados en asteriscos pueden obtenerse por índice, por ejemplo `$1`, `$2`, etc. +Cada asterisco es no-greedy (se traduce a un grupo de captura `(.*?)`); al usar múltiples +asteriscos, un `*` final coincide con el resto del path. + +:::caution +El middleware Rewrite debe registrarse mediante `Echo#Pre()` para que se ejecute antes del router. +::: + +## Configuración personalizada + +```go +e := echo.New() +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) +``` + +## Configuración + +```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 +} +``` + +Configuración por defecto: + +| Nombre | Valor | +| ------- | -------------- | +| Skipper | DefaultSkipper | + +### Reglas basadas en regex + +Para rewriting avanzado de paths, también se pueden definir reglas usando expresiones regulares. +Los grupos de captura normales se pueden definir con `()` y referenciar por índice +(`$1`, `$2`, ...) en el path reescrito. + +`RegexRules` y `Rules` normales se pueden combinar. + +```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/es/middleware/secure.md b/site/src/content/docs/es/middleware/secure.md new file mode 100644 index 00000000..7bef8a6f --- /dev/null +++ b/site/src/content/docs/es/middleware/secure.md @@ -0,0 +1,113 @@ +--- +title: Secure +description: Protege contra XSS, content sniffing, clickjacking y otros ataques de inyección. +sidebar: + order: 22 +--- + +El middleware Secure proporciona protección contra cross-site scripting (XSS), content type +sniffing, clickjacking, conexiones inseguras y otros ataques de inyección de código. + +## Uso + +```go +e.Use(middleware.Secure()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +:::note +Pasar un `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions` o +`ContentSecurityPolicy` vacío deshabilita esa protección. +::: + +## Configuración + +```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 ,