bug_bunny 4.10.3 → 4.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31a6f6e9b83b1a82216f344a101ca0a99daebb5fd9e44bf5abc9c9e9753f4011
4
- data.tar.gz: f101254aad9d8bbd66a359438a1efe970acd58e4b853841bcda72966c2472c8c
3
+ metadata.gz: efe99ebd57d867b306ecb7bc2b67d92b72bb1b0b3ddaabaf1c72755ed337ded9
4
+ data.tar.gz: 997cb254c582a4524275c295ad12694762f1b7b010e690cbb70dfdd7c6684039
5
5
  SHA512:
6
- metadata.gz: 6b8a42f884ae9c8075dab9e4052658a5c0828cae904800fe222fd058cdaf3cd1307bba6478ca7824e273a517484d71da0416fd3b7c38bb08a9a0845e39dacb90
7
- data.tar.gz: 6de916fae62566f01142b557b6e6c4ed4cf44d58298d9ff3c174e8d38b4422238487dacd29f273d7b3ea356d30f8c08a5c5a872f9ed08f6cacb8a540511a933a
6
+ metadata.gz: 7bb1d730e90c2d58c39087e65ea67415a258cd0f4dc4e0064610ee67d7167480726b3238155687f8d77f8390d2c41a707482df0d56da8022be9b2d89269a9f97
7
+ data.tar.gz: 4b46a0f617856554700b9c31e2917256ee5aa723f5699c0097cd8a1e82b28e8655e98ddf61ddb884e85794979ad9fcdf2b6c7bd2a8bcb7bd150bdeecdd71dfa8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.11.0] - 2026-04-08
4
+
5
+ ### Correcciones
6
+ - **Query string en route matching:** El consumer incluía el query string como parte del path al hacer route matching (ej. `secrets?q%5Bname%5D=postgres_password`), causando 404 en rutas válidas con query params. Ahora se separa el path limpio via `URI.parse` antes de invocar `RouteSet#recognize`. — @Gabriel
7
+
8
+ ### ✨ New Features
9
+ - **`BugBunny::RoutingError`:** Nueva excepción `RoutingError < NotFound` análoga a `ActionController::RoutingError` en Rails. Permite al productor distinguir "la ruta no existe en el servicio remoto" de "el recurso no fue encontrado". El consumer ahora envía `error_type: 'routing_error'` en el body del 404 cuando la ruta o el controller no existen, y el middleware `RaiseError` levanta `RoutingError` con el detalle del error. `rescue BugBunny::NotFound` sigue capturando ambos casos. — @Gabriel
10
+
3
11
  ## [4.10.2] - 2026-04-08
4
12
 
5
13
  ### Correcciones
@@ -184,17 +184,18 @@ module BugBunny
184
184
  normalized_path = path.gsub(%r{^/|/$}, '')
185
185
 
186
186
  uri = URI.parse("http://dummy/#{normalized_path}")
187
+ clean_path = uri.path.delete_prefix('/dummy/').delete_prefix('/')
187
188
 
188
189
  # Extraemos query params (ej. /nodes?status=active)
189
190
  query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
190
191
  query_params = query_params.with_indifferent_access if defined?(ActiveSupport::HashWithIndifferentAccess)
191
192
 
192
193
  # Le preguntamos al motor de rutas global quién debe manejar esto
193
- route_info = BugBunny.routes.recognize(http_method, normalized_path)
194
+ route_info = BugBunny.routes.recognize(http_method, clean_path)
194
195
 
195
196
  if route_info.nil?
196
- safe_log(:warn, 'consumer.route_not_found', method: http_method, path: normalized_path)
197
- handle_fatal_error(properties, 404, 'Not Found', "No route matches [#{http_method}] \"#{normalized_path}\"")
197
+ safe_log(:warn, 'consumer.route_not_found', method: http_method, path: clean_path)
198
+ handle_routing_error(properties, "No route matches [#{http_method}] \"#{clean_path}\"")
198
199
  session.channel.reject(delivery_info.delivery_tag, false)
199
200
  return
200
201
  end
@@ -213,7 +214,7 @@ module BugBunny
213
214
  controller_class = controller_class_name.constantize
214
215
  rescue NameError
215
216
  safe_log(:warn, 'consumer.controller_not_found', controller: controller_class_name)
216
- handle_fatal_error(properties, 404, 'Not Found', "Controller #{controller_class_name} not found")
217
+ handle_routing_error(properties, "Controller #{controller_class_name} not found")
217
218
  session.channel.reject(delivery_info.delivery_tag, false)
218
219
  return
219
220
  end
@@ -300,6 +301,22 @@ module BugBunny
300
301
  # @param detail [String] Detalle del error.
301
302
  # @param exception [StandardError, nil] Excepción original (para status 500).
302
303
  # @api private
304
+ # Maneja errores de enrutamiento (ruta o controller no encontrado).
305
+ #
306
+ # Envía una respuesta 404 con `error_type: 'routing_error'` para que el
307
+ # middleware del productor pueda levantar {BugBunny::RoutingError} en vez
308
+ # de un {BugBunny::NotFound} genérico.
309
+ #
310
+ # @param properties [Bunny::MessageProperties] Headers y propiedades AMQP.
311
+ # @param detail [String] Descripción del error de routing.
312
+ # @api private
313
+ def handle_routing_error(properties, detail)
314
+ return unless properties.reply_to
315
+
316
+ body = { error: 'Not Found', detail: detail, error_type: 'routing_error' }
317
+ reply({ status: 404, body: body }, properties.reply_to, properties.correlation_id)
318
+ end
319
+
303
320
  def handle_fatal_error(properties, status, error_title, detail, exception = nil)
304
321
  return unless properties.reply_to
305
322
 
@@ -35,6 +35,15 @@ module BugBunny
35
35
  # El recurso solicitado (o la ruta RPC) no existe en el servidor remoto.
36
36
  class NotFound < ClientError; end
37
37
 
38
+ # Error 404 específico de enrutamiento.
39
+ # Se lanza cuando el servicio remoto no tiene una ruta registrada para el verbo y path solicitados.
40
+ # Análogo a `ActionController::RoutingError` en Rails.
41
+ #
42
+ # @example
43
+ # rescue BugBunny::RoutingError => e
44
+ # e.message # => 'No route matches [GET] "secrets"'
45
+ class RoutingError < NotFound; end
46
+
38
47
  # Error 406: Not Acceptable.
39
48
  # El servidor no puede generar una respuesta con las características de contenido aceptadas.
40
49
  class NotAcceptable < ClientError; end
@@ -39,7 +39,7 @@ module BugBunny
39
39
  when 400
40
40
  raise BugBunny::BadRequest, format_error_message(body)
41
41
  when 404
42
- raise BugBunny::NotFound
42
+ raise_not_found(body)
43
43
  when 406
44
44
  raise BugBunny::NotAcceptable
45
45
  when 408
@@ -87,6 +87,20 @@ module BugBunny
87
87
  end
88
88
  end
89
89
 
90
+ # Distingue un error de routing (ruta/controller no existe en el servicio remoto)
91
+ # de un 404 genérico de recurso no encontrado.
92
+ #
93
+ # @param body [Hash, String, nil] El cuerpo de la respuesta 404.
94
+ # @raise [BugBunny::RoutingError] Si el consumer marcó `error_type: 'routing_error'`.
95
+ # @raise [BugBunny::NotFound] Para 404 genéricos.
96
+ def raise_not_found(body)
97
+ if body.is_a?(Hash) && body['error_type'] == 'routing_error'
98
+ raise BugBunny::RoutingError, format_error_message(body)
99
+ end
100
+
101
+ raise BugBunny::NotFound, format_error_message(body)
102
+ end
103
+
90
104
  # Maneja códigos de error genéricos no mapeados explícitamente en el `case`.
91
105
  #
92
106
  # @param status [Integer] El código de estado HTTP (ej. 418, 502).
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.10.3'
4
+ VERSION = '4.11.0'
5
5
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe BugBunny::Middleware::RaiseError do
6
+ subject(:middleware) { described_class.new(->(_env) { {} }) }
7
+
8
+ describe '#on_complete' do
9
+ context 'when status is 404 with error_type routing_error' do
10
+ let(:response) do
11
+ {
12
+ 'status' => 404,
13
+ 'body' => { 'error' => 'Not Found', 'detail' => 'No route matches [GET] "secrets"',
14
+ 'error_type' => 'routing_error' }
15
+ }
16
+ end
17
+
18
+ it 'raises BugBunny::RoutingError' do
19
+ expect { middleware.on_complete(response) }.to raise_error(BugBunny::RoutingError)
20
+ end
21
+
22
+ it 'includes the detail in the error message' do
23
+ expect { middleware.on_complete(response) }.to raise_error(
24
+ BugBunny::RoutingError, /No route matches \[GET\] "secrets"/
25
+ )
26
+ end
27
+
28
+ it 'is rescuable as BugBunny::NotFound' do
29
+ expect { middleware.on_complete(response) }.to raise_error(BugBunny::NotFound)
30
+ end
31
+ end
32
+
33
+ context 'when status is 404 without error_type (resource not found)' do
34
+ let(:response) do
35
+ { 'status' => 404, 'body' => { 'error' => 'Not Found', 'detail' => 'User 999 not found' } }
36
+ end
37
+
38
+ it 'raises BugBunny::NotFound but not RoutingError' do
39
+ error = nil
40
+ begin
41
+ middleware.on_complete(response)
42
+ rescue BugBunny::NotFound => e
43
+ error = e
44
+ end
45
+
46
+ expect(error).to be_a(BugBunny::NotFound)
47
+ expect(error).not_to be_a(BugBunny::RoutingError)
48
+ end
49
+ end
50
+
51
+ context 'when status is 404 with nil body' do
52
+ let(:response) { { 'status' => 404, 'body' => nil } }
53
+
54
+ it 'raises BugBunny::NotFound' do
55
+ expect { middleware.on_complete(response) }.to raise_error(BugBunny::NotFound)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -176,5 +176,34 @@ RSpec.describe BugBunny::Routing::Route do
176
176
  expect(route_not_found_event[:kwargs][:path]).to eq('services/otaigccd59q0k7kxb1h193go2/restart')
177
177
  end
178
178
  end
179
+
180
+ context 'when properties.type includes query string' do
181
+ let(:properties) do
182
+ double('properties',
183
+ type: 'secrets?q%5Bname%5D=postgres_password',
184
+ headers: { 'x-http-method' => 'GET' },
185
+ correlation_id: 'corr-abc-123',
186
+ reply_to: nil,
187
+ content_type: 'application/json')
188
+ end
189
+
190
+ it 'strips query string before route recognition' do
191
+ allow(BugBunny.routes).to receive(:recognize).and_return(nil)
192
+
193
+ test_consumer.send(:process_message, delivery_info, properties, '')
194
+
195
+ expect(BugBunny.routes).to have_received(:recognize).with('GET', 'secrets')
196
+ end
197
+
198
+ it 'logs route_not_found with clean path (without query string)' do
199
+ allow(BugBunny.routes).to receive(:recognize).and_return(nil)
200
+
201
+ test_consumer.send(:process_message, delivery_info, properties, '')
202
+
203
+ route_not_found_event = logged_events.find { |e| e[:event] == 'consumer.route_not_found' }
204
+ expect(route_not_found_event).not_to be_nil
205
+ expect(route_not_found_event[:kwargs][:path]).to eq('secrets')
206
+ end
207
+ end
179
208
  end
180
209
  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.3
4
+ version: 4.11.0
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/raise_error_spec.rb
291
292
  - spec/unit/remote_error_spec.rb
292
293
  - spec/unit/request_spec.rb
293
294
  - spec/unit/resource_attributes_spec.rb
@@ -303,7 +304,7 @@ metadata:
303
304
  homepage_uri: https://github.com/gedera/bug_bunny
304
305
  source_code_uri: https://github.com/gedera/bug_bunny
305
306
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
306
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.3/skill
307
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.11.0/skill
307
308
  post_install_message:
308
309
  rdoc_options: []
309
310
  require_paths: