soajs.controller 4.3.3 → 4.3.5
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.
- package/Gruntfile.js +1 -1
- package/docs/configuration-custom-registry.md +149 -9
- package/docs/middleware/cache.md +237 -0
- package/docs/middleware/idempotency.md +209 -0
- package/docs/request-flow.md +35 -4
- package/lib/request.js +5 -0
- package/mw/cache/index.js +150 -0
- package/mw/cache/model/memory.js +117 -0
- package/mw/cache/model/mongo.js +98 -0
- package/mw/gotoService/redirectToService.js +77 -16
- package/mw/idempotency/index.js +171 -0
- package/mw/idempotency/model/memory.js +128 -0
- package/mw/idempotency/model/mongo.js +116 -0
- package/mw/mt/lib.js +17 -17
- package/mw/soajsRoutes/keyACL/index.js +1 -1
- package/package.json +3 -3
- package/server/controller.js +14 -0
- package/test/integration/UC4-multiuser-acl/readme.md +11 -0
- package/test/unit/index.js +4 -0
- package/test/unit/mw/cache/index.js +461 -0
- package/test/unit/mw/cache/memory.js +143 -0
- package/test/unit/mw/idempotency/index.js +354 -0
- package/test/unit/mw/idempotency/memory.js +151 -0
package/Gruntfile.js
CHANGED
|
@@ -16,6 +16,13 @@ registry.custom
|
|
|
16
16
|
│ │ ├── model # Throttling storage backend
|
|
17
17
|
│ │ ├── ip2ban # Banned IP addresses
|
|
18
18
|
│ │ └── throttling # Service-level rate limiting
|
|
19
|
+
│ ├── idempotency # Idempotency key configuration
|
|
20
|
+
│ │ ├── model # Storage backend (memory/mongo)
|
|
21
|
+
│ │ └── [service] # Per-service idempotency config
|
|
22
|
+
│ ├── cache # GET response caching
|
|
23
|
+
│ │ ├── model # Storage backend (memory/mongo)
|
|
24
|
+
│ │ ├── defaultTTL # Default cache TTL
|
|
25
|
+
│ │ └── [service] # Per-service cache config
|
|
19
26
|
│ ├── maintenanceMode # Gateway maintenance mode
|
|
20
27
|
│ ├── lastSeen # User activity tracking
|
|
21
28
|
│ └── gotoService
|
|
@@ -122,7 +129,119 @@ Configures rate limiting strategies and storage backend.
|
|
|
122
129
|
|
|
123
130
|
---
|
|
124
131
|
|
|
125
|
-
## 4.
|
|
132
|
+
## 4. Idempotency
|
|
133
|
+
|
|
134
|
+
**Path:** `registry.custom.gateway.value.idempotency`
|
|
135
|
+
|
|
136
|
+
**Middleware:** `mw/idempotency/index.js`
|
|
137
|
+
|
|
138
|
+
Prevents duplicate write operations by tracking requests via `Idempotency-Key` headers. When a client retries a request with the same key, the cached response is returned without re-executing the operation.
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
{
|
|
142
|
+
"idempotency": {
|
|
143
|
+
"model": "memory", // Storage: "memory" or "mongo"
|
|
144
|
+
"av": { // Service name
|
|
145
|
+
"enabled": true, // Enable for this service
|
|
146
|
+
"ttl": 60000, // Key expiration (ms)
|
|
147
|
+
"apis": [ // APIs to protect
|
|
148
|
+
"POST /av/calls",
|
|
149
|
+
"PUT /av/call/p2p/ended",
|
|
150
|
+
"PUT /av/call/p2p/rejected"
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"payment": {
|
|
154
|
+
"enabled": true,
|
|
155
|
+
"ttl": 120000,
|
|
156
|
+
"apis": [
|
|
157
|
+
"POST /payment/charge",
|
|
158
|
+
"POST /payment/refund"
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| Field | Type | Default | Description |
|
|
166
|
+
|-------|------|---------|-------------|
|
|
167
|
+
| `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
|
|
168
|
+
| `[service].enabled` | boolean | `false` | Enable idempotency for this service |
|
|
169
|
+
| `[service].ttl` | number | `60000` | Key expiration time in milliseconds |
|
|
170
|
+
| `[service].apis` | string[] | `[]` | API patterns to protect (e.g., `"POST /path"`) |
|
|
171
|
+
|
|
172
|
+
**Client Usage:**
|
|
173
|
+
```http
|
|
174
|
+
POST /av/calls HTTP/1.1
|
|
175
|
+
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Error Codes:**
|
|
179
|
+
- `180` (400): Invalid Idempotency-Key format (not UUID v4)
|
|
180
|
+
- `181` (409): Request with this key is still being processed
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 5. Response Caching
|
|
185
|
+
|
|
186
|
+
**Path:** `registry.custom.gateway.value.cache`
|
|
187
|
+
|
|
188
|
+
**Middleware:** `mw/cache/index.js`
|
|
189
|
+
|
|
190
|
+
Caches GET API responses with per-API TTL configuration. Returns `X-Cache: HIT` or `X-Cache: MISS` headers.
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
{
|
|
194
|
+
"cache": {
|
|
195
|
+
"model": "memory", // Storage: "memory" or "mongo"
|
|
196
|
+
"defaultTTL": 300000, // Default TTL: 5 minutes
|
|
197
|
+
"av": { // Service name
|
|
198
|
+
"enabled": true, // Enable caching for this service
|
|
199
|
+
"apis": {
|
|
200
|
+
"GET /av/calls": {
|
|
201
|
+
"enabled": true,
|
|
202
|
+
"ttl": 30000 // 30 seconds
|
|
203
|
+
},
|
|
204
|
+
"GET /av/call/:id": {
|
|
205
|
+
"enabled": true,
|
|
206
|
+
"ttl": 60000 // 60 seconds
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"catalog": {
|
|
211
|
+
"enabled": true,
|
|
212
|
+
"apis": {
|
|
213
|
+
"GET /catalog/products": {
|
|
214
|
+
"enabled": true,
|
|
215
|
+
"ttl": 600000 // 10 minutes
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
| Field | Type | Default | Description |
|
|
224
|
+
|-------|------|---------|-------------|
|
|
225
|
+
| `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
|
|
226
|
+
| `defaultTTL` | number | `300000` | Default cache TTL in milliseconds |
|
|
227
|
+
| `[service].enabled` | boolean | `false` | Enable caching for this service |
|
|
228
|
+
| `[service].apis` | object | `{}` | API-specific cache configuration |
|
|
229
|
+
| `[service].apis[api].enabled` | boolean | `false` | Enable caching for this API |
|
|
230
|
+
| `[service].apis[api].ttl` | number | `defaultTTL` | Cache TTL for this specific API |
|
|
231
|
+
|
|
232
|
+
**Response Headers:**
|
|
233
|
+
- `X-Cache: HIT` - Response served from cache
|
|
234
|
+
- `X-Cache: MISS` - Response fetched from backend
|
|
235
|
+
- `X-Cache-Age: <seconds>` - Time since cached (on HIT)
|
|
236
|
+
|
|
237
|
+
**Notes:**
|
|
238
|
+
- Only GET requests are cached
|
|
239
|
+
- Only 2xx responses are cached
|
|
240
|
+
- Cache key includes tenant ID, service name, path, and query hash
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 6. Multi-Tenant IP Whitelist
|
|
126
245
|
|
|
127
246
|
**Path:** `registry.custom.gateway.value.mt.whitelist`
|
|
128
247
|
|
|
@@ -150,7 +269,7 @@ Allows specific IP ranges to bypass ACL and/or OAuth checks.
|
|
|
150
269
|
|
|
151
270
|
---
|
|
152
271
|
|
|
153
|
-
##
|
|
272
|
+
## 7. User Activity Tracking (Last Seen)
|
|
154
273
|
|
|
155
274
|
**Path:** `registry.custom.gateway.value.lastSeen`
|
|
156
275
|
|
|
@@ -180,7 +299,7 @@ Tracks user activity by notifying a service (typically URAC) of user access.
|
|
|
180
299
|
|
|
181
300
|
---
|
|
182
301
|
|
|
183
|
-
##
|
|
302
|
+
## 8. Request Monitoring
|
|
184
303
|
|
|
185
304
|
**Path:** `registry.custom.gateway.value.gotoService.monitor`
|
|
186
305
|
|
|
@@ -208,7 +327,7 @@ Controls which services are monitored for metrics collection.
|
|
|
208
327
|
|
|
209
328
|
---
|
|
210
329
|
|
|
211
|
-
##
|
|
330
|
+
## 9. Request Timeout Renewal
|
|
212
331
|
|
|
213
332
|
**Path:** `registry.custom.gateway.value.gotoService.renewReqMonitorOff`
|
|
214
333
|
|
|
@@ -230,7 +349,7 @@ Disables the heartbeat renewal mechanism for long-running requests.
|
|
|
230
349
|
|
|
231
350
|
---
|
|
232
351
|
|
|
233
|
-
##
|
|
352
|
+
## 10. OAuth Roaming (Cross-Environment)
|
|
234
353
|
|
|
235
354
|
**Path:** `registry.custom.oauth.value.roaming`
|
|
236
355
|
|
|
@@ -260,7 +379,7 @@ Enables cross-environment access for specific services with IP-based whitelistin
|
|
|
260
379
|
|
|
261
380
|
---
|
|
262
381
|
|
|
263
|
-
##
|
|
382
|
+
## 11. PIN Login Wrapper
|
|
264
383
|
|
|
265
384
|
**Path:** `registry.custom.oauth.value.pinWrapper`
|
|
266
385
|
|
|
@@ -284,7 +403,7 @@ Defines a custom PIN-based login endpoint that bypasses standard OAuth flow.
|
|
|
284
403
|
|
|
285
404
|
---
|
|
286
405
|
|
|
287
|
-
##
|
|
406
|
+
## 12. PIN Login API Whitelist
|
|
288
407
|
|
|
289
408
|
**Path:** `registry.custom.oauth.value.pinWhitelist`
|
|
290
409
|
|
|
@@ -359,6 +478,25 @@ Whitelists specific APIs that can be accessed when a user is PIN-locked (during
|
|
|
359
478
|
}
|
|
360
479
|
}
|
|
361
480
|
},
|
|
481
|
+
"idempotency": {
|
|
482
|
+
"model": "mongo",
|
|
483
|
+
"payment": {
|
|
484
|
+
"enabled": true,
|
|
485
|
+
"ttl": 120000,
|
|
486
|
+
"apis": ["POST /payment/charge", "POST /payment/refund"]
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
"cache": {
|
|
490
|
+
"model": "mongo",
|
|
491
|
+
"defaultTTL": 300000,
|
|
492
|
+
"catalog": {
|
|
493
|
+
"enabled": true,
|
|
494
|
+
"apis": {
|
|
495
|
+
"GET /catalog/products": { "enabled": true, "ttl": 600000 },
|
|
496
|
+
"GET /catalog/categories": { "enabled": true, "ttl": 3600000 }
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
},
|
|
362
500
|
"lastSeen": {
|
|
363
501
|
"active": true,
|
|
364
502
|
"serviceName": "urac",
|
|
@@ -417,5 +555,7 @@ Custom registry configurations are processed in this order:
|
|
|
417
555
|
| 4 | gotoService/preRedirect | `gotoService.renewReqMonitorOff` |
|
|
418
556
|
| 5 | gotoService/redirectToService | `gotoService.monitor` |
|
|
419
557
|
| 6 | mt (security checks) | `mt.whitelist`, `oauth.value.pinWrapper`, `oauth.value.pinWhitelist` |
|
|
420
|
-
| 7 |
|
|
421
|
-
| 8 |
|
|
558
|
+
| 7 | idempotency | `idempotency.model`, `idempotency.[service]` |
|
|
559
|
+
| 8 | traffic | `traffic.model`, `traffic.throttling` |
|
|
560
|
+
| 9 | cache | `cache.model`, `cache.defaultTTL`, `cache.[service]` |
|
|
561
|
+
| 10 | lastSeen | `lastSeen` |
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Cache Middleware
|
|
2
|
+
|
|
3
|
+
**File:** `mw/cache/index.js`
|
|
4
|
+
|
|
5
|
+
**Order:** 19 (after Traffic middleware)
|
|
6
|
+
|
|
7
|
+
The Cache middleware provides response caching for GET APIs with per-API TTL configuration.
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
- Caches GET API responses to reduce backend load
|
|
12
|
+
- Supports per-API TTL configuration
|
|
13
|
+
- Provides cache hit/miss headers for debugging
|
|
14
|
+
- Supports memory and MongoDB storage backends
|
|
15
|
+
|
|
16
|
+
## Flow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Incoming Request
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
┌─────────────────────────┐
|
|
23
|
+
│ Check Method │ GET request?
|
|
24
|
+
└────────┬────────────────┘
|
|
25
|
+
│
|
|
26
|
+
├─── Not GET ──▶ next()
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
┌─────────────────────────┐
|
|
30
|
+
│ Check Configuration │ Service enabled? API matches?
|
|
31
|
+
└────────┬────────────────┘
|
|
32
|
+
│
|
|
33
|
+
├─── Not configured ──▶ next()
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
┌─────────────────────────┐
|
|
37
|
+
│ Generate Cache Key │ tenant:service:path:queryHash
|
|
38
|
+
└────────┬────────────────┘
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
┌─────────────────────────┐
|
|
42
|
+
│ Lookup Cache │
|
|
43
|
+
└────────┬────────────────┘
|
|
44
|
+
│
|
|
45
|
+
├─── HIT ──▶ Return cached response (X-Cache: HIT)
|
|
46
|
+
│
|
|
47
|
+
└─── MISS ──▶ Execute → Cache response → Return (X-Cache: MISS)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
**Path:** `registry.custom.gateway.value.cache`
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
{
|
|
56
|
+
"cache": {
|
|
57
|
+
"model": "memory", // Storage: "memory" or "mongo"
|
|
58
|
+
"defaultTTL": 300000, // Default TTL: 5 minutes
|
|
59
|
+
"services": { // Service configurations
|
|
60
|
+
"av": { // Service name
|
|
61
|
+
"enabled": true, // Enable caching for this service
|
|
62
|
+
"apis": {
|
|
63
|
+
"GET /calls": {
|
|
64
|
+
"enabled": true,
|
|
65
|
+
"ttl": 30000 // 30 seconds
|
|
66
|
+
},
|
|
67
|
+
"GET /call/:id": {
|
|
68
|
+
"enabled": true,
|
|
69
|
+
"ttl": 60000 // 60 seconds
|
|
70
|
+
},
|
|
71
|
+
"GET /users": {
|
|
72
|
+
"enabled": true
|
|
73
|
+
// Uses defaultTTL (300000ms)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"catalog": {
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"apis": {
|
|
80
|
+
"GET /products": {
|
|
81
|
+
"enabled": true,
|
|
82
|
+
"ttl": 600000 // 10 minutes
|
|
83
|
+
},
|
|
84
|
+
"GET /categories": {
|
|
85
|
+
"enabled": true,
|
|
86
|
+
"ttl": 3600000 // 1 hour
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Field | Type | Default | Description |
|
|
96
|
+
|-------|------|---------|-------------|
|
|
97
|
+
| `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
|
|
98
|
+
| `defaultTTL` | number | `300000` | Default cache TTL in milliseconds (5 min) |
|
|
99
|
+
| `services.[name].enabled` | boolean | `false` | Enable caching for this service |
|
|
100
|
+
| `services.[name].apis` | object | `{}` | API-specific cache configuration |
|
|
101
|
+
| `services.[name].apis[api].enabled` | boolean | `false` | Enable caching for this API |
|
|
102
|
+
| `services.[name].apis[api].ttl` | number | `defaultTTL` | Cache TTL for this API |
|
|
103
|
+
|
|
104
|
+
## API Pattern Matching
|
|
105
|
+
|
|
106
|
+
APIs are matched using `GET /path` format with support for path parameters:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
"apis": {
|
|
110
|
+
"GET /users": { ... }, // Exact match
|
|
111
|
+
"GET /users/:id": { ... }, // :id matches any segment
|
|
112
|
+
"GET /users/:userId/posts/:postId": { ... } // Multiple params
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Cache Key Structure
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
{
|
|
120
|
+
"l1": "tenant_id",
|
|
121
|
+
"l2": "serviceName:GET:/path:queryHash"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- **tenant_id**: Isolates cache per tenant
|
|
126
|
+
- **serviceName**: The target service name
|
|
127
|
+
- **path**: The API path
|
|
128
|
+
- **queryHash**: MD5 hash of sorted query parameters
|
|
129
|
+
|
|
130
|
+
### Query Parameter Handling
|
|
131
|
+
|
|
132
|
+
Query parameters are included in the cache key via MD5 hash:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
GET /users?page=1&limit=10 → key includes hash of {"limit":"10","page":"1"}
|
|
136
|
+
GET /users?limit=10&page=1 → same hash (sorted)
|
|
137
|
+
GET /users → different key (no query hash)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Storage Backends
|
|
141
|
+
|
|
142
|
+
### Memory (Default)
|
|
143
|
+
|
|
144
|
+
- Fast, in-process storage
|
|
145
|
+
- Lost on restart
|
|
146
|
+
- Good for single-instance deployments
|
|
147
|
+
- Automatic cleanup of expired entries
|
|
148
|
+
|
|
149
|
+
### MongoDB
|
|
150
|
+
|
|
151
|
+
- Persistent storage
|
|
152
|
+
- Shared across instances
|
|
153
|
+
- Required for multi-instance deployments
|
|
154
|
+
- Uses TTL index for automatic expiration
|
|
155
|
+
|
|
156
|
+
## Response Headers
|
|
157
|
+
|
|
158
|
+
| Header | Description |
|
|
159
|
+
|--------|-------------|
|
|
160
|
+
| `X-Cache` | `HIT` or `MISS` indicating cache status |
|
|
161
|
+
| `X-Cache-Age` | Seconds since response was cached (on HIT) |
|
|
162
|
+
|
|
163
|
+
### Cache HIT
|
|
164
|
+
|
|
165
|
+
```http
|
|
166
|
+
HTTP/1.1 200 OK
|
|
167
|
+
X-Cache: HIT
|
|
168
|
+
X-Cache-Age: 15
|
|
169
|
+
Content-Type: application/json
|
|
170
|
+
|
|
171
|
+
{"data": [...]}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Cache MISS
|
|
175
|
+
|
|
176
|
+
```http
|
|
177
|
+
HTTP/1.1 200 OK
|
|
178
|
+
X-Cache: MISS
|
|
179
|
+
Content-Type: application/json
|
|
180
|
+
|
|
181
|
+
{"data": [...]}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Caching Behavior
|
|
185
|
+
|
|
186
|
+
### What Gets Cached
|
|
187
|
+
|
|
188
|
+
- Only successful responses (HTTP 2xx)
|
|
189
|
+
- Response status code
|
|
190
|
+
- Response headers
|
|
191
|
+
- Response body
|
|
192
|
+
|
|
193
|
+
### What Doesn't Get Cached
|
|
194
|
+
|
|
195
|
+
- Non-GET requests
|
|
196
|
+
- Error responses (4xx, 5xx)
|
|
197
|
+
- APIs not configured for caching
|
|
198
|
+
|
|
199
|
+
## Usage Notes
|
|
200
|
+
|
|
201
|
+
- Only GET requests are cached
|
|
202
|
+
- Cache is tenant-isolated
|
|
203
|
+
- Query parameters affect cache key
|
|
204
|
+
- Only 2xx responses are cached
|
|
205
|
+
- Headers like `X-Cache` are not stored in cache
|
|
206
|
+
- MongoDB backend required for horizontal scaling
|
|
207
|
+
- Consider TTL based on data freshness requirements
|
|
208
|
+
|
|
209
|
+
## Example Scenarios
|
|
210
|
+
|
|
211
|
+
### Static Data (Long TTL)
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
"GET /catalog/categories": {
|
|
215
|
+
"enabled": true,
|
|
216
|
+
"ttl": 3600000 // 1 hour - categories rarely change
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Dynamic Data (Short TTL)
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
"GET /av/calls": {
|
|
224
|
+
"enabled": true,
|
|
225
|
+
"ttl": 5000 // 5 seconds - frequently updated
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### User-Specific Data
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// Cache key includes tenant ID, so each tenant has separate cache
|
|
233
|
+
"GET /users/profile": {
|
|
234
|
+
"enabled": true,
|
|
235
|
+
"ttl": 60000 // 1 minute
|
|
236
|
+
}
|
|
237
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Idempotency Middleware
|
|
2
|
+
|
|
3
|
+
**File:** `mw/idempotency/index.js`
|
|
4
|
+
|
|
5
|
+
**Order:** 17 (after MT middleware)
|
|
6
|
+
|
|
7
|
+
The Idempotency middleware prevents duplicate write operations by tracking requests via `Idempotency-Key` headers.
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
- Prevents duplicate side effects from retried requests
|
|
12
|
+
- Stores and replays responses for completed requests
|
|
13
|
+
- Returns 409 Conflict for in-flight duplicate requests
|
|
14
|
+
- Supports memory and MongoDB storage backends
|
|
15
|
+
|
|
16
|
+
## Flow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Incoming Request
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
┌─────────────────────────┐
|
|
23
|
+
│ Check Idempotency-Key │ Header present?
|
|
24
|
+
└────────┬────────────────┘
|
|
25
|
+
│
|
|
26
|
+
├─── Missing ──▶ next() (backwards compatible)
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
┌─────────────────────────┐
|
|
30
|
+
│ Validate UUID v4 │ Format check
|
|
31
|
+
└────────┬────────────────┘
|
|
32
|
+
│
|
|
33
|
+
├─── Invalid ──▶ 400 Bad Request (code: 180)
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
┌─────────────────────────┐
|
|
37
|
+
│ Check Configuration │ Service enabled? API matches?
|
|
38
|
+
└────────┬────────────────┘
|
|
39
|
+
│
|
|
40
|
+
├─── Not configured ──▶ next()
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
┌─────────────────────────┐
|
|
44
|
+
│ Lookup Existing Key │ Check store
|
|
45
|
+
└────────┬────────────────┘
|
|
46
|
+
│
|
|
47
|
+
├─── Found (completed) ──▶ Return cached response
|
|
48
|
+
│
|
|
49
|
+
├─── Found (in_flight) ──▶ 409 Conflict (code: 181)
|
|
50
|
+
│
|
|
51
|
+
└─── Not found ──▶ Lock key → Execute → Store response
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
**Path:** `registry.custom.gateway.value.idempotency`
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
{
|
|
60
|
+
"idempotency": {
|
|
61
|
+
"model": "memory", // Storage: "memory" or "mongo"
|
|
62
|
+
"services": { // Service configurations
|
|
63
|
+
"av": { // Service name
|
|
64
|
+
"enabled": true, // Enable for this service
|
|
65
|
+
"ttl": 60000, // Time-to-live in ms (default: 60s)
|
|
66
|
+
"apis": [ // APIs to protect
|
|
67
|
+
"POST /calls",
|
|
68
|
+
"PUT /call/p2p/ended",
|
|
69
|
+
"PUT /call/p2p/rejected"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"payment": {
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"ttl": 120000,
|
|
75
|
+
"apis": [
|
|
76
|
+
"POST /charge",
|
|
77
|
+
"POST /refund"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Field | Type | Default | Description |
|
|
86
|
+
|-------|------|---------|-------------|
|
|
87
|
+
| `model` | string | `"memory"` | Storage backend: `"memory"` or `"mongo"` |
|
|
88
|
+
| `services.[name].enabled` | boolean | `false` | Enable idempotency for this service |
|
|
89
|
+
| `services.[name].ttl` | number | `60000` | Key expiration time in milliseconds |
|
|
90
|
+
| `services.[name].apis` | string[] | `[]` | API patterns to protect (e.g., `"POST /path"`) |
|
|
91
|
+
|
|
92
|
+
## API Pattern Matching
|
|
93
|
+
|
|
94
|
+
APIs are matched using `METHOD /path` format with support for path parameters:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
"apis": [
|
|
98
|
+
"POST /av/calls", // Exact match
|
|
99
|
+
"PUT /av/call/:id/ended", // :id matches any segment
|
|
100
|
+
"DELETE /users/:userId/posts/:postId" // Multiple params
|
|
101
|
+
]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Storage Backends
|
|
105
|
+
|
|
106
|
+
### Memory (Default)
|
|
107
|
+
|
|
108
|
+
- Fast, in-process storage
|
|
109
|
+
- Lost on restart
|
|
110
|
+
- Good for single-instance deployments
|
|
111
|
+
- Automatic cleanup of expired entries
|
|
112
|
+
|
|
113
|
+
### MongoDB
|
|
114
|
+
|
|
115
|
+
- Persistent storage
|
|
116
|
+
- Shared across instances
|
|
117
|
+
- Required for multi-instance deployments
|
|
118
|
+
- Uses TTL index for automatic expiration
|
|
119
|
+
|
|
120
|
+
## Client Usage
|
|
121
|
+
|
|
122
|
+
Clients must include the `Idempotency-Key` header with a UUID v4:
|
|
123
|
+
|
|
124
|
+
```http
|
|
125
|
+
POST /av/calls HTTP/1.1
|
|
126
|
+
Host: api.example.com
|
|
127
|
+
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
|
|
128
|
+
Content-Type: application/json
|
|
129
|
+
|
|
130
|
+
{"caller": "user1", "callee": "user2"}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Response Scenarios
|
|
134
|
+
|
|
135
|
+
### New Request (First Time)
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
Request processed normally, response cached
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Replay (Completed)
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
HTTP/1.1 200 OK
|
|
145
|
+
(Cached response body returned, no side effects)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Replay (In-Flight)
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
HTTP/1.1 409 Conflict
|
|
152
|
+
Content-Type: application/json
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
"result": false,
|
|
156
|
+
"errors": {
|
|
157
|
+
"codes": [181],
|
|
158
|
+
"details": [{
|
|
159
|
+
"code": 181,
|
|
160
|
+
"message": "Request with this Idempotency-Key is still being processed."
|
|
161
|
+
}]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Invalid Key Format
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
HTTP/1.1 400 Bad Request
|
|
170
|
+
Content-Type: application/json
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
"result": false,
|
|
174
|
+
"errors": {
|
|
175
|
+
"codes": [180],
|
|
176
|
+
"details": [{
|
|
177
|
+
"code": 180,
|
|
178
|
+
"message": "Invalid Idempotency-Key format. Expected UUID v4."
|
|
179
|
+
}]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Key Structure
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
{
|
|
188
|
+
"l1": "tenant_id", // Tenant isolation
|
|
189
|
+
"l2": "idempotency_key" // Client-provided UUID
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Keys are isolated per tenant to prevent cross-tenant collisions.
|
|
194
|
+
|
|
195
|
+
## Error Codes
|
|
196
|
+
|
|
197
|
+
| Code | HTTP Status | Description |
|
|
198
|
+
|------|-------------|-------------|
|
|
199
|
+
| 180 | 400 | Invalid Idempotency-Key format (not UUID v4) |
|
|
200
|
+
| 181 | 409 | Request with this key is still being processed |
|
|
201
|
+
|
|
202
|
+
## Usage Notes
|
|
203
|
+
|
|
204
|
+
- Only applies to non-GET requests (POST, PUT, DELETE, PATCH)
|
|
205
|
+
- GET requests always pass through (idempotent by nature)
|
|
206
|
+
- Keys expire after TTL (default 60 seconds)
|
|
207
|
+
- Clients should generate a new UUID for each unique operation
|
|
208
|
+
- Same UUID = same operation (for retries)
|
|
209
|
+
- MongoDB backend required for horizontal scaling
|