bug_bunny 4.8.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 (51) 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/sentry/SKILL.md +135 -0
  6. data/.agents/skills/sentry/references/api-endpoints.md +147 -0
  7. data/.agents/skills/sentry/scripts/sentry.rb +194 -0
  8. data/.agents/skills/skill-builder/SKILL.md +232 -0
  9. data/.agents/skills/skill-manager/SKILL.md +172 -0
  10. data/.agents/skills/skill-manager/scripts/sync.rb +310 -0
  11. data/.agents/skills/yard/SKILL.md +311 -0
  12. data/.agents/skills/yard/references/tipos.md +144 -0
  13. data/CHANGELOG.md +8 -0
  14. data/CLAUDE.md +28 -231
  15. data/lib/bug_bunny/version.rb +1 -1
  16. data/skill/SKILL.md +230 -0
  17. data/skill/references/client-middleware.md +144 -0
  18. data/skill/references/consumer.md +104 -0
  19. data/skill/references/controller.md +105 -0
  20. data/skill/references/errores.md +97 -0
  21. data/skill/references/resource.md +116 -0
  22. data/skill/references/routing.md +82 -0
  23. data/skill/references/testing.md +138 -0
  24. data/skills.lock +24 -0
  25. data/skills.yml +19 -0
  26. metadata +24 -28
  27. data/.claude/commands/gem-ai-setup.md +0 -174
  28. data/.claude/commands/pr.md +0 -53
  29. data/.claude/commands/release.md +0 -52
  30. data/.claude/commands/rubocop.md +0 -22
  31. data/.claude/commands/service-ai-setup.md +0 -168
  32. data/.claude/commands/test.md +0 -28
  33. data/.claude/commands/yard.md +0 -46
  34. data/docs/_index.md +0 -50
  35. data/docs/ai/_index.md +0 -56
  36. data/docs/ai/antipatterns.md +0 -166
  37. data/docs/ai/api.md +0 -251
  38. data/docs/ai/architecture.md +0 -92
  39. data/docs/ai/errors.md +0 -158
  40. data/docs/ai/faq_external.md +0 -133
  41. data/docs/ai/faq_internal.md +0 -86
  42. data/docs/ai/glossary.md +0 -45
  43. data/docs/concepts.md +0 -140
  44. data/docs/howto/controller.md +0 -194
  45. data/docs/howto/middleware_client.md +0 -119
  46. data/docs/howto/middleware_consumer.md +0 -127
  47. data/docs/howto/rails.md +0 -214
  48. data/docs/howto/resource.md +0 -200
  49. data/docs/howto/routing.md +0 -133
  50. data/docs/howto/testing.md +0 -259
  51. data/docs/howto/tracing.md +0 -119
@@ -1,86 +0,0 @@
1
- ## FAQ — Internal (Maintainer / Developer of BugBunny)
2
-
3
- ### Why is Producer cached per connection slot instead of instantiated per request?
4
-
5
- `Producer#initialize` calls `channel.basic_consume` on the `amq.rabbitmq.reply-to` pseudo-queue to listen for RPC replies. Creating a second `basic_consume` on the same channel raises an AMQP error. Therefore, one Producer per channel (= per connection slot) is mandatory. The cache is stored as `@_bug_bunny_producer` ivar on the Bunny connection object, which is safe because `ConnectionPool` guarantees each slot is used by one thread at a time.
6
-
7
- ---
8
-
9
- ### Why is Session cached per connection slot?
10
-
11
- Same reason as Producer — and the Session wraps the channel. Recreating a Session would create a new channel and invalidate the cached exchange/queue objects. Caching is stored as `@_bug_bunny_session` on the Bunny connection object.
12
-
13
- ---
14
-
15
- ### How does the Session handle thread safety for exchange/queue declarations?
16
-
17
- Session uses a double-checked locking pattern with a `Mutex` for its caches (`@exchange_cache`, `@queue_cache`). The read path (fast path) checks without locking; the write path acquires the mutex and checks again before writing. This avoids redundant AMQP declarations without making every read a mutex acquisition.
18
-
19
- ---
20
-
21
- ### Why does Resource have two dirty tracking mechanisms?
22
-
23
- ActiveModel::Dirty only tracks attributes declared with `attribute :name, :type`. BugBunny Resources allow dynamic attributes (unknown at class definition time) stored in `@extra_attributes`. These are tracked manually via `@dynamic_changes` (a `Set`). The `changed?` and `changed` methods merge both to produce a unified change list.
24
-
25
- ---
26
-
27
- ### How does the 3-level config cascade work in Resource?
28
-
29
- `resolve_config(key, instance_var)` checks in order:
30
- 1. `Thread.current["bb_#{object_id}_#{key}"]` — set by `.with(...)`.
31
- 2. `self.instance_variable_get(instance_var)` — class-level static config.
32
- 3. Walk `superclass` chain up to (but not including) `BugBunny::Resource`.
33
-
34
- This means a subclass can override the parent's config, and `.with` overrides everything for the duration of its block.
35
-
36
- ---
37
-
38
- ### How does ConsumerMiddleware::Stack protect against concurrent registration?
39
-
40
- `Stack#use` wraps the append in a `@mutex.synchronize` block. The execution path (`call`) does not use the mutex — it reads `@middlewares` once at call time (snapshot). This is safe because Rails/Zeitwerk loads middleware registrations at boot, before any concurrent requests.
41
-
42
- ---
43
-
44
- ### How does routing with namespaces work internally?
45
-
46
- `namespace :admin { resources :users }` generates `Route` objects with:
47
- - `path_prefix: 'admin'` — prepended to the path pattern.
48
- - `namespace: 'Admin::Controllers'` — overrides the default namespace for `constantize`.
49
-
50
- In `process_message`, the Consumer resolves `base_namespace = route_info[:namespace] || config.controller_namespace` before calling `constantize`.
51
-
52
- ---
53
-
54
- ### What is the RCE prevention in the Consumer?
55
-
56
- After `constantize`, the Consumer checks `controller_class < BugBunny::Controller`. This prevents an attacker from crafting a message with a `type` header pointing to an arbitrary Ruby class (e.g., `Kernel` or `File`) and triggering its methods. Any class not inheriting from `BugBunny::Controller` gets a 403 reply and the message is rejected.
57
-
58
- ---
59
-
60
- ### How does `before_action` halt chain propagation?
61
-
62
- `run_before_actions` iterates before-actions and calls each. After each call, it checks `rendered_response`. If a filter called `render(...)`, `@rendered_response` is set. The method returns `false`, and `core_execution` skips the action and `after_actions`. After-actions do not run if the before-action chain was halted.
63
-
64
- ---
65
-
66
- ### How does `after_action` differ from `around_action`?
67
-
68
- `after_action` runs after the action method returns, but only if no `before_action` halted the chain and no exception was raised. `around_action` wraps the entire execution including before/after actions and is responsible for yielding. Use `around_action` when you need cleanup regardless of exceptions.
69
-
70
- ---
71
-
72
- ### How does `safe_log` prevent log failures from affecting main flow?
73
-
74
- `safe_log` wraps every logger call in `rescue StandardError`. It also filters sensitive keys by checking if the key string matches a predefined set of patterns (`password`, `pass`, `passwd`, `secret`, `token`, `api_key`, `auth`) before emitting values. Blocks are always passed to `logger.debug` to avoid string interpolation cost at non-debug levels.
75
-
76
- ---
77
-
78
- ### How is the Consumer health check implemented?
79
-
80
- `start_health_check` creates a `Concurrent::TimerTask` that runs every `health_check_interval` seconds. Each tick calls `channel.queue_declare(queue_name, passive: true)` — a passive declare that verifies the queue exists without creating it. On failure, it calls `session.close`, which triggers the reconnect loop in `subscribe`. The health file is touched on success.
81
-
82
- ---
83
-
84
- ### What triggers the reconnect loop in Consumer?
85
-
86
- Any `StandardError` raised inside the `subscribe` rescue block. The loop uses exponential backoff: `wait = [interval * 2^(attempt-1), max_interval].min`. If `max_reconnect_attempts` is set and exceeded, the error is re-raised, killing the process.
data/docs/ai/glossary.md DELETED
@@ -1,45 +0,0 @@
1
- ## Glossary
2
-
3
- **AMQP** — Advanced Message Queuing Protocol. The binary wire protocol used by RabbitMQ. BugBunny uses Bunny as its AMQP client.
4
-
5
- **Exchange** — A RabbitMQ routing point. Publishers send messages to exchanges; exchanges route them to queues based on bindings and routing keys. BugBunny supports `direct`, `topic`, and `fanout` types.
6
-
7
- **Queue** — A buffer that holds messages until they are consumed. A queue is bound to an exchange with a routing key pattern.
8
-
9
- **Routing key** — A string label attached to a published message. The exchange uses it to decide which queues receive the message. In BugBunny, the routing key defaults to the pluralized resource name (e.g., `users`).
10
-
11
- **Binding** — The link between an exchange and a queue, specifying which routing key patterns match.
12
-
13
- **Session** — `BugBunny::Session`. A wrapper around a Bunny channel. Declares exchanges and queues, caches their AMQP objects, and handles double-checked locking for thread safety.
14
-
15
- **Producer** — `BugBunny::Producer`. Publishes messages to RabbitMQ. Implements both RPC (blocking) and fire-and-forget (non-blocking) modes. One Producer is cached per connection slot.
16
-
17
- **Consumer** — `BugBunny::Consumer`. The subscribe loop that runs in a worker process. Receives messages, routes them through middleware and the router, dispatches to a controller, and sends RPC replies.
18
-
19
- **RPC (Remote Procedure Call)** — A synchronous request-response pattern over RabbitMQ. The producer publishes with `reply_to: 'amq.rabbitmq.reply-to'` and blocks on a `Concurrent::IVar` until the consumer replies to that pseudo-queue.
20
-
21
- **Fire-and-forget** — An asynchronous publish with no reply expected. The producer does not block. Used for events, notifications, and side effects.
22
-
23
- **Direct Reply-to** — A RabbitMQ pseudo-queue (`amq.rabbitmq.reply-to`) that allows RPC replies without declaring a temporary queue. BugBunny uses this for all RPC responses.
24
-
25
- **Controller** — `BugBunny::Controller`. A class that handles an incoming routed message. Mirrors Rails ActionController: supports `before_action`, `around_action`, `after_action`, `rescue_from`, `params`, and `render`.
26
-
27
- **Resource** — `BugBunny::Resource`. An ActiveRecord-like ORM over AMQP. Wraps CRUD operations as RPC calls. Used by the publishing side to interact with a remote service.
28
-
29
- **Client** — `BugBunny::Client`. High-level API for the publisher side. Implements the Onion Middleware (Faraday-style) pattern around the Producer.
30
-
31
- **ConsumerMiddleware** — A pipeline of middleware objects that runs before each message is dispatched to a controller. Used for cross-cutting concerns: tracing, authentication, logging.
32
-
33
- **ConnectionPool** — A `connection_pool` gem pool of Bunny connections. Each slot holds one connection, one Session, and one Producer. `Resource.connection_pool` must be set before making AMQP calls.
34
-
35
- **IVar (Ivar / Concurrent::IVar)** — An immutable variable from the `concurrent-ruby` gem. Used to block the calling thread until the RPC reply arrives or the timeout expires.
36
-
37
- **health_check_file** — A file path that BugBunny touches periodically (every `health_check_interval` seconds) after verifying the RabbitMQ connection. Used as a liveness probe in Docker/Kubernetes.
38
-
39
- **rpc_reply_headers** — A `Proc` returning a `Hash` of AMQP headers injected into every RPC reply. Used to propagate trace context from consumer back to producer.
40
-
41
- **on_rpc_reply** — A `Proc` called on the producer's thread when an RPC reply arrives, with the reply's AMQP headers. Used to hydrate trace context in the calling service.
42
-
43
- **Namespace routing** — A DSL feature that prefixes a group of routes with a module namespace and a path prefix. Example: `namespace :admin` generates routes under `Admin::Controllers::*Controller` with path prefix `admin/`.
44
-
45
- **param_key** — The root key used to wrap the payload in POST/PUT requests. Defaults to the singularized resource name (e.g., `user` for `UsersResource`).
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
- ```