bug_bunny 4.18.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +43 -0
- data/CHANGELOG.md +31 -0
- data/CLAUDE.md +14 -4
- data/README.md +0 -1
- data/docs/config/configuracion.md +172 -0
- data/docs/consumed/rabbitmq.md +102 -0
- data/docs/errors/errors.md +196 -0
- data/docs/release/release.md +108 -0
- data/docs/test/test.md +110 -0
- data/lib/bug_bunny/exception.rb +33 -7
- data/lib/bug_bunny/middleware/raise_error.rb +97 -43
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +7 -1
- data/skill/references/consumer.md +1 -1
- data/skill/references/errores.md +36 -7
- data/skill/references/routing.md +1 -1
- data/skills.yml +5 -21
- data/spec/unit/raise_error_spec.rb +176 -0
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1076566b6a3b8c25f6f00fc624c92681e8db881ffc3b8d6640da3bb7158333b
|
|
4
|
+
data.tar.gz: 2510fe871ec6f32804dba3faa7f1110408656e8cff30d2c0d537c7d0b30e1686
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13525491f89b99f1e1be12663f64990109e17b9da22e034ecea463a3fda5ff68abfb3ab10fdf274d43df5460b3c74eabd14e1699e2af7f8c38e608a78e22cb69
|
|
7
|
+
data.tar.gz: 9f31c6ed9fbfff650e2770f970ab94e49979f8eec8b254c212817ca410106b4f4293cc9b6cde87f382145a2483f639aabe0998134c5656b7ec6b053aebcb7c73
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# AGENTS.md — bug_bunny
|
|
2
|
+
|
|
3
|
+
Entrada de harness para agentes que trabajan en este repo. Para el contrato de
|
|
4
|
+
proyecto y convenciones de equipo, ver `CLAUDE.md`. Para la entrada humana, ver
|
|
5
|
+
`README.md`; para la entrada agente version-locked, `skill/SKILL.md`.
|
|
6
|
+
|
|
7
|
+
## Mapa de conocimiento
|
|
8
|
+
|
|
9
|
+
> Stanza RFC-008 r2 — **indexa, no duplica**. Cada fila apunta al artefacto de
|
|
10
|
+
> detalle que es la fuente de verdad de esa capa. Leé el artefacto antes de
|
|
11
|
+
> responder sobre su capa.
|
|
12
|
+
|
|
13
|
+
| capa | artefacto | estado | qué responde |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Comportamiento | `docs/behavior/behavior.md` | completo (6 flujos) | secuencias de publish/RPC/consume/confirms, contrato de error-wrapping |
|
|
16
|
+
| Glosario | `docs/glossary/glossary.md` | parcial (acreta por PR) | término de negocio → binding físico en `lib/` |
|
|
17
|
+
| Errores | `docs/errors/errors.md` | completo (§a/b/d estructura + §c política inferida) | jerarquía de excepciones públicas, mapeo `status→excepción`, shape del payload, política retry |
|
|
18
|
+
| Configuración | `docs/config/configuracion.md` | completo (estructura + enrich §f/g/h) | opciones de `Configuration`, defaults, failure-mode/threading, inyecciones del `Railtie`, ENV sugeridas |
|
|
19
|
+
| Dependencias consumidas | `docs/consumed/rabbitmq.md` | completo (estructura + enrich §c/e) | qué consume del broker RabbitMQ vía `bunny`, mapeo error-Bunny→excepción, retry/degradación |
|
|
20
|
+
| Test | `docs/test/test.md` | completo (estructura + enrich §e-h) | suites RSpec/Minitest, CI, contract-assessment, link a incidentes (#49/#52) |
|
|
21
|
+
| Release | `docs/release/release.md` | completo | patrón gema-tag, versionado, publish a RubyGems |
|
|
22
|
+
| Datos | — | n/a | gema sin DB |
|
|
23
|
+
| Operaciones / Interfaz / Topología | — | dev-structure F2 no implementado | contrato embebido en `README.md`/`skill/SKILL.md` (interim RFC-008 §2) |
|
|
24
|
+
| Eventos (RFC-005) | — | n/a | la gema es el transporte de eventos, no declara un catálogo de eventos de dominio propio |
|
|
25
|
+
| Seguridad (RFC-017) | — | n/a | sin authn/authz propias; el único control es el guard anti-RCE (clase enrutada debe heredar de `BugBunny::Controller`, si no → 403), cubierto en `docs/errors/` |
|
|
26
|
+
| Multi-tenancy (RFC-023) | — | n/a | sin modelo de tenant propio; el aislamiento es por `vhost` de RabbitMQ (config) |
|
|
27
|
+
| Data-lifecycle (RFC-026) | — | n/a | gema sin DB ni datos persistidos propios |
|
|
28
|
+
|
|
29
|
+
## Enriquecimiento
|
|
30
|
+
|
|
31
|
+
Completo en errors (§c), config (§f/g/h), consumed (§c/e) y test (§e-h), anclado
|
|
32
|
+
a YARD/specs/CHANGELOG. Pendiente de **verificación humana** (inferencias):
|
|
33
|
+
|
|
34
|
+
- `docs/errors/errors.md` §c — política retry inferida de HTTP/AMQP (`confidence:medium`); confirmar idempotencia de re-publish y caso `RemoteError` con consumidores reales.
|
|
35
|
+
- `docs/glossary/glossary.md` — parcial por diseño, acreta por PR.
|
|
36
|
+
|
|
37
|
+
## Convenciones operativas
|
|
38
|
+
|
|
39
|
+
- Ruby: `.ruby-version` · gemas: `Gemfile.lock` · manager: chruby + Bundler.
|
|
40
|
+
- Antes de commitear: `bundle exec rubocop -a` (base `rubocop-rails-omakase`),
|
|
41
|
+
`bundle exec rspec`, YARD incremental (`bundle exec yard stats --list-undoc`).
|
|
42
|
+
- Releases: `/gem-release` (el GitHub Action publica a RubyGems al pushear tag `v*`).
|
|
43
|
+
- Doc por-PR: si tocás una capa, mové su artefacto en el mismo PR (régimen RFC-001).
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.0.0] - 2026-07-01
|
|
4
|
+
|
|
5
|
+
> **BREAKING.** Se elimina la constante pública `BugBunny::SecurityError`. Aunque la excepción nunca se *levantaba*, su **ausencia rompe en evaluación**: un `rescue BugBunny::SecurityError` en un consumidor resuelve la constante cuando *cualquier* excepción entra a ese bloque → `NameError` que enmascara la excepción original. Por eso es breaking real, no inerte.
|
|
6
|
+
|
|
7
|
+
### Removed
|
|
8
|
+
- **`BugBunny::SecurityError` eliminada:** introducida en `4f27bea` ("security controller") como la excepción prevista del control anti-RCE, pero **nunca se cableó** — ninguna ruta de código la levanta. La protección real (la clase enrutada debe heredar de `BugBunny::Controller`) vive en `Consumer` (`consumer.rb:222-228`) y responde **403 Forbidden** + reject + log `consumer.security_violation`, no una excepción. Eliminar la clase **no debilita** la protección (el guard 403 queda intacto). Doc ajustada (`docs/errors/`, README, skill). — @Gabriel
|
|
9
|
+
|
|
10
|
+
**Migración para consumidores:** quitar cualquier `rescue BugBunny::SecurityError`; el caso de controlador inválido llega como respuesta **403** en el envelope RPC (mapeable a `BugBunny::ClientError`/manejo de status), no como excepción local.
|
|
11
|
+
|
|
12
|
+
**Alcance verificado:** el grep que confirmó "nunca levantada" fue **in-repo** (lib + spec + test de esta gema). No se auditaron apps/gemas consumidoras externas — el bump MAJOR es la salvaguarda para ellas.
|
|
13
|
+
|
|
14
|
+
### Correcciones
|
|
15
|
+
- **CI: el workflow disparaba en `push` a `master`, pero la rama por default es `main` (#56):** el job de tests (Ruby 3.4.4) nunca corría en push a `main`, solo entraba por `pull_request`. Apuntado a `main`. — @Gabriel
|
|
16
|
+
|
|
17
|
+
### Documentación
|
|
18
|
+
- **Cobertura de arquitectura completa (dev-* / RFC-artefacto):** nuevas capas `docs/errors/` (RFC-020), `docs/config/` (RFC-012), `docs/consumed/` (RFC-018) y `docs/test/` (RFC-013), con enriquecimiento semántico (política de errores, retry/degradación, threading, contract-assessment); `docs/release/` re-anclado a RFC-014 `accepted`; `AGENTS.md` con stanza "Mapa de conocimiento". Empaquetado version-locked en la gema (#54, #55, #57). — @Gabriel
|
|
19
|
+
|
|
20
|
+
## [4.19.0] - 2026-06-25
|
|
21
|
+
|
|
22
|
+
> Capa de errores transport-agnostic (#52). Cambio **aditivo y retrocompatible**: `.message` no se degrada y se suman dos accesores. Bump minor.
|
|
23
|
+
|
|
24
|
+
### Nuevas funcionalidades
|
|
25
|
+
- **`status` + `raw_response` uniformes en toda la jerarquía (#52):** subidos a la base `BugBunny::Error` (`attr_accessor`) y poblados por `RaiseError.on_complete` en **todas** las clases de error, no solo `UnprocessableEntity`. El consumidor accede a `e.status` / `e.raw_response` de forma uniforme; la gema queda agnóstica al payload y el boundary del servicio interpreta el envelope de dominio. — @Gabriel
|
|
26
|
+
|
|
27
|
+
### Correcciones
|
|
28
|
+
- **`.message` ya no vuelca un `Hash#inspect` con el envelope anidado (#52):** ante `{ error: { code, message, details } }`, `format_error_message` extrae `error.message` best-effort en vez de interpolar el Hash; mantiene el shape plano histórico (incluido `error: ""`) y cae a JSON. Es solo el string humano para logs/Sentry, no contrato. — @Gabriel
|
|
29
|
+
|
|
30
|
+
### Documentación
|
|
31
|
+
- `skill/references/errores.md` y `skill/SKILL.md`: nueva sección "Materia prima del error" (`status`/`raw_response`), formato de `.message` endurecido y advertencia de sanitización (no loguear `raw_response` crudo). — @Gabriel
|
|
32
|
+
- Piloto RFC-014: `docs/release/release.md` (suplemento de release de la gema) (#51) — @Gabriel
|
|
33
|
+
|
|
3
34
|
## [4.18.0] - 2026-05-26
|
|
4
35
|
|
|
5
36
|
> Behavior change menor: cualquier `Bunny::Exception` que escape en la frontera del gem (TCP fail, conn rota, canal cerrado, auth fail) ahora se envuelve como `BugBunny::CommunicationError`. La excepción original queda accesible vía `.cause`. Callers que rescatan `Bunny::TCPConnectionFailed` directo deben migrar a `BugBunny::CommunicationError`. Callers que ya rescatan `BugBunny::Error` / `CommunicationError`: sin cambios.
|
data/CLAUDE.md
CHANGED
|
@@ -13,10 +13,20 @@ BugBunny es una gema Ruby que implementa una capa de enrutamiento RESTful sobre
|
|
|
13
13
|
`skill/SKILL.md` agente version-locked) **indexan, no duplican**.
|
|
14
14
|
Artefactos generados por `dev-structure` / `dev-enrich`; compuestos por
|
|
15
15
|
`dev-compose`. Verificación humana antes de commitear.
|
|
16
|
-
- **Estado actual:**
|
|
17
|
-
`docs/
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
- **Estado actual:**
|
|
17
|
+
- `docs/data` = n/a (gema sin DB, declarado solo en índice).
|
|
18
|
+
- `docs/behavior` completo (6 flujos, backfill on-demand).
|
|
19
|
+
- `docs/glossary` parcial (acreta por PR).
|
|
20
|
+
- `docs/errors` (RFC-020) completo: §a/§b/§d (estructura) + §c política
|
|
21
|
+
(enrich, **inferida** de HTTP/AMQP, verificación humana pendiente).
|
|
22
|
+
- `docs/config` (RFC-012) completo: inventario §a-§e/§i + enrich §f/§g/§h
|
|
23
|
+
(failure-mode/threading, anclado a YARD); §j n/a.
|
|
24
|
+
- `docs/consumed` (RFC-018) RabbitMQ-vía-`bunny` completo: §a/§b/§d + enrich
|
|
25
|
+
§c/§e (retry/backoff/degradación, anclado a `consumer.rb`).
|
|
26
|
+
- `docs/test` (RFC-013) completo: estructura §a-§d + enrich §e-§h
|
|
27
|
+
(contract-assessment, link a incidentes #49/#52, gaps de CI).
|
|
28
|
+
- operaciones/interfaz/topología (RFC-003/004/006) = dev-structure F2 no
|
|
29
|
+
implementado (interim RFC-008 §2).
|
|
20
30
|
- **Para agentes AI**: `skill/SKILL.md` (empaquetada en el `.gem`) +
|
|
21
31
|
`skill/references/`.
|
|
22
32
|
- **Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de
|
data/README.md
CHANGED
|
@@ -379,7 +379,6 @@ BugBunny maps RabbitMQ responses to a semantic exception hierarchy, similar to h
|
|
|
379
379
|
BugBunny::Error
|
|
380
380
|
├── CommunicationError (wraps any Bunny::Exception — TCP/auth/channel — at the gem boundary; .cause preserves the original)
|
|
381
381
|
├── ConfigurationError (invalid config attribute)
|
|
382
|
-
├── SecurityError (unauthorized controller resolution)
|
|
383
382
|
├── PublishNacked (broker basic.nack on :confirmed publish)
|
|
384
383
|
├── PublishUnroutable (broker basic.return on mandatory + :confirmed)
|
|
385
384
|
├── ClientError (4xx)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Configuración — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto configuración · RFC-012 · generado `arch-structure` (inventario
|
|
4
|
+
> §a-§e/§i) + `arch-enrich` (§f/§g/§h/§j) · anclado a `24ea397`,
|
|
5
|
+
> `lib/bug_bunny/configuration.rb`, `lib/bug_bunny.rb`, `lib/bug_bunny/railtie.rb`,
|
|
6
|
+
> `lib/bug_bunny/consumer.rb`,
|
|
7
|
+
> `lib/generators/bug_bunny/install/templates/initializer.rb` · fecha 2026-06-30
|
|
8
|
+
> · cobertura: §a-§e/§i (estructura) + §f/§g/§h (enrich, anclado a YARD) completas;
|
|
9
|
+
> §j n/a.
|
|
10
|
+
|
|
11
|
+
## 1. Resumen
|
|
12
|
+
|
|
13
|
+
Gema configurable vía `BugBunny.configure { |c| ... }` (`bug_bunny.rb:59`) sobre
|
|
14
|
+
la clase `Configuration` (`configuration.rb`): atributos con **code-defaults**
|
|
15
|
+
seguros, validados por `validate!` al cierre del bloque. La gema **no lee `ENV[`
|
|
16
|
+
en `lib/`**; el `ENV.fetch` vive en el **template** que sugiere al consumidor
|
|
17
|
+
(`initializer.rb`). Inyecta al host vía `Railtie`.
|
|
18
|
+
|
|
19
|
+
## 2. Cuerpo
|
|
20
|
+
|
|
21
|
+
### a. Hecho verificable
|
|
22
|
+
|
|
23
|
+
- **Total opciones (`Configuration`):** 29 (attr_accessor/reader).
|
|
24
|
+
- **Validadas requeridas (`VALIDATIONS`, `configuration.rb:25-37`):** 5 (`host`,
|
|
25
|
+
`port`, `username`, `password`, `vhost` — `required: true`; igual tienen
|
|
26
|
+
default, `validate!` exige no-vacío).
|
|
27
|
+
- **Con default:** 29 (todas; defaults en `initialize`/`init_callback_defaults`).
|
|
28
|
+
- **Con rango validado:** 7 (`port`, `heartbeat`, `connection_timeout`,
|
|
29
|
+
`read_timeout`, `write_timeout`, `rpc_timeout`, `channel_prefetch`).
|
|
30
|
+
- **Secretas (por nombre):** 1 (`password`).
|
|
31
|
+
- **ENV leídas por la gema en `lib/`:** 0 (config 100% por code-default + bloque).
|
|
32
|
+
|
|
33
|
+
### b. Inventario base
|
|
34
|
+
|
|
35
|
+
Origen `code-default` salvo nota. Consumidor = `configuration.rb` (default en
|
|
36
|
+
`initialize`) salvo indicado.
|
|
37
|
+
|
|
38
|
+
| nombre | tipo | requerida | default | origen | consumidor | secret? |
|
|
39
|
+
|---|---|---|---|---|---|---|
|
|
40
|
+
| `host` | String | sí (validate!) | `'127.0.0.1'` | code-default | `configuration.rb:182` | no |
|
|
41
|
+
| `port` | Integer | sí (validate!, 1..65535) | `5672` | code-default | `:183` | no |
|
|
42
|
+
| `username` | String | sí (validate!) | `'guest'` | code-default | `:184` | no |
|
|
43
|
+
| `password` | String | sí (validate!) | `'guest'` | code-default | `:185` | **sí** |
|
|
44
|
+
| `vhost` | String | sí (validate!) | `'/'` | code-default | `:186` | no |
|
|
45
|
+
| `logger` | Logger | no | `Logger.new($stdout)` INFO | code-default | `:188` | no |
|
|
46
|
+
| `bunny_logger` | Logger | no | `Logger.new($stdout)` WARN | code-default | `:191` | no |
|
|
47
|
+
| `automatically_recover` | Boolean | no | `true` | code-default | `:193` | no |
|
|
48
|
+
| `network_recovery_interval` | Integer | no | `5` | code-default | `:194` | no |
|
|
49
|
+
| `max_reconnect_attempts` | Integer/nil | no | `nil` (reintenta ∞) | code-default | `:195` | no |
|
|
50
|
+
| `max_reconnect_interval` | Integer | no | `60` | code-default | `:196` | no |
|
|
51
|
+
| `connection_timeout` | Integer | no (1..300) | `10` | code-default | `:197` | no |
|
|
52
|
+
| `read_timeout` | Integer | no (1..300) | `30` | code-default | `:198` | no |
|
|
53
|
+
| `write_timeout` | Integer | no (1..300) | `30` | code-default | `:199` | no |
|
|
54
|
+
| `heartbeat` | Integer | no (0..3600) | `15` | code-default | `:200` | no |
|
|
55
|
+
| `continuation_timeout` | Integer (ms) | no | `15000` | code-default | `:201` | no |
|
|
56
|
+
| `channel_prefetch` | Integer | no (1..10000) | `1` | code-default | `:202` | no |
|
|
57
|
+
| `rpc_timeout` | Integer | no (1..3600) | `10` | code-default | `:203` | no |
|
|
58
|
+
| `health_check_interval` | Integer | no | `60` | code-default | `:204` | no |
|
|
59
|
+
| `health_check_file` | String/nil | no | `nil` (desactivado) | code-default | `:207` | no |
|
|
60
|
+
| `controller_namespace` | String | no | `'BugBunny::Controllers'` | code-default | `:210` | no |
|
|
61
|
+
| `log_tags` | Array | no | `[:uuid]` | code-default | `:212` | no |
|
|
62
|
+
| `exchange_options` | Hash | no | `{}` | code-default | `:215` | no |
|
|
63
|
+
| `queue_options` | Hash | no | `{}` | code-default | `:216` | no |
|
|
64
|
+
| `consumer_middlewares` | Stack (attr_reader) | no | `Stack.new` | code-default | `:218` | no |
|
|
65
|
+
| `rpc_reply_headers` | Proc/nil | no | `nil` | code-default | `:251` | no |
|
|
66
|
+
| `on_rpc_reply` | Proc/nil | no | `nil` | code-default | `:252` | no |
|
|
67
|
+
| `on_return` | Proc/nil | no | `nil` | code-default | `:253` | no |
|
|
68
|
+
| `nack_raise` | Boolean | no | `true` | code-default | `:254` | no |
|
|
69
|
+
| `return_raise` | Boolean | no | `true` | code-default | `:255` | no |
|
|
70
|
+
|
|
71
|
+
> **Override por request:** `nack_raise` y `return_raise` se sobreescriben por
|
|
72
|
+
> llamada con `nack_raise:` / `return_raise:` en `Client#publish` (scope-override
|
|
73
|
+
> → detalle a `arch-enrich`).
|
|
74
|
+
|
|
75
|
+
### c. Meta-templates
|
|
76
|
+
|
|
77
|
+
El install template (`initializer.rb`) **sugiere al consumidor** wirear 4 ENV
|
|
78
|
+
(la gema no las lee — el consumidor las pasa al bloque `configure`):
|
|
79
|
+
|
|
80
|
+
| plantilla | template | instancias |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `RABBITMQ_{X}` → `config.{y}` | `ENV.fetch('RABBITMQ_{X}', '{default}')` | `RABBITMQ_HOST`→host (`'localhost'`), `RABBITMQ_USERNAME`→username (`'guest'`), `RABBITMQ_PASSWORD`→password (`'guest'`), `RABBITMQ_VHOST`→vhost (`'/'`) |
|
|
83
|
+
|
|
84
|
+
> `spec/spec_helper.rb` usa `RABBITMQ_HOST` / `RABBITMQ_USER` / `RABBITMQ_PASS`
|
|
85
|
+
> (nombres distintos al template) — convención de test, no contrato.
|
|
86
|
+
|
|
87
|
+
### d. Derivaciones simples
|
|
88
|
+
|
|
89
|
+
- `url` ← `"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"`
|
|
90
|
+
(`configuration.rb:225`).
|
|
91
|
+
- `create_connection(**options)` ← `merge_connection_options(options)` sobre la
|
|
92
|
+
config global; las options explícitas pisan los defaults (`bug_bunny.rb:88`).
|
|
93
|
+
|
|
94
|
+
### e. Scheduling
|
|
95
|
+
|
|
96
|
+
**n/a** — la gema no trae `sidekiq.yml`/`queue.yml`/`recurring.yml` ni cron.
|
|
97
|
+
`health_check_interval` es polling interno de salud, no un scheduler.
|
|
98
|
+
|
|
99
|
+
### i. Inyecciones al host (`Railtie`, `railtie.rb`)
|
|
100
|
+
|
|
101
|
+
| inyección | qué hace | ancla |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `initializer 'bug_bunny.add_autoload_paths'` | registra `app/rabbit` en el autoloader (Zeitwerk, `eager_load: true`) si existe | `:15-18` |
|
|
104
|
+
| `config.after_initialize` → `ForkTracker.after_fork` | cierra la conexión heredada en cada fork (Rails 7.1+) | `:24-26` |
|
|
105
|
+
| `config.after_initialize` → `Puma.events.on_worker_boot` | mismo cierre, hook legacy Puma <5 | `:30-34` |
|
|
106
|
+
| `rake_tasks` | carga `tasks/bug_bunny.rake` (`bug_bunny:sync`) | `:38-40` |
|
|
107
|
+
| `Spring.after_fork` | cierra conexión en preloader Spring | `:43-47` |
|
|
108
|
+
|
|
109
|
+
### f. Enriquecimiento semántico
|
|
110
|
+
|
|
111
|
+
Agrupado por familia. Anclado a los YARD de `configuration.rb` y al comportamiento
|
|
112
|
+
del código.
|
|
113
|
+
|
|
114
|
+
| familia | categoría | failure-mode | side-effect | business-reason |
|
|
115
|
+
|---|---|---|---|---|
|
|
116
|
+
| Conexión (`host`/`port`/`username`/`password`/`vhost`) | conectividad | valor inválido/vacío → `ConfigurationError` en `validate!`; credencial/host errados → `CommunicationError` al conectar (`bug_bunny.rb:95`) | abre socket TCP al broker | identidad y destino del broker; `vhost` aísla ambientes |
|
|
117
|
+
| Timeouts (`connection_timeout`/`read_timeout`/`write_timeout`/`heartbeat`/`continuation_timeout`) | resiliencia/latencia | muy bajo → cortes espurios bajo carga; muy alto → detección de fallo lenta | — | tuning de la conexión Bunny; `heartbeat` detecta conexiones zombi |
|
|
118
|
+
| `rpc_timeout` | latencia | el worker remoto no responde a tiempo → `RequestTimeout` (`producer.rb:124,214`) | bloquea el hilo llamante hasta el timeout | techo de espera de un RPC síncrono |
|
|
119
|
+
| Resiliencia (`automatically_recover`/`network_recovery_interval`/`max_reconnect_attempts`/`max_reconnect_interval`) | resiliencia | `max_reconnect_attempts` agotado → el Consumer re-levanta y muere (`consumer.rb:110-112`) | reintentos con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval` (`consumer.rb:115-118`) | sobrevivir caídas transitorias del broker sin perder el worker |
|
|
120
|
+
| QoS (`channel_prefetch`) | rendimiento | alto → un worker lento acapara mensajes; `1` → menor throughput | controla unacked in-flight (backpressure) | balancea fairness vs throughput (default `1` = fair round-robin) |
|
|
121
|
+
| Health (`health_check_interval`/`health_check_file`) | observabilidad | `health_check_file` no escribible → el touch falla (degradación de visibilidad, no del flujo) | **escribe (touch) un archivo** en cada health check OK; `nil` desactiva | probe para orquestadores (K8s/Swarm) |
|
|
122
|
+
| Callbacks (`on_return`/`on_rpc_reply`/`rpc_reply_headers`) | extensibilidad | una excepción en `on_return` se captura pero **degrada visibilidad** (YARD `configuration.rb:147`) | corren en hilos sensibles (ver §h) | propagar trace-context / alertar unroutable |
|
|
123
|
+
| Confirms (`nack_raise`/`return_raise`) | integridad de entrega | `false` → NACK/return solo se logea, la llamada retorna `202` (modo legacy, posible pérdida silenciosa) | habilitan el raise de `PublishNacked`/`PublishUnroutable` | elegir entre fail-fast vs best-effort en publish confirmado |
|
|
124
|
+
| Routing (`controller_namespace`) | seguridad | clase resuelta no subclase de `BugBunny::Controller` → el worker responde **403** + reject (guard anti-RCE, `consumer.rb:222-228`) | acota qué clases son enrutables | superficie de control de RCE |
|
|
125
|
+
| Logging (`logger`/`bunny_logger`/`log_tags`) | observabilidad | — | salida a `$stdout` por default | trazabilidad estructurada |
|
|
126
|
+
| Infra (`exchange_options`/`queue_options`) | infraestructura | options incompatibles con el broker → `PreconditionFailed` (vía `CommunicationError`) | defaults globales mergeados por recurso | declaración AMQP por default |
|
|
127
|
+
|
|
128
|
+
### g. Ramificadores intra-config
|
|
129
|
+
|
|
130
|
+
- `health_check_file = nil` (default) **desactiva** el touchfile aunque
|
|
131
|
+
`health_check_interval` siga corriendo (`configuration.rb:99-101,207`).
|
|
132
|
+
- `return_raise` es **inerte cuando `mandatory: false`** — sin `mandatory` el
|
|
133
|
+
broker nunca retorna, así que el flag no tiene efecto (`configuration.rb:171`).
|
|
134
|
+
- `nack_raise`/`return_raise` se sobreescriben **por request** (`Client#publish`),
|
|
135
|
+
ganando sobre el valor global (scope-override).
|
|
136
|
+
|
|
137
|
+
### h. Threading
|
|
138
|
+
|
|
139
|
+
| opción | hilo de ejecución | restricción |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `on_return` | **hilo interno del consumidor de Bunny** (`configuration.rb:147`) | debe ser rápido y no lanzar; BugBunny captura, pero degrada visibilidad |
|
|
142
|
+
| `on_rpc_reply` | **hilo llamante** tras recibir el reply RPC (`configuration.rb:132`) | hidrata trace-context en el publisher |
|
|
143
|
+
| `rpc_reply_headers` | hilo del consumer, justo antes del `basic_publish` del reply (`configuration.rb:125`) | debe retornar un Hash de headers |
|
|
144
|
+
| reconexión del Consumer | hilo del `subscribe` loop (`consumer.rb:90,122`) | `sleep wait` bloquea ese hilo durante el backoff |
|
|
145
|
+
|
|
146
|
+
### j. Inyección a gemas configuradas
|
|
147
|
+
|
|
148
|
+
**n/a** — la gema **no** configura otras gemas vía bloque `Gema.configure` en
|
|
149
|
+
initializers; expone su propio `BugBunny.configure`. El `Railtie` inyecta al
|
|
150
|
+
host (§i), no a gemas terceras.
|
|
151
|
+
|
|
152
|
+
## 3. Inferencias
|
|
153
|
+
|
|
154
|
+
- **`requerida` de host/port/username/password/vhost:** `VALIDATIONS` las marca
|
|
155
|
+
`required: true`, pero `initialize` les da default → nunca son `nil` salvo que
|
|
156
|
+
el consumidor las setee a `''`/`nil`. Marcadas `sí (validate!)`: el contrato es
|
|
157
|
+
"no-vacías al cierre del bloque", no "sin default". `confidence: high`.
|
|
158
|
+
- **`password` secret?=sí** por nombre (regla RFC-012). El default `'guest'` es
|
|
159
|
+
el usuario default de RabbitMQ (placeholder), **no** un secreto real → ver §4.
|
|
160
|
+
|
|
161
|
+
## 4. Cobertura y fronteras
|
|
162
|
+
|
|
163
|
+
- §a-§e/§i (estructura) + §f/§g/§h (enrich) **completas** al commit ancla; §j n/a.
|
|
164
|
+
- **Linter de secretos (advisory):** `password` default `'guest'` y el template
|
|
165
|
+
`RABBITMQ_PASSWORD` default `'guest'` matchean el patrón de secreto, pero el
|
|
166
|
+
valor es el placeholder default de RabbitMQ, **no** un secreto hardcodeado. El
|
|
167
|
+
consumidor debe inyectar la credencial real vía ENV en prod (lo dice el header
|
|
168
|
+
del template). No es hallazgo de fuga.
|
|
169
|
+
- **Enriquecimiento (§f/§g/§h):** anclado a los YARD de `configuration.rb` y al
|
|
170
|
+
backoff real de `consumer.rb` — no inventado. La elección de valores concretos
|
|
171
|
+
(tuning de timeouts/prefetch por ambiente) es decisión operativa del consumidor,
|
|
172
|
+
no del artefacto.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Dependencia consumida: RabbitMQ (vía `bunny`) — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto consumed · RFC-018 · generado `arch-structure` (§a/§b/§d) +
|
|
4
|
+
> `arch-enrich` (§c/§e) · anclado a `24ea397`, `bug_bunny.gemspec`,
|
|
5
|
+
> `lib/bug_bunny.rb`, `lib/bug_bunny/session.rb`, `lib/bug_bunny/producer.rb`,
|
|
6
|
+
> `lib/bug_bunny/consumer.rb`, `lib/bug_bunny/middleware/raise_error.rb` · fecha
|
|
7
|
+
> 2026-06-30 · cobertura: §a/§b/§d (estructura) + §c/§e (enrich, anclado a
|
|
8
|
+
> `consumer.rb`) completas.
|
|
9
|
+
|
|
10
|
+
## 1. Resumen
|
|
11
|
+
|
|
12
|
+
La gema consume **un único sistema externo: el broker RabbitMQ**, vía el SDK
|
|
13
|
+
`bunny ~> 2.24` (AMQP 0-9-1). Es el corazón del gem: toda publicación, consumo,
|
|
14
|
+
RPC y declaración de infraestructura pasa por `Bunny`. El pooling de conexiones
|
|
15
|
+
lo da `connection_pool` (infra, ver §4).
|
|
16
|
+
|
|
17
|
+
## 2. Cuerpo
|
|
18
|
+
|
|
19
|
+
### a. Identidad
|
|
20
|
+
|
|
21
|
+
| campo | valor |
|
|
22
|
+
|---|---|
|
|
23
|
+
| proveedor / sistema | **RabbitMQ broker** |
|
|
24
|
+
| sub-tipo | **externo** (sistema de mensajería, no un repo del fleet) |
|
|
25
|
+
| transporte | AMQP 0-9-1 (TCP) |
|
|
26
|
+
| cliente nuestro | gem `bunny ~> 2.24` (`bug_bunny.gemspec:36`), envuelto por `BugBunny.create_connection` (`bug_bunny.rb:87`) + `Session` |
|
|
27
|
+
| auth | SASL user/pass (`username`/`password` de `Configuration`); URL `amqp://user:pass@host:port/vhost` |
|
|
28
|
+
| ancla (doc proveedor) | Bunny: https://github.com/ruby-amqp/bunny · AMQP 0-9-1: https://www.rabbitmq.com/amqp-0-9-1-reference.html |
|
|
29
|
+
|
|
30
|
+
### b. Operaciones consumidas (subset usado)
|
|
31
|
+
|
|
32
|
+
| operación Bunny | dónde | qué mandamos / esperamos |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `Bunny.new(opts).start` | `bug_bunny.rb:89,93` | abre `Bunny::Session` (conexión TCP + handshake); espera sesión iniciada |
|
|
35
|
+
| `after_recovery_completed` | `bug_bunny.rb:90` | callback de recuperación de conexión → log `bug_bunny.connection_recovered` |
|
|
36
|
+
| `session.create_channel` | `session.rb:177` (rescata fallo) | abre canal AMQP |
|
|
37
|
+
| reconnect / recovery | `session.rb:285` | reconexión; en fallo → `CommunicationError` |
|
|
38
|
+
| publish confirmado | `producer.rb:90` (`Producer#confirmed`) | `basic.publish` + publisher confirms; espera ACK/NACK |
|
|
39
|
+
| publisher confirms (`nacked_set`) | `producer.rb:235` | lee NACKs del canal → `PublishNacked` |
|
|
40
|
+
| `mandatory: true` + `basic.return` | `producer.rb:325` | mensaje no ruteable → `PublishUnroutable` (reply_code 312 NO_ROUTE) |
|
|
41
|
+
| RPC (publish + reply-to consume) | `producer.rb:124,214` | request/response; timeout → `RequestTimeout` |
|
|
42
|
+
| AMQP publish/consume genérico | `client.rb:168` | cualquier `Bunny::Exception` en la frontera → `CommunicationError` |
|
|
43
|
+
|
|
44
|
+
> El mapa completo de operaciones que la gema **expone** (no las que consume)
|
|
45
|
+
> es la capa operaciones (RFC-003, `docs/api/`, pendiente F2).
|
|
46
|
+
|
|
47
|
+
### d. Errores del proveedor → excepción nuestra
|
|
48
|
+
|
|
49
|
+
`bunny` levanta `Bunny::Exception` (y subclases). La gema las **envuelve** en la
|
|
50
|
+
frontera de abstracción. La columna "excepción nuestra" **referencia** el
|
|
51
|
+
catálogo `docs/errors/errors.md` (RFC-020), no lo redefine.
|
|
52
|
+
|
|
53
|
+
| error del proveedor (Bunny) | excepción nuestra | dónde se mapea |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `Bunny::Exception` (TCP fail, auth fail, vhost inválido) al conectar | `CommunicationError` | `bug_bunny.rb:95-98` |
|
|
56
|
+
| `Bunny::Exception` al crear canal | `CommunicationError` | `session.rb:177` |
|
|
57
|
+
| `Bunny::Exception` en reconexión | `CommunicationError` | `session.rb:285` |
|
|
58
|
+
| `Bunny::Exception` en publish confirmado | `CommunicationError` | `producer.rb:90` |
|
|
59
|
+
| `Bunny::Exception` / `ConnectionClosedError` en publish/consume | `CommunicationError` | `client.rb:168` |
|
|
60
|
+
| NACK del broker (publisher confirms) | `PublishNacked` | `producer.rb:235` |
|
|
61
|
+
| `basic.return` (mandatory, no ruteable) | `PublishUnroutable` | `producer.rb:325` |
|
|
62
|
+
| timeout esperando reply RPC | `RequestTimeout` | `producer.rb:124,214` |
|
|
63
|
+
|
|
64
|
+
> Regla: los callers no rescatan `Bunny::*` directo — `rescue
|
|
65
|
+
> BugBunny::CommunicationError` cubre cualquier fallo de transporte/broker. La
|
|
66
|
+
> original queda en `.cause`.
|
|
67
|
+
|
|
68
|
+
### c. Retry / idempotencia
|
|
69
|
+
|
|
70
|
+
| aspecto | comportamiento (anclado) |
|
|
71
|
+
|---|---|
|
|
72
|
+
| recuperación de conexión | `automatically_recover = true` (default): Bunny recupera canales/suscripciones tras corte TCP (`configuration.rb:60-61`). |
|
|
73
|
+
| retry del Consumer | `Consumer#subscribe` reintenta en cualquier `StandardError` con **backoff exponencial** `network_recovery_interval * 2^(n-1)` cap `max_reconnect_interval`, hasta `max_reconnect_attempts` (`nil` = ∞) (`consumer.rb:106-124`). |
|
|
74
|
+
| ack | `manual_ack: true` (`consumer.rb:90`) → entrega **at-least-once**: si el worker cae tras procesar pero antes del ack, el mensaje se re-entrega. **El handler debe ser idempotente.** |
|
|
75
|
+
| retry de publish | **no automático** — un publish fallido levanta `CommunicationError`/`PublishNacked`; reintentarlo puede **duplicar** salvo dedup aguas abajo. |
|
|
76
|
+
| RPC | `request` espera reply hasta `rpc_timeout`; el retry de un RPC mutante requiere idempotencia (correlación por `correlation_id`). |
|
|
77
|
+
|
|
78
|
+
### e. Degradación (si RabbitMQ cae)
|
|
79
|
+
|
|
80
|
+
- **Al conectar:** `create_connection` levanta `CommunicationError` (`bug_bunny.rb:95`); no hay fallback ni cola local — el caller decide (circuit-break/alertar).
|
|
81
|
+
- **Consumer:** entra al loop de reconexión con backoff; por default (`max_reconnect_attempts = nil`) **reintenta indefinidamente**, logueando `consumer.connection_error` con `retry_in_s` (`consumer.rb:120-122`). Si se fija un máximo y se agota → `consumer.reconnect_exhausted` y re-raise (el worker muere).
|
|
82
|
+
- **Publisher:** cada publish/RPC sobre conexión caída levanta `CommunicationError` (envuelto en `client.rb:168`); **sin buffering** — el mensaje no sale.
|
|
83
|
+
- **Sin circuit-breaker propio:** la gema no trae breaker ni outbox; la resiliencia aguas arriba (reintentar el comando, encolar, alertar) es responsabilidad del consumidor.
|
|
84
|
+
|
|
85
|
+
## 3. Inferencias
|
|
86
|
+
|
|
87
|
+
- El gem declara **dependencia directa** solo de RabbitMQ-vía-bunny. Las demás
|
|
88
|
+
gemas del gemspec (`activemodel`, `activesupport`, `rack`, `json`,
|
|
89
|
+
`concurrent-ruby`, `ostruct`) son librerías de soporte, no sistemas externos
|
|
90
|
+
consumidos → van a topología (RFC-006), no acá.
|
|
91
|
+
|
|
92
|
+
## 4. Cobertura y fronteras
|
|
93
|
+
|
|
94
|
+
- §a/§b/§d (estructura) + §c/§e (enrich, anclado a `consumer.rb`) **completas**.
|
|
95
|
+
- **`connection_pool` (`>= 2.4`):** utilidad de pooling de las conexiones Bunny
|
|
96
|
+
(`Resource.connection_pool`), **no** un sistema externo con su propio
|
|
97
|
+
contrato de error de red → no es entrada consumed; el timeout de pool
|
|
98
|
+
(`ConnectionPool::TimedStack`) nace dentro de `@pool.with` y termina envuelto
|
|
99
|
+
como `CommunicationError` (ver `CHANGELOG` #49). Pertenece a topología.
|
|
100
|
+
- **Subset, no la API completa de Bunny:** solo se documentan las operaciones
|
|
101
|
+
que el gem invoca. Parámetros de tuning (heartbeat, prefetch, timeouts) viven
|
|
102
|
+
en `docs/config/`.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Errores — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto errores · RFC-020 · generado `arch-structure` (§a/§b/§d) +
|
|
4
|
+
> `arch-enrich` (§c) · anclado a `d0533bf`, `lib/bug_bunny/exception.rb`,
|
|
5
|
+
> `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb`
|
|
6
|
+
> · fecha 2026-06-30 · cobertura: §a/§b/§d completas (estructura); §c política
|
|
7
|
+
> completa (enrich, **inferida** de HTTP/AMQP — verificación humana pendiente).
|
|
8
|
+
|
|
9
|
+
## 1. Resumen
|
|
10
|
+
|
|
11
|
+
Catálogo del unhappy path que la gema **emite** a sus consumidores: jerarquía de
|
|
12
|
+
excepciones bajo `BugBunny::Error`, el mapeo `status RPC → excepción` que aplica
|
|
13
|
+
`Middleware::RaiseError` en el lado cliente, y el shape del envelope de error que
|
|
14
|
+
emite el lado worker (`Controller`). La gema es **agnóstica al payload**: expone
|
|
15
|
+
la materia prima (`status` + `raw_response`) y no interpreta la estructura de
|
|
16
|
+
dominio del cuerpo (#52).
|
|
17
|
+
|
|
18
|
+
## 2. Cuerpo
|
|
19
|
+
|
|
20
|
+
### a. Inventario de excepciones públicas
|
|
21
|
+
|
|
22
|
+
Jerarquía (todas bajo `BugBunny::Error < ::StandardError`):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
::StandardError
|
|
26
|
+
└── BugBunny::Error base · attr_accessor :status, :raw_response (#52)
|
|
27
|
+
├── CommunicationError fallo de red/conexión/protocolo AMQP
|
|
28
|
+
├── ConfigurationError configuración inválida de la gema
|
|
29
|
+
├── PublishNacked broker NACK en publish :confirmed
|
|
30
|
+
├── PublishUnroutable broker return (mandatory) sin binding
|
|
31
|
+
├── ClientError base 4xx
|
|
32
|
+
│ ├── BadRequest 400
|
|
33
|
+
│ ├── NotFound 404
|
|
34
|
+
│ │ └── RoutingError 404 · ruta RPC inexistente en el remoto
|
|
35
|
+
│ ├── NotAcceptable 406
|
|
36
|
+
│ ├── RequestTimeout 408 · y timeout local de RPC
|
|
37
|
+
│ ├── Conflict 409
|
|
38
|
+
│ └── UnprocessableEntity 422 · parsea body, expone error_messages
|
|
39
|
+
└── ServerError base 5xx
|
|
40
|
+
├── InternalServerError 500 genérico
|
|
41
|
+
└── RemoteError 500 · propaga excepción serializada del worker
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| excepción | jerarquía base | qué la levanta (ancla) |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `Error` | `::StandardError` | base — no se levanta directa salvo `resource.rb:122` (pool ausente) |
|
|
47
|
+
| `CommunicationError` | `Error` | `bug_bunny.rb:96`, `session.rb:177,285`, `producer.rb:90`, `client.rb:168` — envuelve cualquier `Bunny::Exception` en la frontera del gem; original en `.cause` |
|
|
48
|
+
| `ConfigurationError` | `Error` | `configuration.rb:262,269,277` — validación al final de `BugBunny.configure` |
|
|
49
|
+
| `PublishNacked` | `Error` | `producer.rb:235` — NACK del broker en modo `:confirmed`. Attrs: `path`, `nacked_count`. Opt-out `nack_raise: false` |
|
|
50
|
+
| `PublishUnroutable` | `Error` | `producer.rb:325` — `basic.return` con `mandatory: true`. Attrs: `path`, `exchange`, `routing_key`, `reply_code`, `reply_text`, `correlation_id`. Opt-out `return_raise: false` |
|
|
51
|
+
| `ClientError` | `Error` | `raise_error.rb:170` — 4xx no mapeado explícito |
|
|
52
|
+
| `BadRequest` | `ClientError` | `raise_error.rb:38` — status 400 |
|
|
53
|
+
| `NotFound` | `ClientError` | `raise_error.rb:155` — status 404 genérico |
|
|
54
|
+
| `RoutingError` | `NotFound` | `raise_error.rb:152` — status 404 con `body['error_type'] == 'routing_error'` |
|
|
55
|
+
| `NotAcceptable` | `ClientError` | `raise_error.rb:40` — status 406 |
|
|
56
|
+
| `RequestTimeout` | `ClientError` | `raise_error.rb:41` (status 408) · `producer.rb:124,214` (timeout local de RPC) |
|
|
57
|
+
| `Conflict` | `ClientError` | `raise_error.rb:42` — status 409 |
|
|
58
|
+
| `UnprocessableEntity` | `ClientError` | `raise_error.rb:44` — status 422. Parsea body, expone `error_messages` (busca clave `errors`); setea `raw_response` en el ctor |
|
|
59
|
+
| `ServerError` | `Error` | `raise_error.rb:168` — 5xx no mapeado explícito |
|
|
60
|
+
| `InternalServerError` | `ServerError` | `raise_error.rb:67`, `producer.rb:396` (JSON inválido) — status 500 genérico |
|
|
61
|
+
| `RemoteError` | `ServerError` | `raise_error.rb:63` — status 5xx con `body['bug_bunny_exception']`. Attrs: `original_class`, `original_message`, `original_backtrace` |
|
|
62
|
+
|
|
63
|
+
> `ArgumentError`/`NameError` que levantan `client.rb:51`, `controller.rb:101,171`,
|
|
64
|
+
> `routing/*` son de la API de configuración/programación (uso incorrecto del
|
|
65
|
+
> dev), no del contrato runtime RPC — no se listan como contrato de error público.
|
|
66
|
+
|
|
67
|
+
### b. Códigos de estado por superficie
|
|
68
|
+
|
|
69
|
+
Mapeo `status → excepción` que aplica `Middleware::RaiseError#on_complete`
|
|
70
|
+
(`raise_error.rb:32-48`) en el **lado cliente** del RPC. Referencia las
|
|
71
|
+
operaciones de RFC-003 (`docs/api/`, hoy pendiente — ver §4), no las redefine.
|
|
72
|
+
|
|
73
|
+
| superficie | status | excepción levantada | cuándo |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| Cliente RPC (`RaiseError`) | 200..299 | — (none) | éxito, flujo normal |
|
|
76
|
+
| Cliente RPC | 400 | `BadRequest` | bad request |
|
|
77
|
+
| Cliente RPC | 404 | `NotFound` / `RoutingError` | recurso / ruta RPC inexistente (`error_type: routing_error`) |
|
|
78
|
+
| Cliente RPC | 406 | `NotAcceptable` | content negotiation |
|
|
79
|
+
| Cliente RPC | 408 | `RequestTimeout` | timeout reportado por el remoto |
|
|
80
|
+
| Cliente RPC | 409 | `Conflict` | conflicto de estado/negocio |
|
|
81
|
+
| Cliente RPC | 422 | `UnprocessableEntity` | validación semántica del modelo remoto |
|
|
82
|
+
| Cliente RPC | 500..599 | `RemoteError` / `InternalServerError` | error del worker (RemoteError si trae `bug_bunny_exception`) |
|
|
83
|
+
| Cliente RPC | otro ≥500 | `ServerError` | 5xx no mapeado |
|
|
84
|
+
| Cliente RPC | otro ≥400 | `ClientError` | 4xx no mapeado |
|
|
85
|
+
| Productor (publish `:confirmed`) | n/a (AMQP) | `PublishNacked` / `PublishUnroutable` | NACK / return del broker — no es status HTTP |
|
|
86
|
+
| Transporte (frontera gem) | n/a (AMQP) | `CommunicationError` | fallo de red/broker, envuelve `Bunny::Exception` |
|
|
87
|
+
| **Worker (dispatch)** | **403** | — (no excepción; responde 403 + reject) | guard anti-RCE: clase enrutada no subclase de `BugBunny::Controller` (`consumer.rb:222-228`) |
|
|
88
|
+
|
|
89
|
+
### c. Política por error
|
|
90
|
+
|
|
91
|
+
Enriquecimiento `arch-enrich` (RFC-020 §c). Política **inferida** de semántica
|
|
92
|
+
HTTP (RFC 9110) + AMQP — verificación humana pendiente (ver §3).
|
|
93
|
+
|
|
94
|
+
| excepción | retriable | backoff | idempotencia | acción sugerida |
|
|
95
|
+
|---|---|---|---|---|
|
|
96
|
+
| `CommunicationError` | sí (fallo transitorio de red/broker) | sí (`network_recovery_interval`; Bunny auto-recover) | el retry de un **publish** puede duplicar salvo consumidor idempotente; un **read** RPC es seguro | reintentar con backoff; si persiste, circuit-break y alertar |
|
|
97
|
+
| `ConfigurationError` | **no** (bug de config) | — | n/a | fail-fast al boot; corregir el bloque `configure` |
|
|
98
|
+
| `PublishNacked` | sí, con cautela (NACK puede ser transitorio: disk full, replicación) | sí | el re-publish puede duplicar → requiere dedup aguas abajo | reintentar acotado; si persiste, alertar (problema del broker) |
|
|
99
|
+
| `PublishUnroutable` | **no** (no hay binding para la routing key) | — | n/a | corregir routing key / topología de bindings; alertar (mensaje perdido) |
|
|
100
|
+
| `BadRequest` (400) | **no** | — | n/a | corregir el request del cliente |
|
|
101
|
+
| `NotFound` (404) | **no** | — | n/a | verificar recurso/identificador |
|
|
102
|
+
| `RoutingError` (404) | **no** | — | n/a | corregir verbo/path; el servicio remoto no tiene esa ruta registrada |
|
|
103
|
+
| `NotAcceptable` (406) | **no** | — | n/a | corregir content negotiation |
|
|
104
|
+
| `RequestTimeout` (408) | sí (transitorio) | sí | el retry de un publish/RPC mutante puede duplicar → idempotencia requerida | reintentar con backoff; subir `rpc_timeout` si es crónico |
|
|
105
|
+
| `Conflict` (409) | **no** sin resolver el conflicto | — | depende del flujo | resolver el estado en conflicto antes de reintentar |
|
|
106
|
+
| `UnprocessableEntity` (422) | **no** (error semántico/validación) | — | n/a | corregir el payload; leer `error_messages` / `raw_response` |
|
|
107
|
+
| `ClientError` (4xx genérico) | **no** (default 4xx) | — | n/a | inspeccionar status concreto |
|
|
108
|
+
| `ServerError` (5xx genérico) | sí (transitorio) | sí | idempotencia requerida si la op muta estado | reintentar con backoff |
|
|
109
|
+
| `InternalServerError` (500) | sí (transitorio) | sí | idem | reintentar con backoff; si persiste, escalar al dueño del worker |
|
|
110
|
+
| `RemoteError` (500) | depende de `original_class` | depende | depende | inspeccionar `original_class`/`original_backtrace`; tratar como bug del worker remoto, no reintentar a ciegas |
|
|
111
|
+
|
|
112
|
+
### d. Shape del payload de error
|
|
113
|
+
|
|
114
|
+
**Lado worker → wire (lo que emite `Controller#handle_exception`, `controller.rb:224-233`)**
|
|
115
|
+
ante una excepción no mapeada por `rescue_from`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"status": 500,
|
|
120
|
+
"headers": { },
|
|
121
|
+
"body": {
|
|
122
|
+
"error": "Internal Server Error",
|
|
123
|
+
"detail": "<exception.message>",
|
|
124
|
+
"type": "<exception.class.name>",
|
|
125
|
+
"bug_bunny_exception": {
|
|
126
|
+
"class": "<clase original>",
|
|
127
|
+
"message": "<mensaje original>",
|
|
128
|
+
"backtrace": ["<hasta 25 líneas>"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- El envelope `bug_bunny_exception` lo arma `RemoteError.serialize` (`remote_error.rb:29`);
|
|
135
|
+
también lo agrega `Consumer` cuando `status == 500 && exception` (`consumer.rb:325`).
|
|
136
|
+
Es lo que el cliente reconstruye como `RemoteError` (`raise_error.rb:61-64`).
|
|
137
|
+
- `render status:, json:` (`controller.rb:242`) deja el shape del body de error de
|
|
138
|
+
dominio a criterio del worker — la gema no lo impone.
|
|
139
|
+
|
|
140
|
+
**Lado cliente — materia prima expuesta (#52).** Toda excepción derivada de una
|
|
141
|
+
respuesta RPC trae, vía `RaiseError#raise_typed` (`raise_error.rb:82-86`):
|
|
142
|
+
|
|
143
|
+
- `e.status` → `Integer` (el código de la respuesta).
|
|
144
|
+
- `e.raw_response` → `Hash | String | nil` (el cuerpo crudo, **sin interpretar**).
|
|
145
|
+
`nil` para errores no-RPC (`CommunicationError`, `ConfigurationError`).
|
|
146
|
+
|
|
147
|
+
**Shapes que `format_error_message` reconoce para el `.message` humano**
|
|
148
|
+
(`raise_error.rb:110-141`, best-effort, **NO contrato**):
|
|
149
|
+
|
|
150
|
+
1. Envelope anidado: `{ "error": { "message": "..." } }` → extrae `error.message`.
|
|
151
|
+
2. Shape plano histórico: `{ "error": "texto", "detail": "..." }` → `"texto - detail"`.
|
|
152
|
+
3. Fallback: `body.to_json` (nunca `Hash#inspect`).
|
|
153
|
+
|
|
154
|
+
`UnprocessableEntity` además expone `error_messages` (`exception.rb:219-263`):
|
|
155
|
+
parsea el body, devuelve `parsed['errors']` por convención o el cuerpo completo.
|
|
156
|
+
|
|
157
|
+
> **Seguridad (cruza RFC-017):** `raw_response` puede contener datos sensibles en
|
|
158
|
+
> `details`. La gema lo entrega crudo a propósito; **sanitizar antes de cualquier
|
|
159
|
+
> sink** (Sentry/logs) filtrando `password|pass|passwd|secret|token|api_key|auth`
|
|
160
|
+
> → `[FILTERED]` es responsabilidad del consumidor (`exception.rb:25-30`).
|
|
161
|
+
|
|
162
|
+
## 3. Inferencias
|
|
163
|
+
|
|
164
|
+
- **Guard anti-RCE = 403, no excepción.** El control de seguridad que valida la
|
|
165
|
+
herencia de la clase enrutada vive en `consumer.rb:222-228`: si la clase
|
|
166
|
+
resuelta no es subclase de `BugBunny::Controller`, el worker loguea
|
|
167
|
+
`consumer.security_violation`, responde **403 'Forbidden'** (`handle_fatal_error`)
|
|
168
|
+
y rechaza el mensaje sin requeue — **no levanta una excepción dedicada**. La ex
|
|
169
|
+
`BugBunny::SecurityError` (nunca levantada en ninguna ruta de código) fue
|
|
170
|
+
**eliminada como dead code**; el contrato real del unhappy path de RCE es el 403
|
|
171
|
+
(ver §b). `confidence: high` (verificado por grep exhaustivo + lectura del guard).
|
|
172
|
+
- **§b superficies AMQP** (`PublishNacked`/`PublishUnroutable`/`CommunicationError`)
|
|
173
|
+
no tienen status HTTP: son señales del protocolo AMQP, no del envelope RPC. Se
|
|
174
|
+
listan como `n/a (AMQP)` para no forzar un código HTTP inventado.
|
|
175
|
+
- **§c política (enrich):** la columna retriable/backoff/idempotencia/acción está
|
|
176
|
+
**inferida** de la semántica estándar (HTTP RFC 9110 para 4xx/5xx, comportamiento
|
|
177
|
+
AMQP para NACK/return/transporte), **no** de una política declarada en el código
|
|
178
|
+
del gem. `confidence: medium`. A confirmar con el dueño del código /
|
|
179
|
+
consumidores reales (sobre todo idempotencia de re-publish y el caso
|
|
180
|
+
`RemoteError`, que depende del `original_class` del worker). El gem **no impone**
|
|
181
|
+
retry: la decisión es del consumidor en su boundary.
|
|
182
|
+
|
|
183
|
+
## 4. Cobertura y fronteras
|
|
184
|
+
|
|
185
|
+
- **§a/§b/§d (estructura) + §c (enrich, política inferida) completas** al commit
|
|
186
|
+
ancla. §c marcada `confidence: medium` (ver §3).
|
|
187
|
+
- **RFC-003 (`docs/api/`) pendiente:** §b referencia "superficie" de operaciones
|
|
188
|
+
pero la capa operaciones no está generada (dev-structure F2, ver `CLAUDE.md`).
|
|
189
|
+
Cuando se genere, §b debe cruzar las operaciones reales.
|
|
190
|
+
- **Frontera con `consumed` (RFC-018):** este artefacto = errores que la gema
|
|
191
|
+
**emite**. Los errores de `Bunny::*` que la gema **consume** y envuelve en
|
|
192
|
+
`CommunicationError` son su mapeo error-proveedor→excepción; viven en
|
|
193
|
+
`docs/consumed/rabbitmq.md` §d (que referencia este catálogo, no lo redefine).
|
|
194
|
+
- **Errores internos no-públicos** (rescatados adentro, no cruzan la frontera) y
|
|
195
|
+
los `ArgumentError`/`NameError` de mal-uso de la API de config quedan fuera:
|
|
196
|
+
no son contrato runtime.
|