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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +251 -0
- data/README.md +18 -0
- data/docs/ADAPTERS.md +478 -0
- data/docs/ARCHITECTURE.md +222 -0
- data/docs/CONTENT.md +967 -0
- data/docs/PERFORMANCE.md +492 -0
- data/docs/PLUGIN_DEVELOPMENT.md +688 -0
- data/docs/USAGE.md +4998 -0
- data/docs/plugins/API_KEY_AUTH.md +232 -0
- data/docs/plugins/BASIC_AUTH.md +582 -0
- data/docs/plugins/BEARER_AUTH.md +394 -0
- data/docs/plugins/CACHE.md +369 -0
- data/docs/plugins/COMPRESSION.md +216 -0
- data/docs/plugins/COOKIES.md +30 -0
- data/docs/plugins/CORS.md +283 -0
- data/docs/plugins/CSRF.md +751 -0
- data/docs/plugins/ETAG.md +308 -0
- data/docs/plugins/FORM_PARSER.md +193 -0
- data/docs/plugins/HEALTH_CHECK.md +469 -0
- data/docs/plugins/JSON.md +291 -0
- data/docs/plugins/MULTIPART.md +427 -0
- data/docs/plugins/RATE_LIMITER.md +368 -0
- data/docs/plugins/REQUEST_ID.md +369 -0
- data/docs/plugins/REQUEST_LOGGER.md +151 -0
- data/docs/plugins/SECURITY.md +193 -0
- data/docs/plugins/SESSION.md +98 -0
- data/lib/aris/adapters/rack/adapter.rb +17 -2
- data/lib/aris/adapters/rack/request.rb +29 -11
- data/lib/aris/plugins/basic_auth.rb +3 -1
- data/lib/aris/plugins/cookies.rb +4 -32
- data/lib/aris/plugins/cors.rb +8 -1
- data/lib/aris/plugins/csrf.rb +63 -22
- data/lib/aris/plugins/flash.rb +3 -1
- data/lib/aris/plugins/form_parser.rb +52 -31
- data/lib/aris/plugins/multipart.rb +22 -2
- data/lib/aris/plugins/request_logger.rb +8 -1
- data/lib/aris/plugins/security_headers.rb +8 -1
- data/lib/aris/plugins/session.rb +150 -99
- data/lib/aris/response_helpers.rb +41 -0
- data/lib/aris/version.rb +2 -2
- metadata +31 -3
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Response Caching Plugin
|
|
2
|
+
|
|
3
|
+
In-memory response caching for GET requests. Dramatically improves performance by serving cached responses without executing handlers or database queries.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'aris/plugins/cache'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
cache = Aris::Plugins::Cache.build(ttl: 60) # Cache for 60 seconds
|
|
15
|
+
|
|
16
|
+
Aris.routes({
|
|
17
|
+
"api.example.com": {
|
|
18
|
+
use: [cache],
|
|
19
|
+
"/users": { get: { to: UsersHandler } }
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
| Option | Type | Default | Description |
|
|
27
|
+
|--------|------|---------|-------------|
|
|
28
|
+
| `ttl` | Integer | `60` | Time-to-live in seconds |
|
|
29
|
+
| `store` | Hash | `{}` | Custom cache store (in-memory hash by default) |
|
|
30
|
+
| `skip_paths` | Array | `[]` | Regex patterns of paths to skip caching |
|
|
31
|
+
| `cache_control` | String | `nil` | Cache-Control header value to set |
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
1. **Cache Miss (First Request)**
|
|
36
|
+
- Handler executes normally
|
|
37
|
+
- Response cached with TTL
|
|
38
|
+
- `X-Cache: MISS` header added
|
|
39
|
+
|
|
40
|
+
2. **Cache Hit (Subsequent Requests)**
|
|
41
|
+
- Cached response returned instantly
|
|
42
|
+
- Handler NOT executed
|
|
43
|
+
- `X-Cache: HIT` header added
|
|
44
|
+
|
|
45
|
+
3. **Cache Expiry**
|
|
46
|
+
- After TTL expires, cache miss occurs
|
|
47
|
+
- Handler executes, new response cached
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
### Basic Caching
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
cache = Aris::Plugins::Cache.build(ttl: 300) # 5 minutes
|
|
55
|
+
|
|
56
|
+
Aris.routes({
|
|
57
|
+
"api.example.com": {
|
|
58
|
+
use: [cache],
|
|
59
|
+
"/products": { get: { to: ProductsHandler } }
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
# First request: Handler executes, 100ms
|
|
64
|
+
# Second request: Cached, 1ms (100x faster!)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Different TTLs per Route
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
short_cache = Aris::Plugins::Cache.build(ttl: 60) # 1 minute
|
|
71
|
+
long_cache = Aris::Plugins::Cache.build(ttl: 3600) # 1 hour
|
|
72
|
+
|
|
73
|
+
Aris.routes({
|
|
74
|
+
"api.example.com": {
|
|
75
|
+
"/users": {
|
|
76
|
+
use: [short_cache],
|
|
77
|
+
get: { to: UsersHandler }
|
|
78
|
+
},
|
|
79
|
+
"/static": {
|
|
80
|
+
use: [long_cache],
|
|
81
|
+
get: { to: StaticHandler }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Skip Certain Paths
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
cache = Aris::Plugins::Cache.build(
|
|
91
|
+
ttl: 60,
|
|
92
|
+
skip_paths: [
|
|
93
|
+
/^\/admin/, # Skip /admin/*
|
|
94
|
+
/^\/health/, # Skip health checks
|
|
95
|
+
/^\/auth/ # Skip auth endpoints
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
Aris.routes({
|
|
100
|
+
"api.example.com": {
|
|
101
|
+
use: [cache],
|
|
102
|
+
"/users": { get: { to: UsersHandler } }, # Cached
|
|
103
|
+
"/admin": { get: { to: AdminHandler } }, # NOT cached
|
|
104
|
+
"/health": { get: { to: HealthHandler } } # NOT cached
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Custom Cache-Control Headers
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
cache = Aris::Plugins::Cache.build(
|
|
113
|
+
ttl: 300,
|
|
114
|
+
cache_control: 'public, max-age=300, s-maxage=600'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Response includes:
|
|
118
|
+
# Cache-Control: public, max-age=300, s-maxage=600
|
|
119
|
+
# (CDNs and browsers can cache too)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Bypass Cache with Header
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Client sends:
|
|
126
|
+
# Cache-Control: no-cache
|
|
127
|
+
|
|
128
|
+
# Cache is bypassed, fresh response generated
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Cache Key Generation
|
|
132
|
+
|
|
133
|
+
Keys are generated from:
|
|
134
|
+
- Domain
|
|
135
|
+
- Path
|
|
136
|
+
- Query string
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Different cache entries:
|
|
140
|
+
GET /users → cache_key_1
|
|
141
|
+
GET /users?page=2 → cache_key_2
|
|
142
|
+
GET /products → cache_key_3
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Production Tips
|
|
146
|
+
|
|
147
|
+
### 1. Choose TTL Wisely
|
|
148
|
+
|
|
149
|
+
**Highly dynamic data:**
|
|
150
|
+
```ruby
|
|
151
|
+
cache = Cache.build(ttl: 10) # 10 seconds
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Frequently changing:**
|
|
155
|
+
```ruby
|
|
156
|
+
cache = Cache.build(ttl: 60) # 1 minute
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Rarely changing:**
|
|
160
|
+
```ruby
|
|
161
|
+
cache = Cache.build(ttl: 3600) # 1 hour
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Static data:**
|
|
165
|
+
```ruby
|
|
166
|
+
cache = Cache.build(ttl: 86400) # 24 hours
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 2. Use Redis for Multi-Server
|
|
170
|
+
|
|
171
|
+
In-memory cache doesn't work across servers. Use Redis:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
require 'redis'
|
|
175
|
+
|
|
176
|
+
redis_store = Redis.new(url: ENV['REDIS_URL'])
|
|
177
|
+
|
|
178
|
+
cache = Aris::Plugins::Cache.build(
|
|
179
|
+
ttl: 300,
|
|
180
|
+
store: redis_store # Shared across all servers
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Note:** Current implementation uses Hash. You'd need to adapt it for Redis compatibility.
|
|
185
|
+
|
|
186
|
+
### 3. Skip Non-Cacheable Endpoints
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
cache = Cache.build(
|
|
190
|
+
ttl: 60,
|
|
191
|
+
skip_paths: [
|
|
192
|
+
/^\/admin/, # Admin panels
|
|
193
|
+
/^\/auth/, # Authentication
|
|
194
|
+
/^\/checkout/, # Checkout flows
|
|
195
|
+
/^\/cart/, # Shopping carts
|
|
196
|
+
/\/me$/, # User-specific
|
|
197
|
+
/^\/health/, # Health checks
|
|
198
|
+
/^\/metrics/ # Monitoring
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 4. Combine with ETag
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
etag = Aris::Plugins::ETag.build
|
|
207
|
+
cache = Aris::Plugins::Cache.build(ttl: 300)
|
|
208
|
+
|
|
209
|
+
Aris.routes({
|
|
210
|
+
"api.example.com": {
|
|
211
|
+
use: [cache, etag], # Cache first, then ETag
|
|
212
|
+
"/data": { get: { to: DataHandler } }
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
# Flow:
|
|
217
|
+
# 1. Check cache (X-Cache: HIT)
|
|
218
|
+
# 2. Check ETag (304 Not Modified)
|
|
219
|
+
# 3. Bandwidth saved twice!
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 5. Monitor Cache Hit Rate
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
class CacheMetrics
|
|
226
|
+
def self.call(request, response)
|
|
227
|
+
cache_status = response.headers['X-Cache']
|
|
228
|
+
|
|
229
|
+
StatsD.increment('cache.hits') if cache_status == 'HIT'
|
|
230
|
+
StatsD.increment('cache.misses') if cache_status == 'MISS'
|
|
231
|
+
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Target: 60-80% hit rate for cacheable endpoints
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 6. Vary by User
|
|
240
|
+
|
|
241
|
+
User-specific data needs per-user cache keys:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
class UserCache
|
|
245
|
+
def initialize(**config)
|
|
246
|
+
@cache = Aris::Plugins::Cache.build(**config)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def call(request, response)
|
|
250
|
+
# Add user ID to cache key
|
|
251
|
+
user_id = request.instance_variable_get(:@current_user)&.id
|
|
252
|
+
request.instance_variable_set(:@cache_suffix, user_id)
|
|
253
|
+
|
|
254
|
+
@cache.call(request, response)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 7. Clear Cache on Updates
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
class UsersHandler
|
|
263
|
+
def self.update(request, params)
|
|
264
|
+
user = User.update(params[:id], request.json_body)
|
|
265
|
+
|
|
266
|
+
# Clear cache for this user
|
|
267
|
+
cache.clear! # Or selective clear
|
|
268
|
+
|
|
269
|
+
{ user: user }
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Performance Benchmarks
|
|
275
|
+
|
|
276
|
+
**Example API endpoint:**
|
|
277
|
+
- Without cache: 100ms (database query + JSON serialization)
|
|
278
|
+
- With cache: 1-2ms (memory lookup)
|
|
279
|
+
- **50-100x speedup**
|
|
280
|
+
|
|
281
|
+
**Typical cache hit rates:**
|
|
282
|
+
- Public APIs: 70-90%
|
|
283
|
+
- Authenticated APIs: 40-60%
|
|
284
|
+
- Admin panels: 10-30%
|
|
285
|
+
|
|
286
|
+
## Common Patterns
|
|
287
|
+
|
|
288
|
+
### Tiered Caching
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
fast_cache = Cache.build(ttl: 30) # 30s for hot data
|
|
292
|
+
slow_cache = Cache.build(ttl: 3600) # 1h for cold data
|
|
293
|
+
|
|
294
|
+
Aris.routes({
|
|
295
|
+
"api.example.com": {
|
|
296
|
+
"/trending": { use: [fast_cache], get: { to: TrendingHandler } },
|
|
297
|
+
"/archive": { use: [slow_cache], get: { to: ArchiveHandler } }
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Cache Warming
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
# On deploy/startup
|
|
306
|
+
cache = Cache.build(ttl: 300)
|
|
307
|
+
|
|
308
|
+
popular_paths = ['/products', '/categories', '/home']
|
|
309
|
+
popular_paths.each do |path|
|
|
310
|
+
# Make request to warm cache
|
|
311
|
+
app.call(build_env(path))
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Conditional Caching
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
class SmartCache
|
|
319
|
+
def call(request, response)
|
|
320
|
+
# Only cache successful responses
|
|
321
|
+
return nil unless response.status == 200
|
|
322
|
+
|
|
323
|
+
# Only cache small responses
|
|
324
|
+
return nil if response.body.join.bytesize > 100_000
|
|
325
|
+
|
|
326
|
+
# Proceed with caching
|
|
327
|
+
@cache.call(request, response)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Notes
|
|
333
|
+
|
|
334
|
+
- Only caches GET requests (POST/PUT/PATCH/DELETE not cached)
|
|
335
|
+
- Only caches 200 OK responses
|
|
336
|
+
- In-memory by default (not shared across servers)
|
|
337
|
+
- Thread-safe (uses Mutex)
|
|
338
|
+
- Respects `Cache-Control: no-cache` from client
|
|
339
|
+
- Cache key includes domain, path, and query string
|
|
340
|
+
- Expired entries are removed on access
|
|
341
|
+
|
|
342
|
+
## Limitations
|
|
343
|
+
|
|
344
|
+
1. **In-memory only** - Doesn't persist across restarts
|
|
345
|
+
2. **No cache size limit** - Could grow unbounded (add LRU if needed)
|
|
346
|
+
3. **No cache invalidation** - Only expires via TTL
|
|
347
|
+
4. **Single-server** - Doesn't work across multiple servers
|
|
348
|
+
|
|
349
|
+
For production with multiple servers, integrate Redis or Memcached.
|
|
350
|
+
|
|
351
|
+
## Troubleshooting
|
|
352
|
+
|
|
353
|
+
**Cache not working?**
|
|
354
|
+
- Verify request method is GET
|
|
355
|
+
- Check response status is 200
|
|
356
|
+
- Ensure path isn't in `skip_paths`
|
|
357
|
+
- Look for `X-Cache` header in response
|
|
358
|
+
|
|
359
|
+
**Low cache hit rate?**
|
|
360
|
+
- Check if data changes frequently
|
|
361
|
+
- Verify TTL isn't too short
|
|
362
|
+
- Look for query string variations
|
|
363
|
+
- Monitor cache expiry rate
|
|
364
|
+
|
|
365
|
+
**Memory issues?**
|
|
366
|
+
- Reduce TTL
|
|
367
|
+
- Add cache size limits
|
|
368
|
+
- Use Redis instead of in-memory
|
|
369
|
+
- Skip large responses
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Response Compression Plugin
|
|
2
|
+
|
|
3
|
+
Automatically compresses HTTP responses using gzip to reduce bandwidth usage by 60-80%.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'aris/plugins/compression'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
compression = Aris::Plugins::Compression.build
|
|
15
|
+
|
|
16
|
+
Aris.routes({
|
|
17
|
+
"api.example.com": {
|
|
18
|
+
use: [compression], # Apply to all routes in domain
|
|
19
|
+
"/data": { get: { to: DataHandler } }
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
| Option | Type | Default | Description |
|
|
27
|
+
|--------|------|---------|-------------|
|
|
28
|
+
| `level` | Integer | `Zlib::DEFAULT_COMPRESSION` | Compression level (0-9, higher = better compression but slower) |
|
|
29
|
+
| `min_size` | Integer | `1024` | Minimum response size in bytes to compress |
|
|
30
|
+
|
|
31
|
+
### Compression Levels
|
|
32
|
+
|
|
33
|
+
- `Zlib::NO_COMPRESSION` (0) - No compression
|
|
34
|
+
- `Zlib::BEST_SPEED` (1) - Fastest, least compression
|
|
35
|
+
- `Zlib::DEFAULT_COMPRESSION` (6) - Balanced (default)
|
|
36
|
+
- `Zlib::BEST_COMPRESSION` (9) - Slowest, best compression
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
### Default Compression
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
compression = Aris::Plugins::Compression.build
|
|
44
|
+
|
|
45
|
+
# Compresses responses > 1KB with default level
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### High Compression
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
compression = Aris::Plugins::Compression.build(
|
|
52
|
+
level: Zlib::BEST_COMPRESSION, # Maximum compression
|
|
53
|
+
min_size: 512 # Compress anything > 512 bytes
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Fast Compression
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
compression = Aris::Plugins::Compression.build(
|
|
61
|
+
level: Zlib::BEST_SPEED, # Fast compression
|
|
62
|
+
min_size: 2048 # Only compress larger responses
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Selective Compression
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Compress API responses but not static assets
|
|
70
|
+
api_compression = Aris::Plugins::Compression.build
|
|
71
|
+
|
|
72
|
+
Aris.routes({
|
|
73
|
+
"api.example.com": {
|
|
74
|
+
use: [api_compression],
|
|
75
|
+
"/users": { get: { to: UsersHandler } }
|
|
76
|
+
},
|
|
77
|
+
"static.example.com": {
|
|
78
|
+
use: nil, # No compression for static domain
|
|
79
|
+
"/assets/*path": { get: { to: StaticHandler } }
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
1. **Checks client support**: Only compresses if `Accept-Encoding: gzip` header present
|
|
87
|
+
2. **Size threshold**: Skips responses smaller than `min_size` (overhead not worth it)
|
|
88
|
+
3. **content-type filter**: Only compresses text-based types (JSON, HTML, JS, CSS, XML)
|
|
89
|
+
4. **Smart compression**: Skips compression if it makes response larger
|
|
90
|
+
5. **Header management**: Sets `Content-Encoding: gzip`, adds `Vary: Accept-Encoding`
|
|
91
|
+
|
|
92
|
+
## Compressible Content Types
|
|
93
|
+
|
|
94
|
+
Automatically compresses:
|
|
95
|
+
- `text/*` (HTML, CSS, plain text)
|
|
96
|
+
- `application/json`
|
|
97
|
+
- `application/javascript`
|
|
98
|
+
- `application/xml`
|
|
99
|
+
- `application/xhtml+xml`
|
|
100
|
+
|
|
101
|
+
Binary content (images, video, PDFs) is skipped.
|
|
102
|
+
|
|
103
|
+
## Production Tips
|
|
104
|
+
|
|
105
|
+
### 1. Tune Compression Level
|
|
106
|
+
|
|
107
|
+
**High traffic, CPU-bound:**
|
|
108
|
+
```ruby
|
|
109
|
+
Compression.build(level: Zlib::BEST_SPEED) # Faster, less CPU
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Bandwidth-constrained:**
|
|
113
|
+
```ruby
|
|
114
|
+
Compression.build(level: Zlib::BEST_COMPRESSION) # Smaller, more CPU
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Balanced (recommended):**
|
|
118
|
+
```ruby
|
|
119
|
+
Compression.build # Default level 6
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 2. Adjust Minimum Size
|
|
123
|
+
|
|
124
|
+
Small responses have compression overhead:
|
|
125
|
+
```ruby
|
|
126
|
+
# Conservative (default)
|
|
127
|
+
Compression.build(min_size: 1024)
|
|
128
|
+
|
|
129
|
+
# Aggressive (compress more)
|
|
130
|
+
Compression.build(min_size: 512)
|
|
131
|
+
|
|
132
|
+
# Very conservative (only large responses)
|
|
133
|
+
Compression.build(min_size: 4096)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Order in Plugin Chain
|
|
137
|
+
|
|
138
|
+
Place **after** plugins that modify body, **before** logging:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Aris.routes({
|
|
142
|
+
"api.example.com": {
|
|
143
|
+
use: [
|
|
144
|
+
json_parser, # Parse request body
|
|
145
|
+
bearer_auth, # Authenticate
|
|
146
|
+
compression, # ← Compress response (late in chain)
|
|
147
|
+
request_logger # Log (sees compressed size)
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 4. CDN Compatibility
|
|
154
|
+
|
|
155
|
+
If using a CDN that compresses:
|
|
156
|
+
```ruby
|
|
157
|
+
# Let CDN handle it
|
|
158
|
+
use: nil
|
|
159
|
+
|
|
160
|
+
# Or compress at origin for edge caching
|
|
161
|
+
compression = Compression.build(level: Zlib::BEST_COMPRESSION)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 5. Monitoring
|
|
165
|
+
|
|
166
|
+
Track compression ratio:
|
|
167
|
+
```ruby
|
|
168
|
+
# Before compression
|
|
169
|
+
original_size = response.body.join.bytesize
|
|
170
|
+
|
|
171
|
+
# After compression (in logs)
|
|
172
|
+
compressed_size = response.body.first.bytesize
|
|
173
|
+
|
|
174
|
+
ratio = (1 - compressed_size.to_f / original_size) * 100
|
|
175
|
+
# Typical: 70-80% reduction for JSON/text
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Benchmarks
|
|
179
|
+
|
|
180
|
+
Typical compression ratios:
|
|
181
|
+
- JSON APIs: 75-85% reduction
|
|
182
|
+
- HTML pages: 65-75% reduction
|
|
183
|
+
- JavaScript: 60-70% reduction
|
|
184
|
+
- Plain text: 50-70% reduction
|
|
185
|
+
|
|
186
|
+
Performance impact:
|
|
187
|
+
- Level 1: ~0.1ms overhead per response
|
|
188
|
+
- Level 6: ~0.5ms overhead per response
|
|
189
|
+
- Level 9: ~2ms overhead per response
|
|
190
|
+
|
|
191
|
+
*Based on 10KB responses. YMMV.*
|
|
192
|
+
|
|
193
|
+
## Notes
|
|
194
|
+
|
|
195
|
+
- Compression happens in-memory (entire response buffered)
|
|
196
|
+
- Already-compressed content (images, video) is skipped automatically
|
|
197
|
+
- `Content-Length` header is removed (server recalculates)
|
|
198
|
+
- `Vary: Accept-Encoding` header ensures proper caching
|
|
199
|
+
- Thread-safe (no shared state)
|
|
200
|
+
|
|
201
|
+
## Troubleshooting
|
|
202
|
+
|
|
203
|
+
**Compression not working?**
|
|
204
|
+
- Check `Accept-Encoding` header includes `gzip`
|
|
205
|
+
- Verify response is > `min_size`
|
|
206
|
+
- Confirm `content-type` is compressible
|
|
207
|
+
- Check if compression actually saves space
|
|
208
|
+
|
|
209
|
+
**High CPU usage?**
|
|
210
|
+
- Lower compression level: `level: Zlib::BEST_SPEED`
|
|
211
|
+
- Increase minimum size: `min_size: 2048`
|
|
212
|
+
- Profile with different levels
|
|
213
|
+
|
|
214
|
+
**Wrong compressed output?**
|
|
215
|
+
- Ensure no plugins modify body after compression
|
|
216
|
+
- Verify no double-compression (CDN + origin)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Cookies
|
|
2
|
+
|
|
3
|
+
Reading and writing cookies needs no plugin since 1.5 — the helpers are on every request and response. (`use: [:cookies]` still works; it is a no-op kept for existing apps.)
|
|
4
|
+
|
|
5
|
+
## Reading
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
request.cookies # => { 'theme' => 'dark', ... } parsed from the Cookie header
|
|
9
|
+
request.cookies['theme']
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Writing
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
response.set_cookie('theme', 'dark')
|
|
16
|
+
response.set_cookie('remember', token, max_age: 30 * 24 * 3600, httponly: true, secure: true, same_site: :lax)
|
|
17
|
+
response.delete_cookie('remember')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Options: `path`, `domain`, `max_age` (seconds), `expires` (a `Time`), `httponly`, `secure`, `same_site` (`:lax`, `:strict`, `:none`). Anything you do not pass comes from `Aris::Config.cookie_options`:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
Aris.configure do |c|
|
|
24
|
+
c.cookie_options = { httponly: true, secure: true, same_site: :lax, path: '/' }
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Rack 3 headers
|
|
29
|
+
|
|
30
|
+
Cookies are emitted under the lowercase `set-cookie` header. Several cookies in one response become an **Array** of values, which is what Rack 3 requires — a comma-joined string (what 1.4 produced) is one malformed cookie to a browser. The Rack adapter also lowercases every other response header name for the same reason.
|