aris 1.4.2 → 1.5.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +251 -0
  3. data/README.md +18 -0
  4. data/docs/ADAPTERS.md +478 -0
  5. data/docs/ARCHITECTURE.md +222 -0
  6. data/docs/CONTENT.md +967 -0
  7. data/docs/PERFORMANCE.md +492 -0
  8. data/docs/PLUGIN_DEVELOPMENT.md +688 -0
  9. data/docs/USAGE.md +4998 -0
  10. data/docs/plugins/API_KEY_AUTH.md +232 -0
  11. data/docs/plugins/BASIC_AUTH.md +582 -0
  12. data/docs/plugins/BEARER_AUTH.md +394 -0
  13. data/docs/plugins/CACHE.md +369 -0
  14. data/docs/plugins/COMPRESSION.md +216 -0
  15. data/docs/plugins/COOKIES.md +30 -0
  16. data/docs/plugins/CORS.md +283 -0
  17. data/docs/plugins/CSRF.md +751 -0
  18. data/docs/plugins/ETAG.md +308 -0
  19. data/docs/plugins/FORM_PARSER.md +193 -0
  20. data/docs/plugins/HEALTH_CHECK.md +469 -0
  21. data/docs/plugins/JSON.md +291 -0
  22. data/docs/plugins/MULTIPART.md +427 -0
  23. data/docs/plugins/RATE_LIMITER.md +368 -0
  24. data/docs/plugins/REQUEST_ID.md +369 -0
  25. data/docs/plugins/REQUEST_LOGGER.md +151 -0
  26. data/docs/plugins/SECURITY.md +193 -0
  27. data/docs/plugins/SESSION.md +98 -0
  28. data/lib/aris/adapters/rack/adapter.rb +17 -2
  29. data/lib/aris/adapters/rack/request.rb +29 -11
  30. data/lib/aris/plugins/basic_auth.rb +3 -1
  31. data/lib/aris/plugins/cookies.rb +4 -32
  32. data/lib/aris/plugins/cors.rb +8 -1
  33. data/lib/aris/plugins/csrf.rb +63 -22
  34. data/lib/aris/plugins/flash.rb +3 -1
  35. data/lib/aris/plugins/form_parser.rb +52 -31
  36. data/lib/aris/plugins/multipart.rb +22 -2
  37. data/lib/aris/plugins/request_logger.rb +8 -1
  38. data/lib/aris/plugins/security_headers.rb +8 -1
  39. data/lib/aris/plugins/session.rb +150 -99
  40. data/lib/aris/response_helpers.rb +41 -0
  41. data/lib/aris/version.rb +2 -2
  42. metadata +31 -3
@@ -0,0 +1,368 @@
1
+ # Rate Limiter Plugin
2
+
3
+ Throttle requests to prevent abuse, brute force attacks, and API overuse.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ # lib/aris.rb already includes this
9
+ require_relative 'aris/plugins/rate_limiter'
10
+ ```
11
+
12
+ ## How It Works
13
+
14
+ Tracks request counts per key (API key, IP address, user ID) within a time window. Returns **429 Too Many Requests** when limit exceeded.
15
+
16
+ **Default:** 100 requests per 60 seconds per key.
17
+
18
+ ---
19
+
20
+ ## Basic Usage
21
+
22
+ ### Simple API Rate Limiting
23
+
24
+ ```ruby
25
+ Aris.routes({
26
+ "api.example.com": {
27
+ use: [:rate_limit], # Default: 100 requests/60s
28
+ "/data": { get: { to: DataHandler } }
29
+ }
30
+ })
31
+ ```
32
+
33
+ Requests include rate limit headers:
34
+ ```
35
+ X-RateLimit-Limit: 100
36
+ X-RateLimit-Remaining: 47
37
+ ```
38
+
39
+ When limit exceeded:
40
+ ```
41
+ HTTP/1.1 429 Too Many Requests
42
+ Retry-After: 60
43
+ Rate limit exceeded. Try again later.
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Configuration
49
+
50
+ Rate limiting keys **default to API key or host**:
51
+
52
+ ```ruby
53
+ # Uses HTTP_X_API_KEY header if present, else falls back to HTTP_HOST
54
+ request.headers['HTTP_X_API_KEY'] || request.host
55
+ ```
56
+
57
+ **For custom configuration**, you need to build a custom instance. The plugin is currently not configurable via symbol registration.
58
+
59
+ ---
60
+
61
+ ## Advanced Usage
62
+
63
+ ### Custom Instance (Not Yet Supported)
64
+
65
+ The current implementation is fixed at 100 requests per 60 seconds. For production use, you'll want to enhance it:
66
+
67
+ ```ruby
68
+ # Future enhancement - custom limits
69
+ rate_limit = Aris::Plugins::RateLimiter.build(
70
+ limit: 1000,
71
+ window: 3600, # 1 hour
72
+ key_extractor: ->(request) {
73
+ # Rate limit by authenticated user
74
+ request.instance_variable_get(:@current_user)
75
+ }
76
+ )
77
+
78
+ Aris.routes({
79
+ "api.example.com": {
80
+ use: [rate_limit],
81
+ "/data": { get: { to: DataHandler } }
82
+ }
83
+ })
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Combining with Authentication
89
+
90
+ Always rate limit **after** authentication to prevent brute force:
91
+
92
+ ```ruby
93
+ bearer_auth = Aris::Plugins::BearerAuth.build(
94
+ validator: ->(token) { ApiKey.valid?(token) }
95
+ )
96
+
97
+ Aris.routes({
98
+ "api.example.com": {
99
+ use: [bearer_auth, :rate_limit], # Auth first, then rate limit
100
+ "/data": { get: { to: DataHandler } }
101
+ }
102
+ })
103
+ ```
104
+
105
+ **Why this order?**
106
+ - Invalid auth fails fast (no rate limit check needed)
107
+ - Valid requests get rate limited per authenticated user
108
+
109
+ ---
110
+
111
+ ## Production Setup
112
+
113
+ ### Use Redis Instead of Memory
114
+
115
+ The current implementation uses in-memory storage. **For production, use Redis:**
116
+
117
+ ```ruby
118
+ # lib/aris/plugins/rate_limiter_redis.rb
119
+ require 'redis'
120
+
121
+ class RateLimiterRedis
122
+ REDIS = Redis.new(url: ENV['REDIS_URL'])
123
+
124
+ def initialize(limit: 100, window: 60)
125
+ @limit = limit
126
+ @window = window
127
+ end
128
+
129
+ def call(request, response)
130
+ key = rate_limit_key(request)
131
+ redis_key = "rate_limit:#{key}"
132
+
133
+ count = REDIS.multi do |r|
134
+ r.incr(redis_key)
135
+ r.expire(redis_key, @window)
136
+ end.first
137
+
138
+ response.headers['X-RateLimit-Limit'] = @limit.to_s
139
+ response.headers['X-RateLimit-Remaining'] = [@limit - count, 0].max.to_s
140
+
141
+ if count > @limit
142
+ response.status = 429
143
+ response.headers['Retry-After'] = @window.to_s
144
+ response.body = ['Rate limit exceeded. Try again later.']
145
+ return response
146
+ end
147
+
148
+ nil
149
+ end
150
+
151
+ private
152
+
153
+ def rate_limit_key(request)
154
+ request.headers['HTTP_X_API_KEY'] || request.headers['REMOTE_ADDR']
155
+ end
156
+ end
157
+
158
+ # Use it
159
+ redis_limiter = RateLimiterRedis.new(limit: 1000, window: 3600)
160
+
161
+ Aris.routes({
162
+ "api.example.com": {
163
+ use: [redis_limiter],
164
+ "/data": { get: { to: DataHandler } }
165
+ }
166
+ })
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Common Patterns
172
+
173
+ ### Different Limits for Different Endpoints
174
+
175
+ ```ruby
176
+ strict_limit = RateLimiterRedis.new(limit: 10, window: 60)
177
+ normal_limit = RateLimiterRedis.new(limit: 100, window: 60)
178
+
179
+ Aris.routes({
180
+ "api.example.com": {
181
+ use: [normal_limit], # Default for all routes
182
+
183
+ "/expensive": {
184
+ use: [strict_limit], # Override with stricter limit
185
+ post: { to: ExpensiveHandler }
186
+ }
187
+ }
188
+ })
189
+ ```
190
+
191
+ ### Per-User Rate Limiting
192
+
193
+ ```ruby
194
+ user_limiter = RateLimiterRedis.new(
195
+ limit: 1000,
196
+ window: 3600,
197
+ key_extractor: ->(request) {
198
+ # Rate limit by authenticated user ID
199
+ user = request.instance_variable_get(:@current_user)
200
+ "user:#{user.id}"
201
+ }
202
+ )
203
+ ```
204
+
205
+ ### Bypass Rate Limiting for Premium Users
206
+
207
+ ```ruby
208
+ class SmartRateLimiter
209
+ def call(request, response)
210
+ user = request.instance_variable_get(:@current_user)
211
+
212
+ # Premium users skip rate limiting
213
+ return nil if user&.premium?
214
+
215
+ # Regular rate limiting logic
216
+ # ...
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### Per-IP Brute Force Protection
222
+
223
+ ```ruby
224
+ login_limiter = RateLimiterRedis.new(
225
+ limit: 5,
226
+ window: 300, # 5 attempts per 5 minutes
227
+ key_extractor: ->(request) {
228
+ request.headers['REMOTE_ADDR']
229
+ }
230
+ )
231
+
232
+ Aris.routes({
233
+ "example.com": {
234
+ "/login": {
235
+ use: [login_limiter], # Rate limit login attempts
236
+ post: { to: LoginHandler }
237
+ }
238
+ }
239
+ })
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Testing
245
+
246
+ ```ruby
247
+ class RateLimiterTest < Minitest::Test
248
+ def test_requests_under_limit_pass
249
+ Aris.routes({
250
+ "api.example.com": {
251
+ use: [:rate_limit],
252
+ "/data": { get: { to: DataHandler } }
253
+ }
254
+ })
255
+
256
+ app = Aris::Adapters::RackApp.new
257
+ Aris::Plugins::RateLimiter.reset! # Clear state
258
+
259
+ env = build_env('/data', api_key: 'test-key')
260
+
261
+ # First 100 requests succeed
262
+ 100.times do
263
+ status, _, _ = app.call(env)
264
+ assert_equal 200, status
265
+ end
266
+
267
+ # 101st fails
268
+ status, headers, _ = app.call(env)
269
+ assert_equal 429, status
270
+ assert_equal '60', headers['Retry-After']
271
+ end
272
+ end
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Production Tips
278
+
279
+ ### 1. Monitor Rate Limit Hits
280
+
281
+ ```ruby
282
+ class MonitoredRateLimiter < RateLimiterRedis
283
+ def call(request, response)
284
+ result = super
285
+
286
+ if result == response && response.status == 429
287
+ # Log rate limit hit
288
+ key = rate_limit_key(request)
289
+ Rails.logger.warn("Rate limit exceeded: #{key}")
290
+ Metrics.increment('api.rate_limit.exceeded', tags: ["key:#{key}"])
291
+ end
292
+
293
+ result
294
+ end
295
+ end
296
+ ```
297
+
298
+ ### 2. Graceful Degradation
299
+
300
+ ```ruby
301
+ def call(request, response)
302
+ begin
303
+ # Rate limiting logic
304
+ rescue Redis::ConnectionError => e
305
+ # If Redis is down, allow requests through
306
+ Rails.logger.error("Rate limiter Redis error: #{e.message}")
307
+ return nil
308
+ end
309
+ end
310
+ ```
311
+
312
+ ### 3. Different Limits by Tier
313
+
314
+ ```ruby
315
+ LIMITS = {
316
+ 'free' => { limit: 100, window: 3600 },
317
+ 'pro' => { limit: 1000, window: 3600 },
318
+ 'enterprise' => { limit: 10000, window: 3600 }
319
+ }
320
+
321
+ class TieredRateLimiter
322
+ def call(request, response)
323
+ user = request.instance_variable_get(:@current_user)
324
+ config = LIMITS[user.tier]
325
+
326
+ # Apply tier-specific limit
327
+ # ...
328
+ end
329
+ end
330
+ ```
331
+
332
+ ---
333
+
334
+ ## Security Notes
335
+
336
+ **✅ Use for:**
337
+ - API endpoints
338
+ - Login/authentication endpoints
339
+ - Password reset endpoints
340
+ - Resource-intensive operations
341
+ - Public endpoints
342
+
343
+ **⚠️ Considerations:**
344
+ - Rate limit by user/token (not just IP) to prevent shared IP issues
345
+ - Use Redis for distributed systems (in-memory won't work across servers)
346
+ - Set appropriate limits (too strict = bad UX, too loose = ineffective)
347
+ - Monitor for legitimate users hitting limits
348
+
349
+ **❌ Don't rely solely on:**
350
+ - Rate limiting alone for security (defense in depth)
351
+ - IP-based limiting in cloud environments (shared IPs)
352
+
353
+ ---
354
+
355
+ ## Current Limitations
356
+
357
+ The built-in rate limiter:
358
+ - ✅ Works for single-server deployments
359
+ - ✅ Thread-safe with mutex
360
+ - ❌ Uses in-memory storage (won't persist across restarts)
361
+ - ❌ Won't work across multiple servers
362
+ - ❌ Fixed at 100 requests per 60 seconds
363
+
364
+ **For production:** Implement Redis-backed rate limiting as shown above.
365
+
366
+ ---
367
+
368
+ Need help? Check out the [full plugin development guide](../docs/plugin-development.md).
@@ -0,0 +1,369 @@
1
+ # Request ID Plugin
2
+
3
+ Generates unique request IDs for distributed tracing and log correlation. Essential for debugging in production environments.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ require 'aris/plugins/request_id'
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ```ruby
14
+ request_id = Aris::Plugins::RequestId.build
15
+
16
+ Aris.routes({
17
+ "api.example.com": {
18
+ use: [request_id], # Apply to all routes
19
+ "/users": { get: { to: UsersHandler } }
20
+ }
21
+ })
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ | Option | Type | Default | Description |
27
+ |--------|------|---------|-------------|
28
+ | `header_name` | String | `'X-Request-ID'` | HTTP header name for request ID |
29
+ | `generator` | Proc | `-> { SecureRandom.uuid }` | Custom ID generator function |
30
+
31
+ ## How It Works
32
+
33
+ 1. **Check for existing ID**: If request has `X-Request-ID` header (from proxy/load balancer), use it
34
+ 2. **Generate new ID**: If none provided, generate UUID
35
+ 3. **Store on request**: Handlers can access via `@request_id` instance variable
36
+ 4. **Return in response**: Set `X-Request-ID` header in response
37
+
38
+ ## Examples
39
+
40
+ ### Default Configuration
41
+
42
+ ```ruby
43
+ request_id = Aris::Plugins::RequestId.build
44
+
45
+ # Request without ID:
46
+ # Response: X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
47
+
48
+ # Request with ID:
49
+ # Request: X-Request-ID: existing-id-123
50
+ # Response: X-Request-ID: existing-id-123 (preserved)
51
+ ```
52
+
53
+ ### Custom Header Name
54
+
55
+ ```ruby
56
+ request_id = Aris::Plugins::RequestId.build(
57
+ header_name: 'X-Trace-ID'
58
+ )
59
+
60
+ # Response will have X-Trace-ID instead of X-Request-ID
61
+ ```
62
+
63
+ ### Custom Generator
64
+
65
+ ```ruby
66
+ # Sequential IDs
67
+ counter = 0
68
+ request_id = Aris::Plugins::RequestId.build(
69
+ generator: -> { "REQ-#{counter += 1}" }
70
+ )
71
+
72
+ # Timestamp-based IDs
73
+ request_id = Aris::Plugins::RequestId.build(
74
+ generator: -> { "#{Time.now.to_i}-#{SecureRandom.hex(4)}" }
75
+ )
76
+
77
+ # Short IDs
78
+ request_id = Aris::Plugins::RequestId.build(
79
+ generator: -> { SecureRandom.hex(8) } # 16 characters
80
+ )
81
+ ```
82
+
83
+ ### Accessing Request ID in Handlers
84
+
85
+ ```ruby
86
+ class UsersHandler
87
+ def self.call(request, params)
88
+ request_id = request.instance_variable_get(:@request_id)
89
+
90
+ # Use in logging
91
+ logger.info("Processing user request", request_id: request_id)
92
+
93
+ # Return in response
94
+ {
95
+ users: User.all,
96
+ request_id: request_id
97
+ }
98
+ end
99
+ end
100
+ ```
101
+
102
+ ### With Logging Plugin
103
+
104
+ ```ruby
105
+ request_id = Aris::Plugins::RequestId.build
106
+ logger = Aris::Plugins::RequestLogger.build(format: :json)
107
+
108
+ Aris.routes({
109
+ "api.example.com": {
110
+ use: [request_id, logger], # Request ID first
111
+ "/users": { get: { to: UsersHandler } }
112
+ }
113
+ })
114
+
115
+ # Logs will include request_id for correlation
116
+ ```
117
+
118
+ ## Production Tips
119
+
120
+ ### 1. Plugin Order (Critical)
121
+
122
+ Place **first** in plugin chain:
123
+
124
+ ```ruby
125
+ Aris.routes({
126
+ "api.example.com": {
127
+ use: [
128
+ request_id, # ← FIRST - generate ID
129
+ bearer_auth, # Use request_id in auth logs
130
+ json_parser, # Use request_id in parser logs
131
+ request_logger # Log request_id
132
+ ]
133
+ }
134
+ })
135
+ ```
136
+
137
+ ### 2. Structured Logging
138
+
139
+ Combine with logging plugin for correlation:
140
+
141
+ ```ruby
142
+ class CustomLogger
143
+ def self.call(request, response)
144
+ request_id = request.instance_variable_get(:@request_id)
145
+
146
+ logger.info({
147
+ request_id: request_id,
148
+ method: request.method,
149
+ path: request.path,
150
+ timestamp: Time.now.iso8601
151
+ }.to_json)
152
+
153
+ nil
154
+ end
155
+ end
156
+ ```
157
+
158
+ ### 3. Load Balancer Integration
159
+
160
+ Preserve IDs from upstream:
161
+
162
+ ```ruby
163
+ # AWS ALB sends X-Amzn-Trace-Id
164
+ request_id = Aris::Plugins::RequestId.build(
165
+ header_name: 'X-Amzn-Trace-Id'
166
+ )
167
+
168
+ # Or check multiple headers
169
+ class SmartRequestId
170
+ def self.call(request, response)
171
+ request_id = request.headers['HTTP_X_AMZN_TRACE_ID'] ||
172
+ request.headers['HTTP_X_REQUEST_ID'] ||
173
+ SecureRandom.uuid
174
+
175
+ request.instance_variable_set(:@request_id, request_id)
176
+ response.headers['X-Request-ID'] = request_id
177
+
178
+ nil
179
+ end
180
+ end
181
+ ```
182
+
183
+ ### 4. Error Tracking
184
+
185
+ Include request ID in error responses:
186
+
187
+ ```ruby
188
+ class ErrorHandler
189
+ def self.call(request, exception)
190
+ request_id = request.instance_variable_get(:@request_id)
191
+
192
+ [500, { 'content-type' => 'application/json' }, [
193
+ {
194
+ error: 'Internal Server Error',
195
+ request_id: request_id,
196
+ message: exception.message
197
+ }.to_json
198
+ ]]
199
+ end
200
+ end
201
+
202
+ Aris.default(error: ErrorHandler)
203
+ ```
204
+
205
+ ### 5. Distributed Tracing
206
+
207
+ Propagate to downstream services:
208
+
209
+ ```ruby
210
+ class ServiceClient
211
+ def self.call_api(request, endpoint)
212
+ request_id = request.instance_variable_get(:@request_id)
213
+
214
+ # Pass to downstream service
215
+ HTTParty.get(
216
+ "https://api.example.com#{endpoint}",
217
+ headers: { 'X-Request-ID' => request_id }
218
+ )
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### 6. Database Query Tagging
224
+
225
+ Tag queries with request ID:
226
+
227
+ ```ruby
228
+ class UsersHandler
229
+ def self.call(request, params)
230
+ request_id = request.instance_variable_get(:@request_id)
231
+
232
+ # Tag ActiveRecord queries
233
+ ActiveRecord::Base.connection.execute(
234
+ "SET application_name = 'request_#{request_id}'"
235
+ )
236
+
237
+ User.all
238
+ end
239
+ end
240
+ ```
241
+
242
+ ### 7. APM Integration
243
+
244
+ Send to monitoring services:
245
+
246
+ ```ruby
247
+ class APMHandler
248
+ def self.call(request, params)
249
+ request_id = request.instance_variable_get(:@request_id)
250
+
251
+ # NewRelic
252
+ NewRelic::Agent.add_custom_attributes(request_id: request_id)
253
+
254
+ # Datadog
255
+ Datadog::Tracing.active_span&.set_tag('request.id', request_id)
256
+
257
+ # Your logic here
258
+ end
259
+ end
260
+ ```
261
+
262
+ ## Common Patterns
263
+
264
+ ### Multi-Service Architecture
265
+
266
+ ```ruby
267
+ # Service A (Frontend API)
268
+ request_id = Aris::Plugins::RequestId.build
269
+
270
+ # Service B (Backend API)
271
+ # Receives X-Request-ID from Service A
272
+ request_id = Aris::Plugins::RequestId.build # Preserves existing ID
273
+
274
+ # All logs across services have same request_id for correlation
275
+ ```
276
+
277
+ ### Request ID in Sidekiq Jobs
278
+
279
+ ```ruby
280
+ class ProcessOrderJob
281
+ def perform(order_id, request_id)
282
+ logger.tagged(request_id) do
283
+ logger.info "Processing order #{order_id}"
284
+ # Process order
285
+ end
286
+ end
287
+ end
288
+
289
+ class OrdersHandler
290
+ def self.call(request, params)
291
+ request_id = request.instance_variable_get(:@request_id)
292
+
293
+ # Enqueue with request_id
294
+ ProcessOrderJob.perform_async(params[:id], request_id)
295
+
296
+ { status: 'processing', request_id: request_id }
297
+ end
298
+ end
299
+ ```
300
+
301
+ ### Client Response Headers
302
+
303
+ ```ruby
304
+ # Clients can use request_id for support tickets
305
+ # Response: X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
306
+
307
+ # User: "Error with request ID: 550e8400-..."
308
+ # Support: grep logs for 550e8400-... → full request trace
309
+ ```
310
+
311
+ ## ID Format Options
312
+
313
+ **UUID (default):**
314
+ ```ruby
315
+ # 550e8400-e29b-41d4-a716-446655440000
316
+ generator: -> { SecureRandom.uuid }
317
+ ```
318
+
319
+ **Short hex:**
320
+ ```ruby
321
+ # a3f2bc9e
322
+ generator: -> { SecureRandom.hex(4) }
323
+ ```
324
+
325
+ **Timestamp + random:**
326
+ ```ruby
327
+ # 1634567890-a3f2bc9e
328
+ generator: -> { "#{Time.now.to_i}-#{SecureRandom.hex(4)}" }
329
+ ```
330
+
331
+ **Sequential (testing only):**
332
+ ```ruby
333
+ # REQ-1, REQ-2, REQ-3...
334
+ counter = 0
335
+ generator: -> { "REQ-#{counter += 1}" }
336
+ ```
337
+
338
+ ## Benchmarks
339
+
340
+ **Performance impact:**
341
+ - UUID generation: ~0.01ms
342
+ - Header setting: ~0.001ms
343
+ - Total overhead: <0.02ms per request
344
+
345
+ Negligible impact, essential value.
346
+
347
+ ## Notes
348
+
349
+ - Thread-safe (UUID generation is thread-safe)
350
+ - Preserves IDs from load balancers/proxies
351
+ - Available to all downstream handlers
352
+ - Always returned in response headers
353
+ - Compatible with AWS ALB, Nginx, HAProxy trace IDs
354
+
355
+ ## Troubleshooting
356
+
357
+ **Request ID not showing up?**
358
+ - Check plugin is in `use:` array
359
+ - Verify plugin runs before logging/handlers
360
+ - Confirm response headers are visible
361
+
362
+ **Different IDs in logs vs response?**
363
+ - Ensure request_id plugin runs first
364
+ - Check no other plugin overwrites header
365
+
366
+ **Load balancer ID not preserved?**
367
+ - Check header name matches (`X-Request-ID` vs `X-Amzn-Trace-Id`)
368
+ - Verify load balancer is forwarding header
369
+ ```