bug_bunny 4.10.1 → 4.10.3
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/CHANGELOG.md +5 -0
- data/README.md +51 -0
- data/lib/bug_bunny/remote_error.rb +1 -1
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +6 -1
- data/skill/SKILL.md +4 -0
- data/skill/references/errores.md +35 -1
- data/spec/unit/remote_error_spec.rb +68 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31a6f6e9b83b1a82216f344a101ca0a99daebb5fd9e44bf5abc9c9e9753f4011
|
|
4
|
+
data.tar.gz: f101254aad9d8bbd66a359438a1efe970acd58e4b853841bcda72966c2472c8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b8a42f884ae9c8075dab9e4052658a5c0828cae904800fe222fd058cdaf3cd1307bba6478ca7824e273a517484d71da0416fd3b7c38bb08a9a0845e39dacb90
|
|
7
|
+
data.tar.gz: 6de916fae62566f01142b557b6e6c4ed4cf44d58298d9ff3c174e8d38b4422238487dacd29f273d7b3ea356d30f8c08a5c5a872f9ed08f6cacb8a540511a933a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.10.2] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Correcciones
|
|
6
|
+
- **RemoteError#to_s recursión infinita:** Corregir `SystemStackError` al invocar `to_s` en `BugBunny::RemoteError` en IRB. Antes, `to_s` llamaba a `message`, que en Ruby delega a `to_s`, generando recursión infinita. Ahora usa `super` para invocar `Exception#to_s` directamente. — @Gabriel
|
|
7
|
+
|
|
3
8
|
## [4.10.1] - 2026-04-08
|
|
4
9
|
|
|
5
10
|
### Correcciones
|
data/README.md
CHANGED
|
@@ -228,6 +228,57 @@ Las claves sensibles (`password`, `token`, `secret`, `api_key`, `authorization`,
|
|
|
228
228
|
|
|
229
229
|
---
|
|
230
230
|
|
|
231
|
+
## Error Handling
|
|
232
|
+
|
|
233
|
+
BugBunny maps RabbitMQ responses to a semantic exception hierarchy, similar to how HTTP clients handle status codes.
|
|
234
|
+
|
|
235
|
+
### Exception Hierarchy
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
BugBunny::Error
|
|
239
|
+
├── ClientError (4xx)
|
|
240
|
+
│ ├── BadRequest (400)
|
|
241
|
+
│ ├── NotFound (404)
|
|
242
|
+
│ ├── NotAcceptable (406)
|
|
243
|
+
│ ├── RequestTimeout (408)
|
|
244
|
+
│ ├── Conflict (409)
|
|
245
|
+
│ └── UnprocessableEntity (422)
|
|
246
|
+
└── ServerError (5xx)
|
|
247
|
+
├── InternalServerError (500+)
|
|
248
|
+
└── RemoteError (500)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Remote Exception Propagation
|
|
252
|
+
|
|
253
|
+
When a controller raises an unhandled exception, BugBunny serializes it and sends it back to the caller as a 500 response. The client-side middleware reconstructs it as a `BugBunny::RemoteError` with full access to the original exception details:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
begin
|
|
257
|
+
node = RemoteNode.find('node-123')
|
|
258
|
+
rescue BugBunny::RemoteError => e
|
|
259
|
+
e.original_class # => "TypeError"
|
|
260
|
+
e.original_message # => "nil can't be coerced into Integer"
|
|
261
|
+
e.original_backtrace # => Array<String> from the remote service
|
|
262
|
+
rescue BugBunny::NotFound
|
|
263
|
+
# Resource doesn't exist
|
|
264
|
+
rescue BugBunny::RequestTimeout
|
|
265
|
+
# Consumer didn't respond in time
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Validation Errors
|
|
270
|
+
|
|
271
|
+
`Resource#save` returns `false` on validation failure and loads remote errors into the model:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
order = RemoteOrder.new(total: -1)
|
|
275
|
+
unless order.save
|
|
276
|
+
order.errors.full_messages # => ["total must be greater than 0"]
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
231
282
|
## Documentation
|
|
232
283
|
|
|
233
284
|
- [Concepts](docs/concepts.md) — What BugBunny is, AMQP in 5 minutes, RPC vs fire-and-forget
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -83,7 +83,12 @@ module BugBunny
|
|
|
83
83
|
# @raise [Bunny::TCPConnectionFailed] Si no se puede conectar al servidor.
|
|
84
84
|
def self.create_connection(**options)
|
|
85
85
|
conn_options = merge_connection_options(options)
|
|
86
|
-
Bunny.new(conn_options).tap
|
|
86
|
+
Bunny.new(conn_options).tap do |conn|
|
|
87
|
+
conn.after_recovery_completed do
|
|
88
|
+
safe_log(:info, 'bug_bunny.connection_recovered', host: conn_options[:host])
|
|
89
|
+
end
|
|
90
|
+
conn.start
|
|
91
|
+
end
|
|
87
92
|
end
|
|
88
93
|
|
|
89
94
|
# Cierra la conexión global si existe.
|
data/skill/SKILL.md
CHANGED
|
@@ -238,6 +238,10 @@ No registrar consumer middlewares durante la ejecución de `call()`. El stack to
|
|
|
238
238
|
**Causa:** Fallo de validación en el servicio remoto.
|
|
239
239
|
**Resolución:** `resource.save` devuelve `false`. Acceder a `resource.errors` o `rescue` con `e.error_messages`.
|
|
240
240
|
|
|
241
|
+
### BugBunny::RemoteError (500)
|
|
242
|
+
**Causa:** Excepción no manejada en el controller remoto. Se serializa y propaga al cliente RPC con clase, mensaje y backtrace originales.
|
|
243
|
+
**Resolución:** `rescue BugBunny::RemoteError => e` y acceder a `e.original_class`, `e.original_message`, `e.original_backtrace`. Revisar logs del consumer (`event=controller.unhandled_exception`).
|
|
244
|
+
|
|
241
245
|
### BugBunny::CommunicationError
|
|
242
246
|
**Causa:** Fallo de conexión o reconexión agotada.
|
|
243
247
|
**Resolución:** Verificar conectividad a RabbitMQ. Revisar `max_reconnect_attempts` y logs de reconexión.
|
data/skill/references/errores.md
CHANGED
|
@@ -16,7 +16,8 @@ StandardError
|
|
|
16
16
|
│ ├── BugBunny::Conflict (409)
|
|
17
17
|
│ └── BugBunny::UnprocessableEntity (422)
|
|
18
18
|
└── BugBunny::ServerError (5xx)
|
|
19
|
-
|
|
19
|
+
├── BugBunny::InternalServerError (500+)
|
|
20
|
+
└── BugBunny::RemoteError (500)
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
## Errores de Infraestructura
|
|
@@ -83,6 +84,39 @@ end
|
|
|
83
84
|
**Causa:** Cualquier error de servidor no mapeado a InternalServerError.
|
|
84
85
|
**Resolución:** Similar a InternalServerError.
|
|
85
86
|
|
|
87
|
+
### BugBunny::RemoteError (500)
|
|
88
|
+
**Causa:** Excepción no manejada en el controller remoto. El error se serializa y propaga al cliente RPC.
|
|
89
|
+
|
|
90
|
+
**Flujo completo:**
|
|
91
|
+
1. El controller remoto lanza una excepción (ej: `TypeError`).
|
|
92
|
+
2. `BugBunny::Controller#handle_exception` la captura y serializa con `RemoteError.serialize(exception)` → `{ class:, message:, backtrace: }` (máx 25 líneas).
|
|
93
|
+
3. El consumer responde con status 500 y el campo `bug_bunny_exception` en el body.
|
|
94
|
+
4. El middleware `RaiseError` del cliente detecta `bug_bunny_exception`, reconstituye un `BugBunny::RemoteError` y lo lanza localmente.
|
|
95
|
+
|
|
96
|
+
**Acceso a detalles:**
|
|
97
|
+
```ruby
|
|
98
|
+
# Con Client directo
|
|
99
|
+
begin
|
|
100
|
+
client.request('users/42', method: :get)
|
|
101
|
+
rescue BugBunny::RemoteError => e
|
|
102
|
+
e.original_class # => "TypeError"
|
|
103
|
+
e.original_message # => "nil can't be coerced into Integer"
|
|
104
|
+
e.original_backtrace # => Array<String> (backtrace del servicio remoto)
|
|
105
|
+
e.to_s # => "BugBunny::RemoteError(TypeError): nil can't be coerced into Integer"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Con Resource ORM
|
|
109
|
+
begin
|
|
110
|
+
RemoteNode.find('node-123')
|
|
111
|
+
rescue BugBunny::RemoteError => e
|
|
112
|
+
Rails.logger.error("Remote #{e.original_class}: #{e.original_message}")
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Compatibilidad:** Si la respuesta 500 no contiene `bug_bunny_exception` (ej: servicio remoto con versión anterior de BugBunny), se lanza `BugBunny::InternalServerError` en su lugar.
|
|
117
|
+
|
|
118
|
+
**Herencia:** `RemoteError < ServerError < Error < StandardError`. Se puede capturar con `rescue BugBunny::ServerError` para atrapar tanto `RemoteError` como `InternalServerError`.
|
|
119
|
+
|
|
86
120
|
## Formato de Mensajes de Error
|
|
87
121
|
|
|
88
122
|
El middleware `RaiseError` construye el mensaje así:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe BugBunny::RemoteError do
|
|
6
|
+
subject(:error) do
|
|
7
|
+
described_class.new('TypeError', 'nil can\'t be coerced into Integer', [
|
|
8
|
+
"/app/controllers/services_controller.rb:5:in 'Integer#*'",
|
|
9
|
+
"/app/controllers/services_controller.rb:5:in 'ServicesController#index'"
|
|
10
|
+
])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#to_s' do
|
|
14
|
+
it 'does not cause infinite recursion (message -> to_s -> message)' do
|
|
15
|
+
expect(error.to_s).to eq("BugBunny::RemoteError(TypeError): nil can't be coerced into Integer")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '#message' do
|
|
20
|
+
it 'returns the formatted string without stack overflow' do
|
|
21
|
+
expect(error.message).to eq("BugBunny::RemoteError(TypeError): nil can't be coerced into Integer")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '#inspect' do
|
|
26
|
+
it 'is renderable by IRB without raising' do
|
|
27
|
+
expect { error.inspect }.not_to raise_error
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '.serialize' do
|
|
32
|
+
it 'serializes an exception with class, message and backtrace' do
|
|
33
|
+
exception = TypeError.new('test error')
|
|
34
|
+
exception.set_backtrace(%w[line1 line2])
|
|
35
|
+
|
|
36
|
+
result = described_class.serialize(exception)
|
|
37
|
+
|
|
38
|
+
expect(result).to eq(class: 'TypeError', message: 'test error', backtrace: %w[line1 line2])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'truncates backtrace to max_lines' do
|
|
42
|
+
exception = TypeError.new('test')
|
|
43
|
+
exception.set_backtrace(Array.new(30) { |i| "line#{i}" })
|
|
44
|
+
|
|
45
|
+
result = described_class.serialize(exception, max_lines: 5)
|
|
46
|
+
|
|
47
|
+
expect(result[:backtrace].size).to eq(5)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe 'attributes' do
|
|
52
|
+
it 'exposes the original exception class' do
|
|
53
|
+
expect(error.original_class).to eq('TypeError')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'exposes the original message' do
|
|
57
|
+
expect(error.original_message).to eq("nil can't be coerced into Integer")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'exposes the original backtrace' do
|
|
61
|
+
expect(error.original_backtrace.size).to eq(2)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'sets the Ruby backtrace to the remote backtrace' do
|
|
65
|
+
expect(error.backtrace).to eq(error.original_backtrace)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.10.
|
|
4
|
+
version: 4.10.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
@@ -288,6 +288,7 @@ files:
|
|
|
288
288
|
- spec/unit/observability_spec.rb
|
|
289
289
|
- spec/unit/otel_spec.rb
|
|
290
290
|
- spec/unit/producer_spec.rb
|
|
291
|
+
- spec/unit/remote_error_spec.rb
|
|
291
292
|
- spec/unit/request_spec.rb
|
|
292
293
|
- spec/unit/resource_attributes_spec.rb
|
|
293
294
|
- spec/unit/route_spec.rb
|
|
@@ -302,7 +303,7 @@ metadata:
|
|
|
302
303
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
303
304
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
304
305
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
305
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.
|
|
306
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.3/skill
|
|
306
307
|
post_install_message:
|
|
307
308
|
rdoc_options: []
|
|
308
309
|
require_paths:
|