-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathhttp.ts
More file actions
280 lines (247 loc) · 9.41 KB
/
http.ts
File metadata and controls
280 lines (247 loc) · 9.41 KB
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import Defaults from 'common/lib/util/defaults';
import Platform from 'common/platform';
import BaseRealtime from 'common/lib/client/baserealtime';
import HttpMethods from '../constants/HttpMethods';
import BaseClient from '../lib/client/baseclient';
import ErrorInfo, { IPartialErrorInfo } from '../lib/types/errorinfo';
import Logger from 'common/lib/util/logger';
import * as Utils from 'common/lib/util/utils';
export type PathParameter = string | ((host: string) => string);
export type ResponseHeaders = Partial<Record<string, string | string[]>>;
export type RequestResultError = ErrnoException | IPartialErrorInfo;
/**
* The `body`, `headers`, `unpacked`, and `statusCode` properties of a `RequestResult` may be populated even if its `error` property is non-null.
*/
export type RequestResult = {
error: RequestResultError | null;
body?: unknown;
headers?: ResponseHeaders;
unpacked?: boolean;
statusCode?: number;
};
export type RequestParams = Record<string, string> | null;
export type RequestBody =
| Buffer // only on Node
| ArrayBuffer // only on web
| string;
export interface IPlatformHttpStatic {
new (client?: BaseClient): IPlatformHttp;
methods: Array<HttpMethods>;
methodsWithBody: Array<HttpMethods>;
methodsWithoutBody: Array<HttpMethods>;
}
export interface IPlatformHttp {
supportsAuthHeaders: boolean;
supportsLinkHeaders: boolean;
/**
* This method should not throw any errors; rather, it should communicate any error by populating the {@link RequestResult.error} property of the returned {@link RequestResult}.
*/
doUri(
method: HttpMethods,
uri: string,
headers: Record<string, string> | null,
body: RequestBody | null,
params: RequestParams,
): Promise<RequestResult>;
checkConnectivity?: () => Promise<boolean>;
/**
* @param error An error from the {@link RequestResult.error} property of a result returned by {@link doUri}.
*/
shouldFallback(error: RequestResultError): boolean;
}
export function paramString(params: Record<string, any> | null) {
const paramPairs = [];
if (params) {
for (const needle in params) {
paramPairs.push(needle + '=' + params[needle]);
}
}
return paramPairs.join('&');
}
export function appendingParams(uri: string, params: Record<string, any> | null) {
return uri + (params ? '?' : '') + paramString(params);
}
function logResult(
result: RequestResult,
method: HttpMethods,
uri: string,
params: Record<string, string> | null,
logger: Logger,
) {
if (result.error) {
Logger.logActionNoStrip(
logger,
Logger.LOG_MICRO,
'Http.' + method + '()',
'Received Error; ' + appendingParams(uri, params) + '; Error: ' + Utils.inspectError(result.error),
);
} else {
Logger.logActionNoStrip(
logger,
Logger.LOG_MICRO,
'Http.' + method + '()',
'Received; ' +
appendingParams(uri, params) +
'; Headers: ' +
paramString(result.headers as Record<string, any>) +
'; StatusCode: ' +
result.statusCode +
'; Body' +
(Platform.BufferUtils.isBuffer(result.body)
? ' (Base64): ' + Platform.BufferUtils.base64Encode(result.body)
: ': ' + result.body),
);
}
}
function logRequest(method: HttpMethods, uri: string, body: RequestBody | null, params: RequestParams, logger: Logger) {
if (logger.shouldLog(Logger.LOG_MICRO)) {
Logger.logActionNoStrip(
logger,
Logger.LOG_MICRO,
'Http.' + method + '()',
'Sending; ' +
appendingParams(uri, params) +
'; Body' +
(Platform.BufferUtils.isBuffer(body) ? ' (Base64): ' + Platform.BufferUtils.base64Encode(body) : ': ' + body),
);
}
}
export class Http {
private readonly platformHttp: IPlatformHttp;
checkConnectivity?: () => Promise<boolean>;
constructor(private readonly client?: BaseClient) {
this.platformHttp = new Platform.Http(client);
this.checkConnectivity = this.platformHttp.checkConnectivity
? () => this.platformHttp.checkConnectivity!()
: undefined;
}
get logger(): Logger {
return this.client?.logger ?? Logger.defaultLogger;
}
get supportsAuthHeaders() {
return this.platformHttp.supportsAuthHeaders;
}
get supportsLinkHeaders() {
return this.platformHttp.supportsLinkHeaders;
}
_getHosts(client: BaseClient) {
/* If we're a connected realtime client, try the endpoint we're connected
* to first -- but still have fallbacks, being connected is not an absolute
* guarantee that a datacenter has free capacity to service REST requests. */
const connection = (client as BaseRealtime).connection,
connectionHost = connection && connection.connectionManager.host;
if (connectionHost) {
return [connectionHost].concat(Defaults.getFallbackHosts(client.options));
}
return Defaults.getHosts(client.options);
}
/**
* This method will not throw any errors; rather, it will communicate any error by populating the {@link RequestResult.error} property of the returned {@link RequestResult}.
*/
async do(
method: HttpMethods,
path: PathParameter,
headers: Record<string, string> | null,
body: RequestBody | null,
params: RequestParams,
): Promise<RequestResult> {
try {
/* Unlike for doUri, the presence of `this.client` here is mandatory, as it's used to generate the hosts */
const client = this.client;
if (!client) {
return { error: new ErrorInfo('http.do called without client', 50000, 500) };
}
const uriFromHost =
typeof path === 'function'
? path
: function (host: string) {
return client.baseUri(host) + path;
};
const currentFallback = client._currentFallback;
if (currentFallback) {
if (currentFallback.validUntil > Date.now()) {
/* Use stored fallback */
const result = await this.doUri(method, uriFromHost(currentFallback.host), headers, body, params);
if (result.error && this.platformHttp.shouldFallback(result.error as ErrnoException)) {
/* unstore the fallback and start from the top with the default sequence */
client._currentFallback = null;
return this.do(method, path, headers, body, params);
}
return result;
} else {
/* Fallback expired; remove it and fallthrough to normal sequence */
client._currentFallback = null;
}
}
const hosts = this._getHosts(client);
/* see if we have one or more than one host */
if (hosts.length === 1) {
return this.doUri(method, uriFromHost(hosts[0]), headers, body, params);
}
let tryAHostStartedAt: Date | null = null;
const tryAHost = async (candidateHosts: Array<string>, persistOnSuccess?: boolean): Promise<RequestResult> => {
const host = candidateHosts.shift();
tryAHostStartedAt = tryAHostStartedAt ?? new Date();
const result = await this.doUri(method, uriFromHost(host as string), headers, body, params);
if (result.error && this.platformHttp.shouldFallback(result.error as ErrnoException) && candidateHosts.length) {
// TO3l6
const elapsedTime = Date.now() - tryAHostStartedAt.getTime();
if (elapsedTime > client.options.timeouts.httpMaxRetryDuration) {
return {
error: new ErrorInfo(
`Timeout for trying fallback hosts retries. Total elapsed time exceeded the ${client.options.timeouts.httpMaxRetryDuration}ms limit`,
50003,
500,
),
};
}
return tryAHost(candidateHosts, true);
}
if (persistOnSuccess) {
/* RSC15f */
client._currentFallback = {
host: host as string,
validUntil: Date.now() + client.options.timeouts.fallbackRetryTimeout,
};
}
return result;
};
return tryAHost(hosts);
} catch (err) {
// Handle any unexpected error, to ensure we always meet our contract of not throwing any errors
// ably-os:inline-error-update:50000:2025-08-22:e8u Original: "Unexpected error in Http.do: {error details}"
return { error: new ErrorInfo(`Unexpected error in Http.do: ${Utils.inspectError(err)}`, 50000, 500) };
}
}
/**
* This method will not throw any errors; rather, it will communicate any error by populating the {@link RequestResult.error} property of the returned {@link RequestResult}.
*/
async doUri(
method: HttpMethods,
uri: string,
headers: Record<string, string> | null,
body: RequestBody | null,
params: RequestParams,
): Promise<RequestResult> {
try {
logRequest(method, uri, body, params, this.logger);
const result = await this.platformHttp.doUri(method, uri, headers, body, params);
if (this.logger.shouldLog(Logger.LOG_MICRO)) {
logResult(result, method, uri, params, this.logger);
}
return result;
} catch (err) {
// Handle any unexpected error, to ensure we always meet our contract of not throwing any errors
// ably-os:inline-error-update:50000:2025-08-22:e8u Original: "Unexpected error in Http.doUri: {error details}"
return { error: new ErrorInfo(`Unexpected error in Http.doUri: ${Utils.inspectError(err)}`, 50000, 500) };
}
}
}
export interface ErrnoException extends Error {
errno?: number;
code?: string;
path?: string;
syscall?: string;
stack?: string;
statusCode: number;
}