-
Notifications
You must be signed in to change notification settings - Fork 61
/
default-request-executor.js
125 lines (108 loc) · 4.74 KB
/
default-request-executor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*!
* Copyright (c) 2018-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/
const RequestExecutor = require('./request-executor');
const deepCopy = require('deep-copy');
class DefaultRequestExecutor extends RequestExecutor {
constructor(config = {}) {
super();
if (config.maxRetries && config.maxRetries < 0) {
throw new Error(`okta.client.rateLimit.maxRetries provided as ${config.maxRetries} but must be 0 (disabled) or greater than zero`);
}
if (config.requestTimeout && config.requestTimeout < 0) {
throw new Error(`okta.client.rateLimit.requestTimeout provided as ${config.requestTimeout} but must be 0 (disabled) or greater than zero`);
}
this.requestTimeout = config.requestTimeout || 0;
this.maxRetries = config.maxRetries === undefined ? 2 : config.maxRetries;
this.retryCountHeader = 'X-Okta-Retry-Count';
this.retryForHeader = 'X-Okta-Retry-For';
}
buildRetryRequest(request, requestId, delayMs) {
const elapsedMs = Date.now() - request.startTime;
const newRequest = deepCopy(request);
newRequest.timeout = this.requestTimeout > 0 ? this.requestTimeout - elapsedMs - delayMs : 0;
if (!newRequest.headers) {
newRequest.headers = {};
}
if (!newRequest.headers[this.retryForHeader]) {
newRequest.headers[this.retryForHeader] = requestId;
}
newRequest.headers[this.retryCountHeader] =
newRequest.headers[this.retryCountHeader] ?
newRequest.headers[this.retryCountHeader] + 1 : 1;
return newRequest;
}
validateRetryResponseHeaders(response) {
// Validate that we don't have duplicate headers, see OKTA-112507
// Duplicate headers are returned by fetch as a comma separated list.
const retryHeader = this.getRateLimitReset(response);
return !!(retryHeader && !retryHeader.includes(','));
}
fetch(request) {
if (!request.startTime) {
request.startTime = new Date();
request.timeout = this.requestTimeout;
}
return super.fetch(request).then(this.parseResponse.bind(this, request));
}
getOktaRequestId(response) {
return response.headers.get('x-okta-request-id');
}
getRateLimitReset(response) {
return response.headers.get('x-rate-limit-reset');
}
getResponseDate(response) {
return response.headers.get('date');
}
getRetryDelayMs(response) {
// Determine wait time by getting the delta X-Rate-Limit-Reset and the Date header
// Add 1 second to account for sub second differences between the clocks that create these headers
const nowDate = new Date(this.getResponseDate(response));
const retryDate = new Date(parseInt(this.getRateLimitReset(response), 10) * 1000);
return retryDate.getTime() - nowDate.getTime() + 1000;
}
parseResponse(request, response) {
if (response.status === 429 && this.validateRetryResponseHeaders(response) && !(this.maxRetriesReached(request))) {
const elapsedMs = Date.now() - request.startTime;
const delayMs = this.getRetryDelayMs(response);
const delayDelta = elapsedMs + delayMs;
if (this.requestTimeout > 0) {
if (elapsedMs >= this.requestTimeout) {
return Promise.reject(new Error('HTTP request time exceeded okta.client.rateLimit.requestTimeout'));
}
if (delayDelta >= this.requestTimeout) {
return Promise.reject(new Error('HTTP 429 retry delay would exceed okta.client.rateLimit.requestTimeout'));
}
}
return this.retryRequest(request, response, delayMs);
}
return response;
}
maxRetriesReached(request) {
if (this.maxRetries === 0) {
return true;
}
const retryCount = request.headers && request.headers[this.retryCountHeader];
return retryCount && parseInt(retryCount, 10) >= this.maxRetries;
}
retryRequest(request, response, delayMs) {
const requestId = this.getOktaRequestId(response);
return new Promise(resolve => {
this.emit('backoff', request, response, requestId, delayMs);
setTimeout(resolve, delayMs);
}).then(() => {
const newRequest = this.buildRetryRequest(request, requestId, delayMs);
this.emit('resume', newRequest, requestId);
return this.fetch(newRequest);
});
}
}
module.exports.DefaultRequestExecutor = DefaultRequestExecutor;