bug_bunny 4.7.0 → 4.8.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/documentation-writer/SKILL.md +45 -0
  3. data/.agents/skills/gem-release/SKILL.md +114 -0
  4. data/.agents/skills/quality-code/SKILL.md +51 -0
  5. data/.agents/skills/rabbitmq-expert/SKILL.md +1555 -0
  6. data/.agents/skills/sentry/SKILL.md +135 -0
  7. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  8. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  9. data/.agents/skills/skill-builder/SKILL.md +232 -0
  10. data/.agents/skills/skill-manager/SKILL.md +172 -0
  11. data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
  12. data/.agents/skills/yard/SKILL.md +311 -0
  13. data/.agents/skills/yard/references/tipos.md +144 -0
  14. data/CHANGELOG.md +21 -0
  15. data/CLAUDE.md +29 -220
  16. data/lib/bug_bunny/client.rb +4 -3
  17. data/lib/bug_bunny/controller.rb +10 -14
  18. data/lib/bug_bunny/exception.rb +1 -1
  19. data/lib/bug_bunny/middleware/raise_error.rb +3 -3
  20. data/lib/bug_bunny/observability.rb +5 -5
  21. data/lib/bug_bunny/producer.rb +11 -13
  22. data/lib/bug_bunny/railtie.rb +8 -7
  23. data/lib/bug_bunny/request.rb +3 -11
  24. data/lib/bug_bunny/resource.rb +51 -21
  25. data/lib/bug_bunny/routing/route_set.rb +32 -21
  26. data/lib/bug_bunny/version.rb +1 -1
  27. data/lib/bug_bunny.rb +3 -2
  28. data/lib/generators/bug_bunny/install/install_generator.rb +45 -5
  29. data/lib/tasks/bug_bunny.rake +50 -0
  30. data/skill/SKILL.md +230 -0
  31. data/skill/references/client-middleware.md +144 -0
  32. data/skill/references/consumer.md +104 -0
  33. data/skill/references/controller.md +105 -0
  34. data/skill/references/errores.md +97 -0
  35. data/skill/references/resource.md +116 -0
  36. data/skill/references/routing.md +82 -0
  37. data/skill/references/testing.md +138 -0
  38. data/skills-lock.json +10 -0
  39. data/skills.lock +24 -0
  40. data/skills.yml +19 -0
  41. metadata +27 -17
  42. data/.claude/commands/pr.md +0 -42
  43. data/.claude/commands/release.md +0 -41
  44. data/.claude/commands/rubocop.md +0 -22
  45. data/.claude/commands/test.md +0 -28
  46. data/.claude/commands/yard.md +0 -46
  47. data/docs/concepts.md +0 -140
  48. data/docs/howto/controller.md +0 -194
  49. data/docs/howto/middleware_client.md +0 -119
  50. data/docs/howto/middleware_consumer.md +0 -127
  51. data/docs/howto/rails.md +0 -214
  52. data/docs/howto/resource.md +0 -200
  53. data/docs/howto/routing.md +0 -133
  54. data/docs/howto/testing.md +0 -259
  55. data/docs/howto/tracing.md +0 -119
  56. data/mejoras.md +0 -33
data/docs/concepts.md DELETED
@@ -1,140 +0,0 @@
1
- # Concepts
2
-
3
- ## What is BugBunny?
4
-
5
- BugBunny is a Ruby gem that implements a RESTful routing layer over AMQP (RabbitMQ). It lets microservices communicate through RabbitMQ using familiar HTTP-like patterns: verbs (GET, POST, PUT, DELETE), controllers, declarative routes, synchronous RPC, and fire-and-forget publishing.
6
-
7
- **Problem it solves:** Direct HTTP coupling between microservices creates a tight dependency graph — if Service B is down, Service A fails immediately. RabbitMQ as a message bus decouples availability, but raw AMQP APIs are low-level and verbose. BugBunny gives you the ergonomics of a web framework on top of the reliability of a message broker.
8
-
9
- ---
10
-
11
- ## AMQP in 5 Minutes
12
-
13
- AMQP (Advanced Message Queuing Protocol) is the protocol RabbitMQ implements. The key concepts:
14
-
15
- **Exchange** — Receives messages from producers and routes them to queues based on rules. Types:
16
- - `direct` — Routes to queues whose binding key exactly matches the routing key.
17
- - `topic` — Routes using wildcard patterns (`orders.*`, `#.error`).
18
- - `fanout` — Broadcasts to all bound queues regardless of routing key.
19
-
20
- **Queue** — Stores messages until a consumer picks them up. Durable queues survive broker restarts.
21
-
22
- **Routing Key** — A string the producer attaches to the message. The exchange uses it to decide which queues receive the message.
23
-
24
- **Binding** — A link between an exchange and a queue, optionally with a routing key pattern.
25
-
26
- In BugBunny, the **path** of the message (e.g., `nodes/123`) travels inside the AMQP `type` header. The routing key determines *which service* receives the message; the path determines *which controller and action* handles it inside that service.
27
-
28
- ---
29
-
30
- ## Architecture
31
-
32
- ```
33
- Service A (Producer)
34
- BugBunny::Resource.find(id)
35
- BugBunny::Client#request(path)
36
- |
37
- v
38
- BugBunny::Producer
39
- | publishes to exchange with:
40
- | routing_key: 'nodes'
41
- | type header: 'nodes/123'
42
- | reply_to: 'amq.rabbitmq.reply-to' (RPC only)
43
- | correlation_id: 'abc-123' (RPC only)
44
- v
45
- [ RabbitMQ Exchange ]
46
- |
47
- v
48
- [ RabbitMQ Queue ]
49
- |
50
- v
51
- Service B (Consumer)
52
- BugBunny::Consumer (subscribe loop)
53
- |
54
- v
55
- ConsumerMiddleware::Stack
56
- (tracing, auth, etc.)
57
- |
58
- v
59
- Router (BugBunny.routes)
60
- matches 'GET nodes/123' → NodesController#show
61
- |
62
- v
63
- NodesController#show
64
- render status: :ok, json: node
65
- |
66
- v (RPC only)
67
- reply → amq.rabbitmq.reply-to
68
- |
69
- v
70
- Service A (unblocked)
71
- future.value → { body: {...}, headers: {...} }
72
- ```
73
-
74
- ---
75
-
76
- ## RPC vs Fire-and-Forget
77
-
78
- BugBunny supports two communication patterns. Choosing the right one matters for system design.
79
-
80
- ### Synchronous RPC (`:rpc`)
81
-
82
- The producer blocks until the consumer replies. Uses RabbitMQ's `amq.rabbitmq.reply-to` pseudo-queue — no temporary queues are created.
83
-
84
- ```ruby
85
- response = client.request('users/42', method: :get)
86
- # blocks here until the consumer sends back a reply (or timeout)
87
- ```
88
-
89
- **Use when:** Service A needs the result to continue. Example: fetching user data before building a response, validating inventory before placing an order.
90
-
91
- **Timeout:** Configurable via `config.rpc_timeout`. Raises `BugBunny::RequestTimeout` if the consumer does not reply in time.
92
-
93
- **Cost:** Ties up a thread in Service A for the duration of the call.
94
-
95
- ### Fire-and-Forget (`:publish`)
96
-
97
- The producer publishes and continues immediately. No reply is expected or waited for.
98
-
99
- ```ruby
100
- client.publish('events', body: { type: 'order.placed', order_id: 99 })
101
- # returns immediately with { 'status' => 202 }
102
- ```
103
-
104
- **Use when:** Service A does not need a result. Example: emitting audit events, triggering background jobs, sending notifications.
105
-
106
- **Cost:** None — but you have no confirmation that the message was processed successfully.
107
-
108
- ---
109
-
110
- ## Key Components
111
-
112
- | Class | Role |
113
- |---|---|
114
- | `BugBunny::Configuration` | Global settings. Validates required fields on `BugBunny.configure`. |
115
- | `BugBunny::Session` | Wraps a Bunny channel. Declares exchanges and queues. Thread-safe with double-checked locking. |
116
- | `BugBunny::Producer` | Publishes messages. Implements RPC with `Concurrent::IVar`. |
117
- | `BugBunny::Client` | High-level publisher API. Manages a connection pool and middleware stack. |
118
- | `BugBunny::Consumer` | Subscribe loop. Routes messages to controllers via `BugBunny.routes`. |
119
- | `BugBunny::ConsumerMiddleware::Stack` | Pipeline of middlewares executed before `process_message`. |
120
- | `BugBunny::Controller` | Base class for message handlers. Supports `before_action`, `after_action`, `around_action`, `rescue_from`. |
121
- | `BugBunny::Resource` | ActiveRecord-like ORM over AMQP. Provides `find`, `where`, `create`, `save`, `destroy`. |
122
- | `BugBunny::Routing::RouteSet` | Stores and matches declared routes. |
123
- | `BugBunny::Observability` | Mixin for structured logging. `safe_log` never raises. |
124
-
125
- ---
126
-
127
- ## Connection Pool
128
-
129
- BugBunny uses the `connection_pool` gem to share connections safely across threads (Puma workers, Sidekiq threads).
130
-
131
- ```
132
- ConnectionPool
133
- slot 0: Bunny::Session → BugBunny::Session → BugBunny::Producer
134
- slot 1: Bunny::Session → BugBunny::Session → BugBunny::Producer
135
- slot N: ...
136
- ```
137
-
138
- Each pool slot caches its `Session` and `Producer` for the lifetime of the slot. This avoids re-creating AMQP channels (expensive) and prevents the double `basic_consume` error that would occur if a new Producer were created on a reused channel.
139
-
140
- Thread safety is guaranteed by `ConnectionPool` itself: each slot is used by one thread at a time, so no additional mutex is needed at the Session or Producer level.
@@ -1,194 +0,0 @@
1
- # Controllers
2
-
3
- Controllers receive routed messages and produce responses. They follow the same lifecycle as ActionController in Rails.
4
-
5
- ## Defining a Controller
6
-
7
- ```ruby
8
- module BugBunny
9
- module Controllers
10
- class NodesController < BugBunny::Controller
11
- def index
12
- nodes = Node.all
13
- render status: :ok, json: nodes.map(&:as_json)
14
- end
15
-
16
- def show
17
- node = Node.find(params[:id])
18
- render status: :ok, json: node.as_json
19
- end
20
-
21
- def create
22
- node = Node.new(node_params)
23
- if node.save
24
- render status: :created, json: node.as_json
25
- else
26
- render status: :unprocessable_entity, json: { errors: node.errors }
27
- end
28
- end
29
- end
30
- end
31
- end
32
- ```
33
-
34
- The controller namespace defaults to `BugBunny::Controllers`. Override it globally with `config.controller_namespace`, or per route group with `namespace` blocks in the DSL.
35
-
36
- ---
37
-
38
- ## params
39
-
40
- `params` merges path parameters, query string parameters, and body parameters into a single `HashWithIndifferentAccess`:
41
-
42
- ```ruby
43
- # Message for PUT nodes/42?verbose=true with body { "node": { "status": "active" } }
44
- params[:id] # => "42" (from path)
45
- params[:verbose] # => "true" (from query string)
46
- params[:node][:status] # => "active" (from JSON body)
47
- ```
48
-
49
- Use strong-parameters style to extract what you need:
50
-
51
- ```ruby
52
- def node_params
53
- params.require(:node).permit(:name, :status)
54
- end
55
- ```
56
-
57
- ---
58
-
59
- ## before_action
60
-
61
- Runs before the action. Rendering inside a `before_action` halts the chain — the action and `after_action` callbacks do not run.
62
-
63
- ```ruby
64
- class ApplicationController < BugBunny::Controller
65
- before_action :authenticate!
66
- before_action :set_node, only: [:show, :update, :destroy, :drain]
67
-
68
- private
69
-
70
- def authenticate!
71
- token = request_headers['X-Service-Token']
72
- render status: :unauthorized, json: { error: 'Unauthorized' } unless valid_token?(token)
73
- end
74
-
75
- def set_node
76
- @node = Node.find(params[:id])
77
- render status: :not_found, json: { error: 'Not found' } unless @node
78
- end
79
- end
80
- ```
81
-
82
- ---
83
-
84
- ## after_action
85
-
86
- Runs after the action completes successfully. Skipped if a `before_action` halted the chain or if the action raised an exception — same behavior as Rails.
87
-
88
- ```ruby
89
- class NodesController < ApplicationController
90
- after_action :emit_audit_event, only: [:create, :update, :destroy]
91
-
92
- private
93
-
94
- def emit_audit_event
95
- AuditLog.record(action: action_name, node_id: params[:id], actor: current_service)
96
- end
97
- end
98
- ```
99
-
100
- ---
101
-
102
- ## around_action
103
-
104
- Wraps the action. Must call `yield` to execute the inner chain.
105
-
106
- ```ruby
107
- class NodesController < ApplicationController
108
- around_action :with_distributed_lock, only: [:drain]
109
-
110
- private
111
-
112
- def with_distributed_lock
113
- DistributedLock.acquire("node:#{params[:id]}") { yield }
114
- end
115
- end
116
- ```
117
-
118
- ---
119
-
120
- ## rescue_from
121
-
122
- Catches exceptions raised during the action and maps them to responses. Handlers are inherited and can be defined in a base `ApplicationController`.
123
-
124
- ```ruby
125
- class ApplicationController < BugBunny::Controller
126
- rescue_from ActiveRecord::RecordNotFound do |e|
127
- render status: :not_found, json: { error: e.message }
128
- end
129
-
130
- rescue_from ActiveRecord::RecordInvalid do |e|
131
- render status: :unprocessable_entity, json: { errors: e.record.errors.full_messages }
132
- end
133
-
134
- rescue_from StandardError do |e|
135
- logger.error("Unhandled error: #{e.class} — #{e.message}")
136
- render status: :internal_server_error, json: { error: 'Internal server error' }
137
- end
138
- end
139
- ```
140
-
141
- ---
142
-
143
- ## render
144
-
145
- ```ruby
146
- render status: :ok, json: resource.as_json
147
- render status: :created, json: { id: record.id }
148
- render status: :no_content, json: nil
149
- render status: :unprocessable_entity, json: { errors: object.errors }
150
- render status: :not_found, json: { error: 'Not found' }
151
- ```
152
-
153
- Accepts any Rack status symbol (`:ok`, `:created`, `:not_found`, etc.) or an integer status code.
154
-
155
- ### Adding response headers
156
-
157
- ```ruby
158
- # Per-response headers (merged, non-destructive)
159
- render status: :ok, json: data, headers: { 'X-Request-Id' => request_id }
160
-
161
- # Headers set for the lifetime of the action (accessible via response_headers)
162
- def show
163
- response_headers['X-Cache'] = 'HIT'
164
- render status: :ok, json: Node.find(params[:id]).as_json
165
- end
166
- ```
167
-
168
- ---
169
-
170
- ## Accessing AMQP headers
171
-
172
- ```ruby
173
- def create
174
- trace_id = request_headers['X-Trace-Id']
175
- # ...
176
- end
177
- ```
178
-
179
- `request_headers` returns the raw AMQP headers hash from `properties.headers`.
180
-
181
- ---
182
-
183
- ## log_tags
184
-
185
- Injects contextual tags into all log lines produced within the action's execution:
186
-
187
- ```ruby
188
- class NodesController < ApplicationController
189
- log_tag { params[:id] }
190
- log_tag { current_tenant }
191
- end
192
- ```
193
-
194
- Works with Rails' `ActiveSupport::TaggedLogging`.
@@ -1,119 +0,0 @@
1
- # Client Middleware
2
-
3
- The client middleware stack wraps outgoing requests in an onion (Faraday-style). Each middleware can inspect and modify the request before it reaches the producer, and inspect the response on the way back.
4
-
5
- ## Built-in Middlewares
6
-
7
- ### `BugBunny::Middleware::RaiseError`
8
-
9
- Maps HTTP-like status codes in the response to Ruby exceptions:
10
-
11
- | Status | Exception |
12
- |--------|-----------|
13
- | 400 | `BugBunny::BadRequest` |
14
- | 401 | `BugBunny::Unauthorized` |
15
- | 403 | `BugBunny::Forbidden` |
16
- | 404 | `BugBunny::NotFound` |
17
- | 409 | `BugBunny::Conflict` |
18
- | 422 | `BugBunny::UnprocessableEntity` |
19
- | 4xx | `BugBunny::ClientError` |
20
- | 500 | `BugBunny::InternalServerError` |
21
- | 5xx | `BugBunny::ServerError` |
22
- | Timeout| `BugBunny::RequestTimeout` |
23
-
24
- ### `BugBunny::Middleware::JsonResponse`
25
-
26
- Parses the response body from JSON and returns a `HashWithIndifferentAccess`. Without this middleware, the response body is a raw String.
27
-
28
- ---
29
-
30
- ## Using Middlewares with Client
31
-
32
- ```ruby
33
- pool = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
34
- client = BugBunny::Client.new(pool: pool) do |stack|
35
- stack.use BugBunny::Middleware::RaiseError
36
- stack.use BugBunny::Middleware::JsonResponse
37
- stack.use MyLoggingMiddleware
38
- end
39
- ```
40
-
41
- Middlewares execute in the order they are registered (FIFO). `RaiseError` and `JsonResponse` should be registered in that order so that `RaiseError` sees the parsed body.
42
-
43
- ---
44
-
45
- ## Writing a Custom Middleware
46
-
47
- Inherit from `BugBunny::Middleware::Base` and implement `call`:
48
-
49
- ```ruby
50
- class RequestLoggingMiddleware < BugBunny::Middleware::Base
51
- def call(request)
52
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
53
- response = app.call(request)
54
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
55
-
56
- Rails.logger.info("amqp_request path=#{request.path} method=#{request.method} duration_s=#{duration.round(4)}")
57
- response
58
- end
59
- end
60
- ```
61
-
62
- `app.call(request)` invokes the next middleware in the stack (or the producer at the end of the chain). The return value is the response hash `{ 'status' => Integer, 'body' => ..., 'headers' => Hash }`.
63
-
64
- ### Modifying the request
65
-
66
- ```ruby
67
- class InjectTraceHeaderMiddleware < BugBunny::Middleware::Base
68
- def call(request)
69
- request.headers['X-Trace-Id'] = MyTracer.current_trace_id
70
- app.call(request)
71
- end
72
- end
73
- ```
74
-
75
- ### Modifying the response
76
-
77
- ```ruby
78
- class ResponseCachingMiddleware < BugBunny::Middleware::Base
79
- def call(request)
80
- cached = Cache.get(request.path)
81
- return cached if cached
82
-
83
- response = app.call(request)
84
- Cache.set(request.path, response, ttl: 30) if response['status'] == 200
85
- response
86
- end
87
- end
88
- ```
89
-
90
- ---
91
-
92
- ## Middlewares on Resource
93
-
94
- `BugBunny::Resource` uses `RaiseError` and `JsonResponse` by default. Add custom middlewares via the class-level DSL:
95
-
96
- ```ruby
97
- class RemoteNode < BugBunny::Resource
98
- client_middleware do |stack|
99
- stack.use RequestLoggingMiddleware
100
- stack.use InjectTraceHeaderMiddleware
101
- end
102
- end
103
- ```
104
-
105
- Custom middlewares are injected after the core ones, so `RaiseError` and `JsonResponse` always run first.
106
-
107
- Middlewares defined in a parent class are inherited:
108
-
109
- ```ruby
110
- class ApplicationResource < BugBunny::Resource
111
- client_middleware do |stack|
112
- stack.use InjectTraceHeaderMiddleware
113
- end
114
- end
115
-
116
- class RemoteNode < ApplicationResource
117
- # inherits InjectTraceHeaderMiddleware
118
- end
119
- ```
@@ -1,127 +0,0 @@
1
- # Consumer Middleware
2
-
3
- The consumer middleware stack runs before every message reaches the router. It is the right place for cross-cutting concerns: distributed tracing, authentication, audit logging, rate limiting.
4
-
5
- ## Execution Order
6
-
7
- ```
8
- RabbitMQ message arrives
9
- |
10
- v
11
- ConsumerMiddleware::Stack
12
- Middleware A (first registered)
13
- Middleware B
14
- Middleware C
15
- process_message (router → controller → action)
16
- Middleware C (post-processing)
17
- Middleware B (post-processing)
18
- Middleware A (post-processing)
19
- ```
20
-
21
- Middlewares execute FIFO. If no middlewares are registered, the overhead is zero.
22
-
23
- ---
24
-
25
- ## Writing a Middleware
26
-
27
- Inherit from `BugBunny::ConsumerMiddleware::Base` and implement `call`:
28
-
29
- ```ruby
30
- class TracingMiddleware < BugBunny::ConsumerMiddleware::Base
31
- def call(delivery_info, properties, body)
32
- trace_id = properties.headers&.dig('X-Trace-Id') || SecureRandom.uuid
33
- MyTracer.with_trace(trace_id) do
34
- @app.call(delivery_info, properties, body)
35
- end
36
- end
37
- end
38
- ```
39
-
40
- `@app.call(delivery_info, properties, body)` passes control to the next middleware. Always call it (unless you intentionally want to halt processing).
41
-
42
- ### Available data
43
-
44
- | Argument | Type | Contents |
45
- |---|---|---|
46
- | `delivery_info` | `Bunny::DeliveryInfo` | `routing_key`, `exchange`, `delivery_tag`, `redelivered` |
47
- | `properties` | `Bunny::MessageProperties` | `headers` (custom AMQP headers), `correlation_id`, `reply_to`, `content_type` |
48
- | `body` | `String` | Raw message payload (typically JSON) |
49
-
50
- ### Post-processing
51
-
52
- Code after `@app.call` runs once the message has been fully processed (controller action completed):
53
-
54
- ```ruby
55
- class AuditMiddleware < BugBunny::ConsumerMiddleware::Base
56
- def call(delivery_info, properties, body)
57
- @app.call(delivery_info, properties, body)
58
- rescue => e
59
- AuditLog.record_failure(routing_key: delivery_info.routing_key, error: e.class.name)
60
- raise
61
- ensure
62
- AuditLog.record_received(routing_key: delivery_info.routing_key)
63
- end
64
- end
65
- ```
66
-
67
- ---
68
-
69
- ## Registering Middlewares
70
-
71
- ```ruby
72
- # After BugBunny.configure
73
- BugBunny.consumer_middlewares.use TracingMiddleware
74
- BugBunny.consumer_middlewares.use AuditMiddleware
75
- BugBunny.consumer_middlewares.use AuthenticationMiddleware
76
- ```
77
-
78
- Registrations are thread-safe. You can register middlewares at any point before the Consumer starts subscribing.
79
-
80
- ---
81
-
82
- ## Auto-registration from External Gems
83
-
84
- Integration gems can register themselves transparently when required, without the user modifying the `configure` block:
85
-
86
- ```ruby
87
- # lib/my_tracing_gem/bug_bunny.rb
88
- require 'my_tracing_gem/bug_bunny/consumer_middleware'
89
- BugBunny.consumer_middlewares.use MyTracingGem::BugBunny::ConsumerMiddleware
90
- ```
91
-
92
- The user only needs:
93
-
94
- ```ruby
95
- require 'my_tracing_gem/bug_bunny'
96
- ```
97
-
98
- ---
99
-
100
- ## Halting Message Processing
101
-
102
- To reject a message without routing it (e.g., authentication failure):
103
-
104
- ```ruby
105
- class AuthMiddleware < BugBunny::ConsumerMiddleware::Base
106
- def call(delivery_info, properties, body)
107
- token = properties.headers&.dig('X-Service-Token')
108
- unless TokenValidator.valid?(token)
109
- # Do not call @app — message is effectively dropped from this consumer's perspective
110
- # The channel will nack/reject it based on the consumer's error handling
111
- return
112
- end
113
-
114
- @app.call(delivery_info, properties, body)
115
- end
116
- end
117
- ```
118
-
119
- Note: halting in a middleware skips all routing and controller logic, but the message is still acknowledged at the AMQP level (the Consumer's normal ack happens in `process_message`, which was never reached). If you need to nack/reject, interact with `delivery_info.delivery_tag` directly — but this is an advanced use case.
120
-
121
- ---
122
-
123
- ## Inspecting the Stack
124
-
125
- ```ruby
126
- BugBunny.consumer_middlewares.empty? # => false if any middleware registered
127
- ```