bug_bunny 4.19.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 +17 -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 +10 -10
- data/docs/test/test.md +110 -0
- data/lib/bug_bunny/exception.rb +0 -4
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +2 -1
- data/skill/references/consumer.md +1 -1
- data/skill/references/errores.md +3 -3
- data/skill/references/routing.md +1 -1
- metadata +8 -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,22 @@
|
|
|
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
|
+
|
|
3
20
|
## [4.19.0] - 2026-06-25
|
|
4
21
|
|
|
5
22
|
> Capa de errores transport-agnostic (#52). Cambio **aditivo y retrocompatible**: `.message` no se degrada y se suman dos accesores. Bump minor.
|
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.
|
data/docs/release/release.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Release — bug_bunny
|
|
2
2
|
|
|
3
|
-
> meta: artefacto release · RFC-014 (
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
> `lib/bug_bunny/version.rb`, `CHANGELOG.md`, git · fecha 2026-06-
|
|
3
|
+
> meta: artefacto release · RFC-014 (`accepted`) · generado por `arch-structure`
|
|
4
|
+
> + `arch-enrich` (híbrido RFC-014 §2; nació como piloto manual #51, re-anclado
|
|
5
|
+
> a la RFC vigente) · anclado a `.github/workflows/release.yml`, `*.gemspec`,
|
|
6
|
+
> `lib/bug_bunny/version.rb`, `CHANGELOG.md`, git · fecha 2026-06-30 · cobertura:
|
|
7
7
|
> completa (régimen gema: build→publish, §e/f/g n/a).
|
|
8
8
|
|
|
9
9
|
## 1. Resumen
|
|
@@ -20,9 +20,9 @@ out-of-repo).
|
|
|
20
20
|
|
|
21
21
|
### a. Hecho verificable
|
|
22
22
|
|
|
23
|
-
- **Convención de versión:** SemVer `vX.X.X`. Actual: **4.
|
|
24
|
-
- **Source of truth:** tag remoto (`v4.
|
|
25
|
-
`lib/bug_bunny/version.rb` (`VERSION = '4.
|
|
23
|
+
- **Convención de versión:** SemVer `vX.X.X`. Actual: **4.19.0**.
|
|
24
|
+
- **Source of truth:** tag remoto (`v4.19.0`) + **triple mirror**
|
|
25
|
+
`lib/bug_bunny/version.rb` (`VERSION = '4.19.0'`) ← `bug_bunny.gemspec:7`
|
|
26
26
|
(`spec.version = BugBunny::VERSION`).
|
|
27
27
|
- **Changelog canónico:** `CHANGELOG.md` único.
|
|
28
28
|
- **Patrón de trigger:** `gema-tag` (patrón 1).
|
|
@@ -33,7 +33,7 @@ out-of-repo).
|
|
|
33
33
|
|
|
34
34
|
- **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
|
|
35
35
|
- **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
|
|
36
|
-
`v4.
|
|
36
|
+
`v4.19.0`).
|
|
37
37
|
- **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
|
|
38
38
|
`bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
|
|
39
39
|
`required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
|
|
@@ -58,7 +58,7 @@ out-of-repo).
|
|
|
58
58
|
`on: push: tags: ['v*']` → `ruby/setup-ruby@v1` → `gem build *.gemspec` +
|
|
59
59
|
`gem push *.gem` (auth `secrets.RUBYGEMS_API_KEY`). Auditable y versionado con
|
|
60
60
|
el código; se ancla a `file:line`, no se referencia como caja negra.
|
|
61
|
-
- **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> 4.
|
|
61
|
+
- **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> 4.19.0"`)
|
|
62
62
|
desde RubyGems — **no** git-source.
|
|
63
63
|
|
|
64
64
|
### e. Deploy / publish
|
|
@@ -80,7 +80,7 @@ procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría e
|
|
|
80
80
|
### h. Dependencias de deploy inter-servicio
|
|
81
81
|
|
|
82
82
|
- **Consumidores** (cruza RFC-018): servicios del fleet la pinnean
|
|
83
|
-
`~> 4.
|
|
83
|
+
`~> 4.19.0` (semántica minor-compatible). Un cambio de contrato del gem
|
|
84
84
|
(ej. el behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`)
|
|
85
85
|
obliga a los consumidores a migrar; el `CHANGELOG.md` lo documenta como
|
|
86
86
|
breaking note. **Orden de deploy:** los consumidores adoptan al hacer `bundle
|
data/docs/test/test.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Test — bug_bunny
|
|
2
|
+
|
|
3
|
+
> meta: artefacto test · RFC-013 · generado `arch-structure` (§a-§d) +
|
|
4
|
+
> `arch-enrich` (§e-§h) · anclado a `24ea397`, `Rakefile`, `bug_bunny.gemspec`,
|
|
5
|
+
> `spec/spec_helper.rb`, `spec/support/integration_helper.rb`,
|
|
6
|
+
> `.github/workflows/main.yml`, `CHANGELOG.md` · fecha 2026-06-30 · cobertura:
|
|
7
|
+
> §a-§d (estructura) + §e-§h (enrich, anclado a specs/CHANGELOG) completas.
|
|
8
|
+
|
|
9
|
+
## 1. Resumen
|
|
10
|
+
|
|
11
|
+
Suite principal **RSpec** (`spec/`, 22 specs: 15 unit + 7 integration). Tarea
|
|
12
|
+
`:test` legacy de **Minitest** (`test/`, 2 archivos) fuera del default y del CI.
|
|
13
|
+
CI corre `bundle exec rake` (= `:spec`) en Ruby 3.4.4. Sin coverage tool
|
|
14
|
+
configurado.
|
|
15
|
+
|
|
16
|
+
## 2. Cuerpo
|
|
17
|
+
|
|
18
|
+
### a. Suites, frameworks y niveles
|
|
19
|
+
|
|
20
|
+
| framework | dir | nivel | nº | propósito |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
| **RSpec** `~> 3.0` | `spec/unit/` | unit | 15 | client/session pool, configuration, consumer, producer, controller, raise_error, remote_error, request, route, observability, otel, resource, middleware |
|
|
23
|
+
| **RSpec** | `spec/integration/` | integration | 7 | client, consumer_middleware, controller, error_handling, infrastructure, publisher_confirms, resource — **requieren RabbitMQ real** (usan `BugBunny.create_connection` + pool) |
|
|
24
|
+
| **Minitest** `~> 5.0` (+ `mocha`, `minitest-reporters`) | `test/integration/` | integration (legacy) | 2 | `manual_client_test.rb`, `infrastructure_test.rb` — tarea `:test`, **no** en default ni CI |
|
|
25
|
+
|
|
26
|
+
Sin tags declarados (`:slow`/`:js`) en la config de RSpec.
|
|
27
|
+
|
|
28
|
+
### b. Comando de corrida
|
|
29
|
+
|
|
30
|
+
| objetivo | comando | qué corre |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| default / CI | `bundle exec rake` | tarea `:spec` (toda la suite RSpec) — `Rakefile:` `task default: :spec` |
|
|
33
|
+
| solo unit | `bundle exec rake spec:unit` | `spec/unit/**/*_spec.rb` |
|
|
34
|
+
| solo integration | `bundle exec rake spec:integration` | `spec/integration/**/*_spec.rb` (requiere broker) |
|
|
35
|
+
| Minitest legacy | `bundle exec rake test` | `test/**/*_test.rb` |
|
|
36
|
+
| CI | `.github/workflows/main.yml` → `bundle exec rake` | matrix Ruby `3.4.4`, `ruby/setup-ruby@v1` + `bundler-cache`, on push `main` + PR |
|
|
37
|
+
|
|
38
|
+
> Todas las tareas RSpec corren con `--require spec_helper` (`Rakefile`).
|
|
39
|
+
|
|
40
|
+
### c. Fixtures / Factories
|
|
41
|
+
|
|
42
|
+
- **Sin FactoryBot ni fixtures YAML.** El setup vive en `spec/spec_helper.rb`:
|
|
43
|
+
configura `BugBunny` (host/user/pass desde ENV con default `guest`), arma un
|
|
44
|
+
`ConnectionPool` de test (`TEST_POOL`, size 5) y declara rutas globales para
|
|
45
|
+
todos los specs (`resources :ping/:node/:user`, `get 'around'/'rescue'/'boom'/
|
|
46
|
+
'echo'`, `post 'events'`).
|
|
47
|
+
- **Carga `.env`** si existe (parse manual, `spec_helper.rb`).
|
|
48
|
+
- **Soporte:** `spec/support/integration_helper.rb` (helpers + `TEST_WORKER_QUEUE_OPTS`
|
|
49
|
+
para colas efímeras), `spec/support/bunny_mocks.rb` (mocks de Bunny para unit).
|
|
50
|
+
- `exchange_options = { durable: false, auto_delete: true }` → exchanges efímeros
|
|
51
|
+
en test.
|
|
52
|
+
|
|
53
|
+
### d. Configuración de coverage
|
|
54
|
+
|
|
55
|
+
**n/a** — sin SimpleCov ni `.simplecov` ni `SimpleCov.start` en el repo. No hay
|
|
56
|
+
umbral de coverage declarado.
|
|
57
|
+
|
|
58
|
+
### e. Gaps de cobertura
|
|
59
|
+
|
|
60
|
+
- **Integration specs no corren en CI:** `main.yml` no declara servicio RabbitMQ;
|
|
61
|
+
las 7 integration specs **se skipean** vía `rabbitmq_available?`
|
|
62
|
+
(`spec/support/integration_helper.rb:14`, ver `publisher_confirms_spec.rb:10`).
|
|
63
|
+
En CI solo se ejercitan las **15 unit specs** → el contrato AMQP real (publish/
|
|
64
|
+
consume/confirms contra broker) **no se valida en pipeline**, solo localmente
|
|
65
|
+
con broker. Gap relevante.
|
|
66
|
+
- **Sin medición de cobertura:** no hay SimpleCov ni umbral → la cobertura no está
|
|
67
|
+
cuantificada (no se sabe el % de líneas ejercidas).
|
|
68
|
+
|
|
69
|
+
### f. Contract-assessment
|
|
70
|
+
|
|
71
|
+
¿Los tests cubren los contratos públicos? (RFC-020/018/012/003)
|
|
72
|
+
|
|
73
|
+
| contrato | specs que lo cubren | veredicto |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Errores RFC-020** (status→excepción, materia prima) | `raise_error_spec`, `remote_error_spec`, `communication_error_wrapping_spec`, `error_handling_spec` (integration) | **bien cubierto** (unit) |
|
|
76
|
+
| **Consumed RFC-018** (Bunny::Exception→`CommunicationError`) | `communication_error_wrapping_spec`, `client_session_pool_spec` | cubierto (unit) |
|
|
77
|
+
| **Config RFC-012** (validaciones de `Configuration`) | `configuration_spec` | cubierto |
|
|
78
|
+
| **Confirms** (`PublishNacked`/`PublishUnroutable`) | `producer_spec`, `publisher_confirms_spec` (integration) | parcial en CI (la parte integration skipea sin broker) |
|
|
79
|
+
| **Operaciones/routing** (RFC-003, capa F2) | `route_spec`, `request_spec`, `controller_spec`, `controller_after_action_spec`, `resource_spec` | cubierto (unit) |
|
|
80
|
+
|
|
81
|
+
### g. Link a incidente → test de regresión
|
|
82
|
+
|
|
83
|
+
| incidente | test de regresión | ancla |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| **#52** (`status`/`raw_response` en toda la jerarquía + hardening de `format_error_message`) | `raise_error_spec` — comentarios explícitos "Alcance issue #52" | `spec/unit/raise_error_spec.rb:59,120,169` |
|
|
86
|
+
| **#49** (leak de `Bunny::TCPConnectionFailedForAllHosts` en `try_create`) | `communication_error_wrapping_spec` — "Client#publish — TCP fail en try_create (issue #49 caso original)" | `spec/unit/communication_error_wrapping_spec.rb:41` |
|
|
87
|
+
|
|
88
|
+
### h. PII en fixtures / factories
|
|
89
|
+
|
|
90
|
+
- **Sin PII.** No hay fixtures con datos personales: `spec_helper` usa
|
|
91
|
+
placeholders (`guest`/`localhost`), las rutas de test son `ping`/`node`/`user`
|
|
92
|
+
**sin payloads de datos reales**, y `bunny_mocks.rb` mockea el driver. Cruza
|
|
93
|
+
RFC-026: nada que clasificar/anonimizar. `confidence: high`.
|
|
94
|
+
|
|
95
|
+
## 3. Inferencias
|
|
96
|
+
|
|
97
|
+
- **Doble framework:** RSpec es el primario (default + CI); Minitest (`test/`,
|
|
98
|
+
`mocha`, `minitest-reporters` en gemspec) es **legacy** — el comentario del
|
|
99
|
+
`Rakefile` lo declara explícito ("tests de integración legacy de Minitest").
|
|
100
|
+
`confidence: high`.
|
|
101
|
+
- ~~CI on `master` pero la rama principal es `main` (drift de naming)~~ →
|
|
102
|
+
**resuelto** (`main.yml` ahora dispara `push: branches: [main]`). El trigger
|
|
103
|
+
`push` corre en la rama por default; `pull_request` cubre los PRs.
|
|
104
|
+
|
|
105
|
+
## 4. Cobertura y fronteras
|
|
106
|
+
|
|
107
|
+
- §a-§d (estructura) + §e-§h (enrich, anclado a specs/CHANGELOG) **completas**.
|
|
108
|
+
- **Integration specs requieren un RabbitMQ vivo** y se skipean sin broker (§e) —
|
|
109
|
+
por eso no se ejercitan en CI.
|
|
110
|
+
- **Contenido de cada test case** queda en el código, no se inventaria acá.
|
data/lib/bug_bunny/exception.rb
CHANGED
|
@@ -60,10 +60,6 @@ module BugBunny
|
|
|
60
60
|
# Se levanta al final de {BugBunny.configure} si algún atributo no pasa las validaciones.
|
|
61
61
|
class ConfigurationError < Error; end
|
|
62
62
|
|
|
63
|
-
# Error lanzado cuando ocurren un acceso no permitido a controladores.
|
|
64
|
-
# Protege contra vulnerabilidades de RCE validando la herencia de las clases enrutadas.
|
|
65
|
-
class SecurityError < Error; end
|
|
66
|
-
|
|
67
63
|
# Error lanzado cuando el broker responde NACK a una publicación en modo `:confirmed`.
|
|
68
64
|
#
|
|
69
65
|
# Un NACK significa que el broker rechazó explícitamente el mensaje (ej: política de
|
data/lib/bug_bunny/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -415,8 +415,9 @@ Limitación de RSpec: `instance_double` valida que el método exista pero **no**
|
|
|
415
415
|
**Causa:** No hubo respuesta en `config.rpc_timeout` segundos.
|
|
416
416
|
**Resolución:** Verificar que el worker esté activo y que el controlador remoto no lance excepciones silenciosas.
|
|
417
417
|
|
|
418
|
-
###
|
|
418
|
+
### Guard anti-RCE (403, no es excepción)
|
|
419
419
|
**Causa:** El mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
|
|
420
|
+
**Comportamiento:** El worker responde **403 Forbidden** + reject + log `event=consumer.security_violation` (`consumer.rb:222-228`); no levanta una excepción dedicada.
|
|
420
421
|
**Resolución:** Verificar la jerarquía de controladores y que `config.controller_namespace` coincida.
|
|
421
422
|
|
|
422
423
|
### BugBunny::RouteNotFoundError (404)
|
|
@@ -119,5 +119,5 @@ livenessProbe:
|
|
|
119
119
|
|-----------|-----------|
|
|
120
120
|
| Ruta no encontrada | 404 + log `event=consumer.route_not_found` |
|
|
121
121
|
| Controller no encontrado (namespace) | 404 + log `event=consumer.controller_not_found` |
|
|
122
|
-
| Controller no hereda de BugBunny::Controller | `
|
|
122
|
+
| Controller no hereda de BugBunny::Controller | 403 Forbidden + reject + log `event=consumer.security_violation` (guard anti-RCE) |
|
|
123
123
|
| Excepción no capturada en controller | 500 + log `event=controller.unhandled_exception` con backtrace |
|
data/skill/references/errores.md
CHANGED
|
@@ -7,7 +7,6 @@ StandardError
|
|
|
7
7
|
└── BugBunny::Error
|
|
8
8
|
├── BugBunny::CommunicationError
|
|
9
9
|
├── BugBunny::ConfigurationError
|
|
10
|
-
├── BugBunny::SecurityError
|
|
11
10
|
├── BugBunny::PublishNacked
|
|
12
11
|
├── BugBunny::PublishUnroutable
|
|
13
12
|
├── BugBunny::ClientError (4xx)
|
|
@@ -58,9 +57,10 @@ message, details } }`), parsealo en el boundary del servicio desde
|
|
|
58
57
|
**Validaciones:** host (String no vacío), port (1-65535), username/password (no nil), heartbeat (0-3600), rpc_timeout (>0), channel_prefetch (1-10000).
|
|
59
58
|
**Resolución:** Revisar el bloque `BugBunny.configure` y corregir valores.
|
|
60
59
|
|
|
61
|
-
###
|
|
60
|
+
### Guard anti-RCE (403, no es excepción)
|
|
62
61
|
**Causa:** Un mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
|
|
63
|
-
**Cuándo:** El consumer resuelve la clase pero falla
|
|
62
|
+
**Cuándo:** El consumer resuelve la clase (`constantize`) pero falla `controller_class < BugBunny::Controller` (`consumer.rb:222-228`).
|
|
63
|
+
**Comportamiento:** El worker **no levanta una excepción** — loguea `event=consumer.security_violation`, responde **403 Forbidden** al caller RPC y rechaza el mensaje sin requeue.
|
|
64
64
|
**Resolución:** Verificar que el controlador herede de `BugBunny::Controller` y que `config.controller_namespace` sea correcto.
|
|
65
65
|
|
|
66
66
|
### BugBunny::PublishNacked
|
data/skill/references/routing.md
CHANGED
|
@@ -69,7 +69,7 @@ El consumer resuelve el controlador concatenando:
|
|
|
69
69
|
|
|
70
70
|
Ejemplo: namespace `:admin`, controller `:reports` → `BugBunny::Controllers::Admin::ReportsController`
|
|
71
71
|
|
|
72
|
-
Valida que el controlador sea subclase de `BugBunny::Controller
|
|
72
|
+
Valida que el controlador sea subclase de `BugBunny::Controller` (guard anti-RCE). Si no, el worker loguea `consumer.security_violation`, responde **403 Forbidden** y rechaza el mensaje sin requeue (`consumer.rb:222-228`) — no levanta una excepción dedicada.
|
|
73
73
|
|
|
74
74
|
## Route Object
|
|
75
75
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 5.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -229,13 +229,18 @@ executables: []
|
|
|
229
229
|
extensions: []
|
|
230
230
|
extra_rdoc_files: []
|
|
231
231
|
files:
|
|
232
|
+
- AGENTS.md
|
|
232
233
|
- CHANGELOG.md
|
|
233
234
|
- CLAUDE.md
|
|
234
235
|
- README.md
|
|
235
236
|
- Rakefile
|
|
236
237
|
- docs/behavior/behavior.md
|
|
238
|
+
- docs/config/configuracion.md
|
|
239
|
+
- docs/consumed/rabbitmq.md
|
|
240
|
+
- docs/errors/errors.md
|
|
237
241
|
- docs/glossary/glossary.md
|
|
238
242
|
- docs/release/release.md
|
|
243
|
+
- docs/test/test.md
|
|
239
244
|
- initializer_example.rb
|
|
240
245
|
- lib/bug_bunny.rb
|
|
241
246
|
- lib/bug_bunny/client.rb
|
|
@@ -308,7 +313,7 @@ metadata:
|
|
|
308
313
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
309
314
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
310
315
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
311
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/
|
|
316
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v5.0.0/skill
|
|
312
317
|
post_install_message:
|
|
313
318
|
rdoc_options: []
|
|
314
319
|
require_paths:
|