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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e1a5c8c069bdf7634c57b1df16aa226d5abc8fd6e864596d273431a680a1b55
4
- data.tar.gz: 49b7cf39e53fe8c3f3b90331cfc4f171c8769264356ba1c1d4a767bf27b868ee
3
+ metadata.gz: 31a6f6e9b83b1a82216f344a101ca0a99daebb5fd9e44bf5abc9c9e9753f4011
4
+ data.tar.gz: f101254aad9d8bbd66a359438a1efe970acd58e4b853841bcda72966c2472c8c
5
5
  SHA512:
6
- metadata.gz: '082acc0c03183a7d077381563ab9da50657e7468006334b5cfc2d00c8442d603285d38d0f557d67793c833fd6c4d5133c82b3cbbe5048f7f89721c19082971b9'
7
- data.tar.gz: 9ee3d5a73a1e339d13241093bedfc9dfe212e2a77fb655f3fe21f1378b02d5e10c86417e12fe637eaa1f9591be3ae1e7c2a72d6ed3dac5ee3007f05d1ee7159f
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
@@ -49,7 +49,7 @@ module BugBunny
49
49
 
50
50
  # @return [String] Representación legible de la excepción.
51
51
  def to_s
52
- "#{self.class.name}(#{original_class}): #{message}"
52
+ "#{self.class.name}(#{original_class}): #{super}"
53
53
  end
54
54
  end
55
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.10.1'
4
+ VERSION = '4.10.3'
5
5
  end
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(&:start)
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.
@@ -16,7 +16,8 @@ StandardError
16
16
  │ ├── BugBunny::Conflict (409)
17
17
  │ └── BugBunny::UnprocessableEntity (422)
18
18
  └── BugBunny::ServerError (5xx)
19
- └── BugBunny::InternalServerError (500+)
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.1
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.1/skill
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: