bug_bunny 4.10.0 → 4.10.1
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/lib/bug_bunny/consumer.rb +6 -4
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +4 -0
- data/skill/references/consumer.md +8 -7
- data/spec/unit/route_spec.rb +180 -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: 1e1a5c8c069bdf7634c57b1df16aa226d5abc8fd6e864596d273431a680a1b55
|
|
4
|
+
data.tar.gz: 49b7cf39e53fe8c3f3b90331cfc4f171c8769264356ba1c1d4a767bf27b868ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '082acc0c03183a7d077381563ab9da50657e7468006334b5cfc2d00c8442d603285d38d0f557d67793c833fd6c4d5133c82b3cbbe5048f7f89721c19082971b9'
|
|
7
|
+
data.tar.gz: 9ee3d5a73a1e339d13241093bedfc9dfe212e2a77fb655f3fe21f1378b02d5e10c86417e12fe637eaa1f9591be3ae1e7c2a72d6ed3dac5ee3007f05d1ee7159f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.10.1] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Correcciones
|
|
6
|
+
- Corregir route matching 404: el path del cliente ahora se normaliza antes de pasarlo a `RouteSet#recognize`. Antes, `URI.parse("http://dummy/#{path}")` prependead un `/` extra al path, causando que rutas existentes no hicieran match. Ahora se usa `path.gsub(%r{^/|/$}, '')` antes del recognize. — @Gabriel
|
|
7
|
+
|
|
3
8
|
## [4.9.1] - 2026-04-06
|
|
4
9
|
|
|
5
10
|
### Correcciones
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -181,18 +181,20 @@ module BugBunny
|
|
|
181
181
|
# ===================================================================
|
|
182
182
|
# 3. Ruteo Declarativo
|
|
183
183
|
# ===================================================================
|
|
184
|
-
|
|
184
|
+
normalized_path = path.gsub(%r{^/|/$}, '')
|
|
185
|
+
|
|
186
|
+
uri = URI.parse("http://dummy/#{normalized_path}")
|
|
185
187
|
|
|
186
188
|
# Extraemos query params (ej. /nodes?status=active)
|
|
187
189
|
query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
|
|
188
190
|
query_params = query_params.with_indifferent_access if defined?(ActiveSupport::HashWithIndifferentAccess)
|
|
189
191
|
|
|
190
192
|
# Le preguntamos al motor de rutas global quién debe manejar esto
|
|
191
|
-
route_info = BugBunny.routes.recognize(http_method,
|
|
193
|
+
route_info = BugBunny.routes.recognize(http_method, normalized_path)
|
|
192
194
|
|
|
193
195
|
if route_info.nil?
|
|
194
|
-
safe_log(:warn, 'consumer.route_not_found', method: http_method, path:
|
|
195
|
-
handle_fatal_error(properties, 404, 'Not Found', "No route matches [#{http_method}] \"
|
|
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}\"")
|
|
196
198
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
197
199
|
return
|
|
198
200
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -230,6 +230,10 @@ No registrar consumer middlewares durante la ejecución de `call()`. El stack to
|
|
|
230
230
|
**Causa:** El mensaje intenta ejecutar un controlador que no hereda de `BugBunny::Controller`.
|
|
231
231
|
**Resolución:** Verificar la jerarquía de controladores y que `config.controller_namespace` coincida.
|
|
232
232
|
|
|
233
|
+
### BugBunny::RouteNotFoundError (404)
|
|
234
|
+
**Causa:** El path del mensaje no coincide con ninguna ruta registrada. El path debe estar normalizado (sin slashes iniciales/trailing).
|
|
235
|
+
**Resolución:** Verificar que el cliente envíe el path sin leading/trailing slashes (ej: `users/42`, no `/users/42/`).
|
|
236
|
+
|
|
233
237
|
### BugBunny::UnprocessableEntity (422)
|
|
234
238
|
**Causa:** Fallo de validación en el servicio remoto.
|
|
235
239
|
**Resolución:** `resource.save` devuelve `false`. Acceder a `resource.errors` o `rescue` con `e.error_messages`.
|
|
@@ -21,13 +21,14 @@ consumer = BugBunny::Consumer.subscribe(
|
|
|
21
21
|
2. Extrae campos **OTel messaging** del mensaje para logs estructurados (sin mutar headers).
|
|
22
22
|
3. Valida que el mensaje tenga header `type` (path).
|
|
23
23
|
4. Parsea el método HTTP de headers (`x-http-method` o `method`).
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
28
|
-
9.
|
|
29
|
-
10.
|
|
30
|
-
11.
|
|
24
|
+
5. **Normaliza el path**: remueve slashes iniciales/trailing (`path.gsub(%r{^/|/$}, '')`).
|
|
25
|
+
6. Emite log `consumer.message_received` con campos OTel (`messaging_operation: 'process'`).
|
|
26
|
+
7. Reconoce la ruta con `BugBunny.routes.recognize(method, normalized_path)`.
|
|
27
|
+
8. Resuelve el controlador validando herencia de `BugBunny::Controller`.
|
|
28
|
+
9. Ejecuta consumer middlewares → controller callbacks → acción.
|
|
29
|
+
10. Responde via `reply_to` si está presente (RPC), inyectando campos OTel (`messaging_operation: 'publish'`).
|
|
30
|
+
11. Emite log `consumer.message_processed` con campos OTel y duraciones.
|
|
31
|
+
12. Hace `ack` del mensaje. En caso de error, `reject`.
|
|
31
32
|
|
|
32
33
|
## Observability: OTel Fields
|
|
33
34
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe BugBunny::Routing::Route do
|
|
6
|
+
describe '#match? and #extract_params — path normalization' do
|
|
7
|
+
let(:route) { described_class.new('GET', 'services/otaigccd59q0k7kxb1h193go2/restart', to: 'services#restart') }
|
|
8
|
+
|
|
9
|
+
it 'matches path without leading slash' do
|
|
10
|
+
expect(route.match?('GET', 'services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'matches path with leading slash' do
|
|
14
|
+
expect(route.match?('GET', '/services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'matches path with trailing slash' do
|
|
18
|
+
expect(route.match?('GET', 'services/otaigccd59q0k7kxb1h193go2/restart/')).to be true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'matches path with leading and trailing slash' do
|
|
22
|
+
expect(route.match?('GET', '/services/otaigccd59q0k7kxb1h193go2/restart/')).to be true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'extracts params from path without slash' do
|
|
26
|
+
params = route.extract_params('services/otaigccd59q0k7kxb1h193go2/restart')
|
|
27
|
+
expect(params).to eq({})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'extracts params from path with slash' do
|
|
31
|
+
params = route.extract_params('/services/otaigccd59q0k7kxb1h193go2/restart')
|
|
32
|
+
expect(params).to eq({})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context 'route with dynamic segment' do
|
|
36
|
+
let(:route_with_id) { described_class.new('GET', 'nodes/:id', to: 'nodes#show') }
|
|
37
|
+
|
|
38
|
+
it 'extracts params from path without slash' do
|
|
39
|
+
params = route_with_id.extract_params('nodes/123')
|
|
40
|
+
expect(params).to eq({ 'id' => '123' })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'extracts params from path with slash' do
|
|
44
|
+
params = route_with_id.extract_params('/nodes/123')
|
|
45
|
+
expect(params).to eq({ 'id' => '123' })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'route defined with leading slash' do
|
|
50
|
+
let(:route_with_leading_slash) do
|
|
51
|
+
described_class.new('GET', '/services/otaigccd59q0k7kxb1h193go2/restart', to: 'services#restart')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'matches path without slash' do
|
|
55
|
+
expect(route_with_leading_slash.match?('GET', 'services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'matches path with slash' do
|
|
59
|
+
expect(route_with_leading_slash.match?('GET', '/services/otaigccd59q0k7kxb1h193go2/restart')).to be true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe 'RouteSet#recognize path normalization' do
|
|
65
|
+
let(:route_set) { BugBunny::Routing::RouteSet.new }
|
|
66
|
+
|
|
67
|
+
before do
|
|
68
|
+
route_set.draw do
|
|
69
|
+
get 'services/otaigccd59q0k7kxb1h193go2/restart', to: 'services#restart'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'recognizes path without slash' do
|
|
74
|
+
result = route_set.recognize('GET', 'services/otaigccd59q0k7kxb1h193go2/restart')
|
|
75
|
+
expect(result).not_to be_nil
|
|
76
|
+
expect(result[:controller]).to eq('services')
|
|
77
|
+
expect(result[:action]).to eq('restart')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'recognizes path with leading slash' do
|
|
81
|
+
result = route_set.recognize('GET', '/services/otaigccd59q0k7kxb1h193go2/restart')
|
|
82
|
+
expect(result).not_to be_nil
|
|
83
|
+
expect(result[:controller]).to eq('services')
|
|
84
|
+
expect(result[:action]).to eq('restart')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns nil for non-matching path' do
|
|
88
|
+
result = route_set.recognize('GET', 'services/nonexistent')
|
|
89
|
+
expect(result).to be_nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe 'Consumer path handling — URI parsing edge case' do
|
|
94
|
+
let(:channel) { BunnyMocks::FakeChannel.new(true) }
|
|
95
|
+
let(:connection) { BunnyMocks::FakeConnection.new(true, channel) }
|
|
96
|
+
|
|
97
|
+
let(:mock_channel) do
|
|
98
|
+
ch = double('channel')
|
|
99
|
+
allow(ch).to receive(:reject)
|
|
100
|
+
allow(ch).to receive(:ack)
|
|
101
|
+
allow(ch).to receive(:open?).and_return(true)
|
|
102
|
+
default_ex = double('default_exchange')
|
|
103
|
+
allow(default_ex).to receive(:publish)
|
|
104
|
+
allow(ch).to receive(:default_exchange).and_return(default_ex)
|
|
105
|
+
ch
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
let(:mock_session) do
|
|
109
|
+
s = instance_double(BugBunny::Session)
|
|
110
|
+
allow(s).to receive(:exchange).and_return(double('exchange'))
|
|
111
|
+
allow(s).to receive(:queue).and_return(double('queue'))
|
|
112
|
+
allow(s).to receive(:close)
|
|
113
|
+
allow(s).to receive(:channel).and_return(mock_channel)
|
|
114
|
+
s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
let(:test_consumer) do
|
|
118
|
+
c = BugBunny::Consumer.new(connection)
|
|
119
|
+
c.instance_variable_set(:@session, mock_session)
|
|
120
|
+
c
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
let(:delivery_info) do
|
|
124
|
+
double('delivery_info',
|
|
125
|
+
exchange: 'events_x',
|
|
126
|
+
routing_key: 'users.created',
|
|
127
|
+
delivery_tag: 'delivery-tag-1',
|
|
128
|
+
redelivered?: false)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
let(:properties) do
|
|
132
|
+
double('properties',
|
|
133
|
+
type: 'services/otaigccd59q0k7kxb1h193go2/restart',
|
|
134
|
+
headers: { 'x-http-method' => 'GET' },
|
|
135
|
+
correlation_id: 'corr-abc-123',
|
|
136
|
+
reply_to: nil,
|
|
137
|
+
content_type: 'application/json')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
let(:logged_events) { [] }
|
|
141
|
+
|
|
142
|
+
before do
|
|
143
|
+
allow(test_consumer).to receive(:safe_log) do |level, event, **kwargs|
|
|
144
|
+
logged_events << { level: level, event: event, kwargs: kwargs }
|
|
145
|
+
end
|
|
146
|
+
allow(test_consumer).to receive(:handle_fatal_error)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'logs route_not_found with normalized path (without extra slash)' do
|
|
150
|
+
allow(BugBunny.routes).to receive(:recognize).and_return(nil)
|
|
151
|
+
|
|
152
|
+
test_consumer.send(:process_message, delivery_info, properties, '{}')
|
|
153
|
+
|
|
154
|
+
route_not_found_event = logged_events.find { |e| e[:event] == 'consumer.route_not_found' }
|
|
155
|
+
expect(route_not_found_event).not_to be_nil
|
|
156
|
+
expect(route_not_found_event[:kwargs][:path]).to eq('services/otaigccd59q0k7kxb1h193go2/restart')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'when properties.type has leading slash' do
|
|
160
|
+
let(:properties) do
|
|
161
|
+
double('properties',
|
|
162
|
+
type: '/services/otaigccd59q0k7kxb1h193go2/restart',
|
|
163
|
+
headers: { 'x-http-method' => 'GET' },
|
|
164
|
+
correlation_id: 'corr-abc-123',
|
|
165
|
+
reply_to: nil,
|
|
166
|
+
content_type: 'application/json')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'logs route_not_found with normalized path (stripped leading slash)' do
|
|
170
|
+
allow(BugBunny.routes).to receive(:recognize).and_return(nil)
|
|
171
|
+
|
|
172
|
+
test_consumer.send(:process_message, delivery_info, properties, '{}')
|
|
173
|
+
|
|
174
|
+
route_not_found_event = logged_events.find { |e| e[:event] == 'consumer.route_not_found' }
|
|
175
|
+
expect(route_not_found_event).not_to be_nil
|
|
176
|
+
expect(route_not_found_event[:kwargs][:path]).to eq('services/otaigccd59q0k7kxb1h193go2/restart')
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
@@ -290,6 +290,7 @@ files:
|
|
|
290
290
|
- spec/unit/producer_spec.rb
|
|
291
291
|
- spec/unit/request_spec.rb
|
|
292
292
|
- spec/unit/resource_attributes_spec.rb
|
|
293
|
+
- spec/unit/route_spec.rb
|
|
293
294
|
- spec/unit/session_spec.rb
|
|
294
295
|
- test/integration/infrastructure_test.rb
|
|
295
296
|
- test/integration/manual_client_test.rb
|
|
@@ -301,7 +302,7 @@ metadata:
|
|
|
301
302
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
302
303
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
303
304
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
304
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.
|
|
305
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.10.1/skill
|
|
305
306
|
post_install_message:
|
|
306
307
|
rdoc_options: []
|
|
307
308
|
require_paths:
|