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/docs/request-flow.md
CHANGED
|
@@ -47,13 +47,25 @@ The complete request lifecycle through the SOAJS Controller Gateway.
|
|
|
47
47
|
│ │
|
|
48
48
|
▼ │
|
|
49
49
|
┌──────────────┐ │
|
|
50
|
-
│ 7.
|
|
50
|
+
│ 7.Idempotency│ Check Idempotency-Key header │
|
|
51
|
+
│ Check │ Return cached response or lock key │
|
|
52
|
+
└──────┬───────┘ │
|
|
53
|
+
│ │
|
|
54
|
+
▼ │
|
|
55
|
+
┌──────────────┐ │
|
|
56
|
+
│ 8. Traffic │ Rate limiting with configurable │
|
|
51
57
|
│ Control │ strategies and storage backends │
|
|
58
|
+
└──────┬───────┘ │
|
|
59
|
+
│ │
|
|
60
|
+
▼ │
|
|
61
|
+
┌──────────────┐ │
|
|
62
|
+
│ 9. Response │ Check cache for GET requests │
|
|
63
|
+
│ Caching │ Return cached response or proceed │
|
|
52
64
|
└──────┬───────┘ │
|
|
53
65
|
│ │
|
|
54
66
|
▼ │
|
|
55
67
|
┌──────────────────────────────────────────────────────────────────┬──────┘
|
|
56
|
-
│
|
|
68
|
+
│ 10. PROXY TO SERVICE │
|
|
57
69
|
│ │
|
|
58
70
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
|
|
59
71
|
│ │ Resolve Host │───▶│ Build Request │───▶│ HTTP Proxy │ │
|
|
@@ -120,14 +132,33 @@ The complete request lifecycle through the SOAJS Controller Gateway.
|
|
|
120
132
|
- ACL enforcement
|
|
121
133
|
- OAuth token validation
|
|
122
134
|
|
|
123
|
-
### 7.
|
|
135
|
+
### 7. Idempotency Check
|
|
136
|
+
|
|
137
|
+
**Middleware:** `mw/idempotency/index.js`
|
|
138
|
+
|
|
139
|
+
- Checks for `Idempotency-Key` header (UUID v4)
|
|
140
|
+
- Returns cached response for completed requests
|
|
141
|
+
- Returns 409 Conflict for in-flight requests
|
|
142
|
+
- Locks key and proceeds for new requests
|
|
143
|
+
- Stores response after completion
|
|
144
|
+
|
|
145
|
+
### 8. Traffic Control
|
|
124
146
|
|
|
125
147
|
**Middleware:** `mw/traffic/index.js`
|
|
126
148
|
|
|
127
149
|
- Rate limiting per strategy
|
|
128
150
|
- Response headers (X-RateLimit-*)
|
|
129
151
|
|
|
130
|
-
###
|
|
152
|
+
### 9. Response Caching
|
|
153
|
+
|
|
154
|
+
**Middleware:** `mw/cache/index.js`
|
|
155
|
+
|
|
156
|
+
- Caches GET API responses
|
|
157
|
+
- Returns cached response on cache hit
|
|
158
|
+
- Adds X-Cache and X-Cache-Age headers
|
|
159
|
+
- Stores response after backend call
|
|
160
|
+
|
|
161
|
+
### 10. Proxy to Service
|
|
131
162
|
|
|
132
163
|
**Middleware:** `mw/gotoService/*`
|
|
133
164
|
|
package/lib/request.js
CHANGED
|
@@ -409,6 +409,11 @@ function proxyRequestMonitor(req, res, options, extraOptions = {}) {
|
|
|
409
409
|
}
|
|
410
410
|
let resContentType = proxyRes.headers['content-type'];
|
|
411
411
|
|
|
412
|
+
// Emit statusCode event for caching
|
|
413
|
+
if (extraOptions && extraOptions.events && extraOptions.events.statusCode) {
|
|
414
|
+
extraOptions.events.statusCode(proxyRes.statusCode, proxyRes.headers);
|
|
415
|
+
}
|
|
416
|
+
|
|
412
417
|
if (extraOptions && extraOptions.events && extraOptions.events.response) {
|
|
413
418
|
extraOptions.events.response(resContentType);
|
|
414
419
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
13
|
+
const memory = require('./model/memory.js');
|
|
14
|
+
const mongo = require('./model/mongo.js');
|
|
15
|
+
|
|
16
|
+
let model = { memory, mongo };
|
|
17
|
+
|
|
18
|
+
function hashQuery(query) {
|
|
19
|
+
if (!query || Object.keys(query).length === 0) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
const sortedQuery = Object.keys(query).sort().reduce((obj, key) => {
|
|
23
|
+
obj[key] = query[key];
|
|
24
|
+
return obj;
|
|
25
|
+
}, {});
|
|
26
|
+
return crypto.createHash('md5').update(JSON.stringify(sortedQuery)).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchesApi(method, path, apis) {
|
|
30
|
+
if (!apis || typeof apis !== 'object') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const requestSignature = `${method} ${path}`;
|
|
34
|
+
|
|
35
|
+
if (apis[requestSignature]) {
|
|
36
|
+
return apis[requestSignature];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const api in apis) {
|
|
40
|
+
if (apis.hasOwnProperty(api)) {
|
|
41
|
+
const [apiMethod, apiPath] = api.split(' ');
|
|
42
|
+
if (apiMethod !== method) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const apiParts = apiPath.split('/');
|
|
46
|
+
const pathParts = path.split('/');
|
|
47
|
+
if (apiParts.length !== pathParts.length) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let matches = true;
|
|
51
|
+
for (let i = 0; i < apiParts.length; i++) {
|
|
52
|
+
if (apiParts[i].startsWith(':')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (apiParts[i] !== pathParts[i]) {
|
|
56
|
+
matches = false;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (matches) {
|
|
61
|
+
return apis[api];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Export model for use by gotoService
|
|
69
|
+
module.exports = function (configuration) {
|
|
70
|
+
model.memory.init(configuration.log);
|
|
71
|
+
model.mongo.init(configuration.log, configuration.gatewayDB);
|
|
72
|
+
|
|
73
|
+
return (req, res, next) => {
|
|
74
|
+
if (req.method.toUpperCase() !== 'GET') {
|
|
75
|
+
return next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!req.soajs || !req.soajs.tenant || !req.soajs.controller || !req.soajs.controller.serviceParams) {
|
|
79
|
+
return next();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const serviceName = req.soajs.controller.serviceParams.name;
|
|
83
|
+
// Strip service prefix from pathname to get API path (e.g., /marketplace/items -> /items)
|
|
84
|
+
const serviceInfo = req.soajs.controller.serviceParams.serviceInfo;
|
|
85
|
+
const apiPath = "/" + serviceInfo.slice(2).join("/");
|
|
86
|
+
const method = req.method.toUpperCase();
|
|
87
|
+
|
|
88
|
+
const cacheConfig = get(["soajs", "registry", "custom", "gateway", "value", "cache"], req);
|
|
89
|
+
if (!cacheConfig) {
|
|
90
|
+
return next();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const serviceConfig = cacheConfig.services && cacheConfig.services[serviceName];
|
|
94
|
+
if (!serviceConfig || !serviceConfig.enabled) {
|
|
95
|
+
return next();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const apiConfig = matchesApi(method, apiPath, serviceConfig.apis);
|
|
99
|
+
if (!apiConfig || !apiConfig.enabled) {
|
|
100
|
+
return next();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const modelType = cacheConfig.model || 'memory';
|
|
104
|
+
if (!model[modelType]) {
|
|
105
|
+
configuration.log.warn('Cache: Unknown model [' + modelType + ']. It can only be [memory || mongo]');
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ttl = apiConfig.ttl || cacheConfig.defaultTTL || 300000;
|
|
110
|
+
const queryHash = hashQuery(req.query);
|
|
111
|
+
const key = {
|
|
112
|
+
'l1': req.soajs.tenant.id,
|
|
113
|
+
'l2': `${serviceName}:GET:${apiPath}:${queryHash}`
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const processCache = async () => {
|
|
117
|
+
try {
|
|
118
|
+
const cached = await model[modelType].get(key);
|
|
119
|
+
|
|
120
|
+
if (cached && cached.response) {
|
|
121
|
+
res.setHeader('X-Cache', 'HIT');
|
|
122
|
+
res.setHeader('X-Cache-Age', Math.floor((Date.now() - cached.cachedAt) / 1000));
|
|
123
|
+
res.writeHead(cached.response.statusCode, cached.response.headers);
|
|
124
|
+
res.end(cached.response.body);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Store cache context for gotoService to use
|
|
129
|
+
req.soajs.cacheContext = {
|
|
130
|
+
key: key,
|
|
131
|
+
ttl: ttl,
|
|
132
|
+
modelType: modelType,
|
|
133
|
+
model: model[modelType],
|
|
134
|
+
log: configuration.log
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
res.setHeader('X-Cache', 'MISS');
|
|
138
|
+
next();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
configuration.log.error('Cache error:', err);
|
|
141
|
+
next();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
processCache();
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Export model for external use
|
|
150
|
+
module.exports.model = model;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let dataHolder = {};
|
|
12
|
+
let cleanupInterval = null;
|
|
13
|
+
|
|
14
|
+
let model = {
|
|
15
|
+
'log': null,
|
|
16
|
+
|
|
17
|
+
'init': (log) => {
|
|
18
|
+
model.log = log;
|
|
19
|
+
dataHolder = {};
|
|
20
|
+
|
|
21
|
+
if (cleanupInterval) {
|
|
22
|
+
clearInterval(cleanupInterval);
|
|
23
|
+
}
|
|
24
|
+
const CLEANUP_INTERVAL = process.env.SOAJS_CACHE_CLEANUP_INTERVAL || 60000;
|
|
25
|
+
cleanupInterval = setInterval(() => {
|
|
26
|
+
model.cleanupExpiredEntries();
|
|
27
|
+
}, CLEANUP_INTERVAL);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
'cleanupExpiredEntries': () => {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
let removedCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const l1Key in dataHolder) {
|
|
35
|
+
if (dataHolder.hasOwnProperty(l1Key)) {
|
|
36
|
+
const tenantData = dataHolder[l1Key];
|
|
37
|
+
|
|
38
|
+
for (const l2Key in tenantData) {
|
|
39
|
+
if (tenantData.hasOwnProperty(l2Key)) {
|
|
40
|
+
const entry = tenantData[l2Key];
|
|
41
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
42
|
+
delete tenantData[l2Key];
|
|
43
|
+
removedCount++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Object.keys(tenantData).length === 0) {
|
|
49
|
+
delete dataHolder[l1Key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (removedCount > 0 && model.log) {
|
|
55
|
+
model.log.debug(`Cache cleanup: removed ${removedCount} expired entries`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
'get': (key) => {
|
|
60
|
+
if (!dataHolder[key.l1]) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const entry = dataHolder[key.l1][key.l2];
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
68
|
+
delete dataHolder[key.l1][key.l2];
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return entry;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'set': (key, response, ttl) => {
|
|
75
|
+
if (!dataHolder[key.l1]) {
|
|
76
|
+
dataHolder[key.l1] = {};
|
|
77
|
+
}
|
|
78
|
+
dataHolder[key.l1][key.l2] = {
|
|
79
|
+
'response': response,
|
|
80
|
+
'cachedAt': Date.now(),
|
|
81
|
+
'expiresAt': Date.now() + ttl
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
'invalidate': (key) => {
|
|
86
|
+
if (dataHolder[key.l1] && dataHolder[key.l1][key.l2]) {
|
|
87
|
+
delete dataHolder[key.l1][key.l2];
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
'invalidateTenant': (tenantId) => {
|
|
92
|
+
if (dataHolder[tenantId]) {
|
|
93
|
+
delete dataHolder[tenantId];
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
'shutdown': () => {
|
|
98
|
+
if (cleanupInterval) {
|
|
99
|
+
clearInterval(cleanupInterval);
|
|
100
|
+
cleanupInterval = null;
|
|
101
|
+
}
|
|
102
|
+
dataHolder = {};
|
|
103
|
+
if (model.log) {
|
|
104
|
+
model.log.info('Cache memory model shutdown complete');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (typeof process !== 'undefined') {
|
|
110
|
+
const shutdownHandler = () => {
|
|
111
|
+
model.shutdown();
|
|
112
|
+
};
|
|
113
|
+
process.once('SIGTERM', shutdownHandler);
|
|
114
|
+
process.once('SIGINT', shutdownHandler);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = model;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const core = require('soajs.core.modules');
|
|
12
|
+
const Mongo = core.mongo;
|
|
13
|
+
|
|
14
|
+
const cache_store = 'cache_store';
|
|
15
|
+
|
|
16
|
+
let model = {
|
|
17
|
+
'mongo': null,
|
|
18
|
+
'log': null,
|
|
19
|
+
|
|
20
|
+
'init': (log, dbConfiguration) => {
|
|
21
|
+
model.log = log;
|
|
22
|
+
if (!model.mongo) {
|
|
23
|
+
model.mongo = new Mongo(dbConfiguration);
|
|
24
|
+
model.mongo.createIndex(cache_store, { 'l1': 1, 'l2': 1 }, { unique: true }, (err, index) => {
|
|
25
|
+
log.debug("Cache index: " + index + " created with error: " + err);
|
|
26
|
+
});
|
|
27
|
+
model.mongo.createIndex(cache_store, { 'expiresAt': 1 }, { expireAfterSeconds: 0 }, (err, index) => {
|
|
28
|
+
log.debug("Cache TTL index: " + index + " created with error: " + err);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
'get': async (key) => {
|
|
34
|
+
let condition = {
|
|
35
|
+
'l1': key.l1,
|
|
36
|
+
'l2': key.l2
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const entry = await model.mongo.findOne(cache_store, condition);
|
|
40
|
+
if (!entry) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt.getTime()) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return entry;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
model.log.error('Cache get:', e.message);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
'set': async (key, response, ttl) => {
|
|
54
|
+
let condition = {
|
|
55
|
+
'l1': key.l1,
|
|
56
|
+
'l2': key.l2
|
|
57
|
+
};
|
|
58
|
+
let doc = {
|
|
59
|
+
'l1': key.l1,
|
|
60
|
+
'l2': key.l2,
|
|
61
|
+
'response': response,
|
|
62
|
+
'cachedAt': new Date(),
|
|
63
|
+
'expiresAt': new Date(Date.now() + ttl)
|
|
64
|
+
};
|
|
65
|
+
let options = { 'upsert': true };
|
|
66
|
+
let s = { '$set': doc };
|
|
67
|
+
try {
|
|
68
|
+
await model.mongo.updateOne(cache_store, condition, s, options);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
model.log.error('Cache set:', e.message);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'invalidate': async (key) => {
|
|
75
|
+
let condition = {
|
|
76
|
+
'l1': key.l1,
|
|
77
|
+
'l2': key.l2
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
await model.mongo.deleteOne(cache_store, condition);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
model.log.error('Cache invalidate:', e.message);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
'invalidateTenant': async (tenantId) => {
|
|
87
|
+
let condition = {
|
|
88
|
+
'l1': tenantId
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
await model.mongo.deleteMany(cache_store, condition);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
model.log.error('Cache invalidateTenant:', e.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
module.exports = model;
|
|
@@ -315,7 +315,21 @@ module.exports = (configuration) => {
|
|
|
315
315
|
let resContentType = "";
|
|
316
316
|
const extraOptions = {};
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// Cache response capture
|
|
319
|
+
let cacheContext = req.soajs.cacheContext;
|
|
320
|
+
let cacheResponseChunks = [];
|
|
321
|
+
let cacheResponseStatusCode = 200;
|
|
322
|
+
let cacheResponseHeaders = {};
|
|
323
|
+
|
|
324
|
+
// Idempotency response capture
|
|
325
|
+
let idempotencyContext = req.soajs.idempotencyContext;
|
|
326
|
+
let idempotencyResponseChunks = [];
|
|
327
|
+
let idempotencyResponseStatusCode = 200;
|
|
328
|
+
let idempotencyResponseHeaders = {};
|
|
329
|
+
|
|
330
|
+
// Set up events for monitoring, caching, and/or idempotency
|
|
331
|
+
let needsEvents = (monitor && !monitor_service_blacklist && monitor.req_response) || cacheContext || idempotencyContext;
|
|
332
|
+
if (needsEvents) {
|
|
319
333
|
const MAX_RESPONSE_SIZE = process.env.SOAJS_MAX_RESPONSE_SIZE || 10 * 1024 * 1024; // 10MB default
|
|
320
334
|
let responseSize = 0;
|
|
321
335
|
let responseExceeded = false;
|
|
@@ -330,35 +344,78 @@ module.exports = (configuration) => {
|
|
|
330
344
|
isStream = resContentType.match(/stream/i);
|
|
331
345
|
}
|
|
332
346
|
}
|
|
333
|
-
|
|
347
|
+
if (monitor && !monitor_service_blacklist && monitor.req_response) {
|
|
348
|
+
monitoObj.time.res_start = new Date().getTime();
|
|
349
|
+
}
|
|
334
350
|
},
|
|
335
351
|
"data": (chunk) => {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
// Capture for caching
|
|
353
|
+
if (cacheContext) {
|
|
354
|
+
cacheResponseChunks.push(chunk);
|
|
355
|
+
}
|
|
356
|
+
// Capture for idempotency
|
|
357
|
+
if (idempotencyContext) {
|
|
358
|
+
idempotencyResponseChunks.push(chunk);
|
|
359
|
+
}
|
|
360
|
+
// Capture for monitoring
|
|
361
|
+
if (monitor && !monitor_service_blacklist && monitor.req_response) {
|
|
362
|
+
if (!isStream && allowedContentType) {
|
|
363
|
+
responseSize += chunk.length;
|
|
364
|
+
if (responseSize > MAX_RESPONSE_SIZE) {
|
|
365
|
+
if (!responseExceeded) {
|
|
366
|
+
responseExceeded = true;
|
|
367
|
+
monitoObj.response = "{\"error\": \"Response body exceeds maximum size limit\"}";
|
|
368
|
+
}
|
|
369
|
+
} else if (!responseExceeded) {
|
|
370
|
+
if (!monitoObj.response) {
|
|
371
|
+
monitoObj.response = chunk;
|
|
372
|
+
} else {
|
|
373
|
+
monitoObj.response += chunk;
|
|
374
|
+
}
|
|
348
375
|
}
|
|
376
|
+
} else {
|
|
377
|
+
monitoObj.response = "{\"contentType\": \"is stream or not allowed\", \"value': \"" + resContentType + "\"}";
|
|
349
378
|
}
|
|
350
|
-
} else {
|
|
351
|
-
monitoObj.response = "{\"contentType\": \"is stream or not allowed\", \"value': \"" + resContentType + "\"}";
|
|
352
379
|
}
|
|
353
380
|
},
|
|
354
381
|
"fetchProxReq": (proxyReq) => {
|
|
355
382
|
req.soajs.controller.redirectedRequest = proxyReq;
|
|
383
|
+
},
|
|
384
|
+
"statusCode": (statusCode, headers) => {
|
|
385
|
+
if (cacheContext) {
|
|
386
|
+
cacheResponseStatusCode = statusCode;
|
|
387
|
+
cacheResponseHeaders = headers || {};
|
|
388
|
+
}
|
|
389
|
+
if (idempotencyContext) {
|
|
390
|
+
idempotencyResponseStatusCode = statusCode;
|
|
391
|
+
idempotencyResponseHeaders = headers || {};
|
|
392
|
+
}
|
|
356
393
|
}
|
|
357
394
|
};
|
|
358
395
|
}
|
|
359
396
|
|
|
360
397
|
proxyRequestMonitor(req, res, requestOptions, extraOptions)
|
|
361
398
|
.then(() => {
|
|
399
|
+
// Save to cache if applicable
|
|
400
|
+
if (cacheContext && cacheResponseStatusCode >= 200 && cacheResponseStatusCode < 300) {
|
|
401
|
+
let cacheBody = Buffer.concat(cacheResponseChunks).toString('utf8');
|
|
402
|
+
let cacheData = {
|
|
403
|
+
statusCode: cacheResponseStatusCode,
|
|
404
|
+
headers: cacheResponseHeaders,
|
|
405
|
+
body: cacheBody
|
|
406
|
+
};
|
|
407
|
+
cacheContext.model.set(cacheContext.key, cacheData, cacheContext.ttl);
|
|
408
|
+
}
|
|
409
|
+
// Complete idempotency if applicable
|
|
410
|
+
if (idempotencyContext) {
|
|
411
|
+
let idempotencyBody = Buffer.concat(idempotencyResponseChunks).toString('utf8');
|
|
412
|
+
let idempotencyData = {
|
|
413
|
+
statusCode: idempotencyResponseStatusCode,
|
|
414
|
+
headers: idempotencyResponseHeaders,
|
|
415
|
+
body: idempotencyBody
|
|
416
|
+
};
|
|
417
|
+
idempotencyContext.model.complete(idempotencyContext.key, idempotencyData, idempotencyContext.ttl);
|
|
418
|
+
}
|
|
362
419
|
if (monitor && !monitor_service_blacklist && monitor.req_response) {
|
|
363
420
|
monitoObj.time.res_end = new Date().getTime();
|
|
364
421
|
log_monitor(monitoObj);
|
|
@@ -370,6 +427,10 @@ module.exports = (configuration) => {
|
|
|
370
427
|
req.soajs.controller.redirectedRequest.destroy();
|
|
371
428
|
req.soajs.controller.redirectedRequest = null;
|
|
372
429
|
}
|
|
430
|
+
// Unlock idempotency on error
|
|
431
|
+
if (idempotencyContext) {
|
|
432
|
+
idempotencyContext.model.unlock(idempotencyContext.key);
|
|
433
|
+
}
|
|
373
434
|
if (!req.soajs.controller.monitorEndingReq) {
|
|
374
435
|
req.soajs.controllerResponse(core.error.getError(135));
|
|
375
436
|
}
|