@zlink-systems/http-client 0.26.0 → 0.28.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser/index.mjs +357 -241
- package/dist/client.d.ts +1 -1
- package/dist/client.js +10 -7
- package/dist/request-builder.d.ts +2 -1
- package/dist/request-builder.js +29 -23
- package/dist/runtime/bounded-response-body.d.ts +2 -0
- package/dist/runtime/bounded-response-body.js +16 -0
- package/dist/runtime/browser-runtime.js +91 -35
- package/dist/runtime/compression.d.ts +5 -0
- package/dist/runtime/compression.js +10 -3
- package/dist/runtime/cookie-jar.js +9 -3
- package/dist/runtime/redirect-policy.js +11 -2
- package/dist/runtime/request-performer.js +65 -25
- package/dist/runtime/response-body-reader.js +12 -22
- package/dist/runtime/retry-policy.d.ts +7 -1
- package/dist/runtime/retry-policy.js +21 -10
- package/dist/runtime/runtime.js +1 -1
- package/dist/runtime/text.d.ts +17 -0
- package/dist/runtime/text.js +26 -1
- package/package.json +2 -2
package/dist/browser/index.mjs
CHANGED
|
@@ -62,222 +62,24 @@ var ZLINK_PACKET_JSON_CONTRACTS = Symbol.for("@zlink-systems/framework:packet-js
|
|
|
62
62
|
// packages/framework/src/contracts/Handlers/Attributes.ts
|
|
63
63
|
var ZLINK_DECORATOR_METADATA = Symbol.for("@zlink-systems/framework:decorator");
|
|
64
64
|
|
|
65
|
-
// packages/http-client/src/runtime/http-client-errors.ts
|
|
66
|
-
function responseBodySizeExceeded(message = "HTTP response exceeded the maximum body size") {
|
|
67
|
-
return new ZLinkFrameworkException(4 /* Rejected */, message);
|
|
68
|
-
}
|
|
69
|
-
function redirectLimitExceeded(message = "HTTP request exceeded the redirect limit") {
|
|
70
|
-
return new ZLinkFrameworkException(8 /* ProtocolError */, message);
|
|
71
|
-
}
|
|
72
|
-
function redirectFormatError(message) {
|
|
73
|
-
return new ZLinkFrameworkException(8 /* ProtocolError */, message);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// packages/http-client/src/runtime/redirect-policy.ts
|
|
77
|
-
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
78
|
-
function isRedirectStatus(status) {
|
|
79
|
-
return REDIRECT_STATUSES.has(status);
|
|
80
|
-
}
|
|
81
|
-
function makeTarget(prefix, path) {
|
|
82
|
-
if (prefix.length === 0 || prefix === "/") {
|
|
83
|
-
return path;
|
|
84
|
-
}
|
|
85
|
-
return prefix.endsWith("/") ? prefix.slice(0, -1) + path : prefix + path;
|
|
86
|
-
}
|
|
87
|
-
function rewriteForRedirect(status, method, body) {
|
|
88
|
-
if (status === 303 || (status === 301 || status === 302) && method === "POST") {
|
|
89
|
-
return { method: "GET", body: void 0 };
|
|
90
|
-
}
|
|
91
|
-
return { method, body };
|
|
92
|
-
}
|
|
93
|
-
function resolveLocation(current, location) {
|
|
94
|
-
try {
|
|
95
|
-
if (location.startsWith("http://") || location.startsWith("https://")) {
|
|
96
|
-
return new URL(location);
|
|
97
|
-
}
|
|
98
|
-
if (location.startsWith("//")) {
|
|
99
|
-
return new URL(current.protocol + location);
|
|
100
|
-
}
|
|
101
|
-
if (location.startsWith("/")) {
|
|
102
|
-
return new URL(current.origin + location);
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
throw redirectFormatError(`HTTP redirect location is malformed: ${location}`);
|
|
106
|
-
}
|
|
107
|
-
throw redirectFormatError(`HTTP redirect location is not supported: ${location}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// packages/http-client/src/runtime/retry-policy.ts
|
|
111
|
-
function delayMsFor(attempt) {
|
|
112
|
-
const ceiling = Math.min(1e3, 50 << Math.min(attempt, 5));
|
|
113
|
-
return Math.floor(Math.random() * (ceiling + 1));
|
|
114
|
-
}
|
|
115
|
-
var RetryPolicy = class {
|
|
116
|
-
constructor(options) {
|
|
117
|
-
this.options = options;
|
|
118
|
-
}
|
|
119
|
-
async execute(spec, perform) {
|
|
120
|
-
const maxRetries = spec.sink !== void 0 || spec.bodyProvider !== void 0 ? 0 : this.options.retryAttempts;
|
|
121
|
-
const timeoutMs = spec.timeoutMs ?? this.options.timeoutMs;
|
|
122
|
-
for (let attempt = 0; ; attempt++) {
|
|
123
|
-
const controller = new AbortController();
|
|
124
|
-
const timer = setTimeout(() => {
|
|
125
|
-
controller.abort();
|
|
126
|
-
}, timeoutMs);
|
|
127
|
-
try {
|
|
128
|
-
return await perform(spec, controller.signal);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
const failure = mapFailure(error, controller.signal.aborted);
|
|
131
|
-
if (isRetriableHttpFailure(failure) && attempt < maxRetries) {
|
|
132
|
-
await delay(delayMsFor(attempt));
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
throw failure;
|
|
136
|
-
} finally {
|
|
137
|
-
clearTimeout(timer);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
function mapFailure(error, aborted) {
|
|
143
|
-
if (error instanceof ZLinkFrameworkException) {
|
|
144
|
-
return error;
|
|
145
|
-
}
|
|
146
|
-
if (aborted || error instanceof Error && error.name === "AbortError") {
|
|
147
|
-
return new ZLinkFrameworkException(
|
|
148
|
-
6 /* DeadlineExceeded */,
|
|
149
|
-
"HTTP request exceeded timeout",
|
|
150
|
-
error
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
const message = error instanceof Error ? error.message : "HTTP transport failure";
|
|
154
|
-
return new ZLinkFrameworkException(5 /* Unavailable */, message, error);
|
|
155
|
-
}
|
|
156
|
-
function isRetriableHttpFailure(error) {
|
|
157
|
-
return error.kind === 5 /* Unavailable */ || error.kind === 6 /* DeadlineExceeded */;
|
|
158
|
-
}
|
|
159
|
-
function delay(ms) {
|
|
160
|
-
return new Promise((resolve) => {
|
|
161
|
-
setTimeout(resolve, ms);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// packages/http-client/src/runtime/browser-runtime.ts
|
|
166
|
-
var HttpClientRuntime = class {
|
|
167
|
-
constructor(options) {
|
|
168
|
-
this.options = options;
|
|
169
|
-
rejectUnsupportedTransportOptions(options);
|
|
170
|
-
this.retryPolicy = new RetryPolicy(options);
|
|
171
|
-
}
|
|
172
|
-
retryPolicy;
|
|
173
|
-
async executeAsync(spec) {
|
|
174
|
-
return await this.retryPolicy.execute(spec, (request, signal) => this.perform(request, signal));
|
|
175
|
-
}
|
|
176
|
-
async close() {
|
|
177
|
-
}
|
|
178
|
-
async perform(spec, signal) {
|
|
179
|
-
const baseUri = new URL(this.options.baseUrl);
|
|
180
|
-
const origin = baseUri.origin;
|
|
181
|
-
let current = new URL(origin + makeTarget(baseUri.pathname, spec.target));
|
|
182
|
-
let method = spec.method;
|
|
183
|
-
let body = spec.body;
|
|
184
|
-
let bodyProvider = spec.bodyProvider;
|
|
185
|
-
let redirectsLeft = this.options.followRedirects;
|
|
186
|
-
for (; ; ) {
|
|
187
|
-
const headers = this.buildHeaders(
|
|
188
|
-
spec,
|
|
189
|
-
current.origin === origin,
|
|
190
|
-
body !== void 0 || bodyProvider !== void 0
|
|
191
|
-
);
|
|
192
|
-
const response = await fetch(current, {
|
|
193
|
-
method,
|
|
194
|
-
headers,
|
|
195
|
-
body: body ?? bodyStream(bodyProvider),
|
|
196
|
-
credentials: this.options.cookies ? "include" : "same-origin",
|
|
197
|
-
redirect: "manual",
|
|
198
|
-
signal
|
|
199
|
-
});
|
|
200
|
-
const location = response.headers.get("location");
|
|
201
|
-
if (this.options.followRedirects > 0 && isRedirectStatus(response.status) && location !== null) {
|
|
202
|
-
if (redirectsLeft === 0) {
|
|
203
|
-
await response.body?.cancel();
|
|
204
|
-
throw redirectLimitExceeded();
|
|
205
|
-
}
|
|
206
|
-
redirectsLeft--;
|
|
207
|
-
({ method, body } = rewriteForRedirect(response.status, method, body));
|
|
208
|
-
bodyProvider = void 0;
|
|
209
|
-
await response.body?.cancel();
|
|
210
|
-
current = resolveLocation(current, location);
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const headersResult = collectHeaders(response.headers);
|
|
214
|
-
if (spec.sink !== void 0) {
|
|
215
|
-
await streamResponse(response, spec.sink, this.options.maxResponseBodySize);
|
|
216
|
-
return { status: response.status, headers: headersResult, body: "" };
|
|
217
|
-
}
|
|
218
|
-
const text = await response.text();
|
|
219
|
-
if (new TextEncoder().encode(text).length > this.options.maxResponseBodySize) {
|
|
220
|
-
throw responseBodySizeExceeded();
|
|
221
|
-
}
|
|
222
|
-
return { status: response.status, headers: headersResult, body: text };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
buildHeaders(spec, keepAuthorization, hasBody) {
|
|
226
|
-
const headers = { accept: "application/json" };
|
|
227
|
-
applyHeaders(headers, this.options.headers, keepAuthorization);
|
|
228
|
-
applyHeaders(headers, spec.headers, keepAuthorization);
|
|
229
|
-
if (!hasBody) delete headers["content-type"];
|
|
230
|
-
return headers;
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
function rejectUnsupportedTransportOptions(options) {
|
|
234
|
-
if (options.trustCertificateFile !== void 0 || options.clientCertificate !== void 0 || options.proxy !== void 0) {
|
|
235
|
-
throw new ZLinkFrameworkException(
|
|
236
|
-
8 /* ProtocolError */,
|
|
237
|
-
"Browser HTTP clients cannot configure certificate files or a transport proxy."
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function bodyStream(provider) {
|
|
242
|
-
if (provider === void 0) return void 0;
|
|
243
|
-
return new ReadableStream({
|
|
244
|
-
pull(controller) {
|
|
245
|
-
const chunk = provider();
|
|
246
|
-
if (chunk === null) controller.close();
|
|
247
|
-
else controller.enqueue(chunk);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
async function streamResponse(response, sink, maximumSize) {
|
|
252
|
-
if (response.body === null) return;
|
|
253
|
-
const reader = response.body.getReader();
|
|
254
|
-
let total = 0;
|
|
255
|
-
for (; ; ) {
|
|
256
|
-
const result = await reader.read();
|
|
257
|
-
if (result.done) return;
|
|
258
|
-
total += result.value.length;
|
|
259
|
-
if (total > maximumSize) {
|
|
260
|
-
await reader.cancel();
|
|
261
|
-
throw responseBodySizeExceeded();
|
|
262
|
-
}
|
|
263
|
-
sink(result.value);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function collectHeaders(headers) {
|
|
267
|
-
const result = {};
|
|
268
|
-
headers.forEach((value, name) => {
|
|
269
|
-
result[name.toLowerCase()] = value;
|
|
270
|
-
});
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
function applyHeaders(target, source, keepAuthorization) {
|
|
274
|
-
for (const [name, value] of Object.entries(source)) {
|
|
275
|
-
const lower = name.toLowerCase();
|
|
276
|
-
if (keepAuthorization || lower !== "authorization") target[lower] = value;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
65
|
// packages/http-client/src/runtime/text.ts
|
|
66
|
+
var HttpHeaderName = Object.freeze({
|
|
67
|
+
Accept: "accept",
|
|
68
|
+
Authorization: "authorization",
|
|
69
|
+
ContentType: "content-type",
|
|
70
|
+
Location: "location",
|
|
71
|
+
SetCookie: "set-cookie",
|
|
72
|
+
UserAgent: "user-agent",
|
|
73
|
+
AcceptEncoding: "accept-encoding",
|
|
74
|
+
Cookie: "cookie",
|
|
75
|
+
ContentEncoding: "content-encoding",
|
|
76
|
+
ContentLength: "content-length"
|
|
77
|
+
});
|
|
78
|
+
var HttpContentType = Object.freeze({
|
|
79
|
+
Json: "application/json",
|
|
80
|
+
UrlEncoded: "application/x-www-form-urlencoded",
|
|
81
|
+
Multipart: "multipart/form-data"
|
|
82
|
+
});
|
|
281
83
|
function isBlank(value) {
|
|
282
84
|
return value.length === 0 || /^[\s]*$/u.test(value);
|
|
283
85
|
}
|
|
@@ -311,11 +113,14 @@ function percentEncode(value) {
|
|
|
311
113
|
function basicAuthorization(user, password) {
|
|
312
114
|
return "Basic " + Buffer.from(`${user}:${password}`, "utf8").toString("base64");
|
|
313
115
|
}
|
|
116
|
+
var MULTIPART_BOUNDARY_PREFIX = "zlink-boundary-";
|
|
117
|
+
var MULTIPART_BOUNDARY_ENTROPY_CHARACTERS = 16;
|
|
314
118
|
function makeMultipartBoundary() {
|
|
315
|
-
return
|
|
119
|
+
return MULTIPART_BOUNDARY_PREFIX + Math.random().toString(16).slice(2).padEnd(MULTIPART_BOUNDARY_ENTROPY_CHARACTERS, "0").slice(0, MULTIPART_BOUNDARY_ENTROPY_CHARACTERS);
|
|
316
120
|
}
|
|
317
121
|
|
|
318
122
|
// packages/http-client/src/request-builder.ts
|
|
123
|
+
var HTTP_FAILURE_STATUS_MIN = 400;
|
|
319
124
|
var ZLinkHttpRequestBuilder = class {
|
|
320
125
|
constructor(client, method, path, clientFactory) {
|
|
321
126
|
this.method = method;
|
|
@@ -364,10 +169,16 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
364
169
|
}
|
|
365
170
|
return this.clientInstance;
|
|
366
171
|
}
|
|
367
|
-
async
|
|
368
|
-
if (this.ownsClient
|
|
369
|
-
|
|
172
|
+
async execute(client, operation) {
|
|
173
|
+
if (!this.ownsClient) return operation;
|
|
174
|
+
const [outcome] = await Promise.allSettled([operation]);
|
|
175
|
+
try {
|
|
176
|
+
await client.close();
|
|
177
|
+
} catch (closeError) {
|
|
178
|
+
if (outcome.status === "fulfilled") throw closeError;
|
|
370
179
|
}
|
|
180
|
+
if (outcome.status === "rejected") throw outcome.reason;
|
|
181
|
+
return outcome.value;
|
|
371
182
|
}
|
|
372
183
|
header(name, value) {
|
|
373
184
|
requireNonBlank(name, "HTTP request header name is required");
|
|
@@ -394,11 +205,11 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
394
205
|
}
|
|
395
206
|
requireNonBlank(contentType, "HTTP request body content type is required");
|
|
396
207
|
this.bodyValue = value;
|
|
397
|
-
this.headersValue[
|
|
208
|
+
this.headersValue[HttpHeaderName.ContentType] = contentType;
|
|
398
209
|
return this;
|
|
399
210
|
}
|
|
400
211
|
this.bodyValue = JSON.stringify(value);
|
|
401
|
-
this.headersValue[
|
|
212
|
+
this.headersValue[HttpHeaderName.ContentType] ??= HttpContentType.Json;
|
|
402
213
|
return this;
|
|
403
214
|
}
|
|
404
215
|
/**
|
|
@@ -414,7 +225,7 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
414
225
|
}
|
|
415
226
|
requireNonBlank(contentType, "HTTP request body content type is required");
|
|
416
227
|
this.bodyProviderValue = provider;
|
|
417
|
-
this.headersValue[
|
|
228
|
+
this.headersValue[HttpHeaderName.ContentType] = contentType;
|
|
418
229
|
return this;
|
|
419
230
|
}
|
|
420
231
|
form(name, value) {
|
|
@@ -438,11 +249,7 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
438
249
|
async submitRaw() {
|
|
439
250
|
const request = this.makeRequest(void 0);
|
|
440
251
|
const client = this.resolveClient();
|
|
441
|
-
|
|
442
|
-
return await client.runtime.executeAsync(request);
|
|
443
|
-
} finally {
|
|
444
|
-
await this.closeIfOwned();
|
|
445
|
-
}
|
|
252
|
+
return this.execute(client, client.runtime.executeAsync(request));
|
|
446
253
|
}
|
|
447
254
|
/**
|
|
448
255
|
* Streams the response body to `sink` chunk by chunk instead of buffering it; the returned
|
|
@@ -457,11 +264,7 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
457
264
|
}
|
|
458
265
|
const request = this.makeRequest(sink);
|
|
459
266
|
const client = this.resolveClient();
|
|
460
|
-
|
|
461
|
-
return await client.runtime.executeAsync(request);
|
|
462
|
-
} finally {
|
|
463
|
-
await this.closeIfOwned();
|
|
464
|
-
}
|
|
267
|
+
return this.execute(client, client.runtime.executeAsync(request));
|
|
465
268
|
}
|
|
466
269
|
submit(callback) {
|
|
467
270
|
const pending = this.executeTyped();
|
|
@@ -474,8 +277,13 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
474
277
|
);
|
|
475
278
|
}
|
|
476
279
|
async executeTyped() {
|
|
477
|
-
const
|
|
478
|
-
|
|
280
|
+
const request = this.makeRequest(void 0);
|
|
281
|
+
const client = this.resolveClient();
|
|
282
|
+
return this.execute(client, this.decodeResponse(client.runtime.executeAsync(request)));
|
|
283
|
+
}
|
|
284
|
+
async decodeResponse(pending) {
|
|
285
|
+
const raw = await pending;
|
|
286
|
+
if (raw.status >= HTTP_FAILURE_STATUS_MIN) {
|
|
479
287
|
throw new ZLinkFrameworkException(
|
|
480
288
|
11 /* InternalFailure */,
|
|
481
289
|
`HTTP request failed with status ${raw.status}`
|
|
@@ -544,12 +352,12 @@ var ZLinkHttpRequestBuilder = class {
|
|
|
544
352
|
return { body: this.bodyValue, headers };
|
|
545
353
|
}
|
|
546
354
|
if (this.formValue.length > 0) {
|
|
547
|
-
headers[
|
|
355
|
+
headers[HttpHeaderName.ContentType] = HttpContentType.UrlEncoded;
|
|
548
356
|
return { body: this.encodeFormBody(), headers };
|
|
549
357
|
}
|
|
550
358
|
if (this.multipartValue.length > 0) {
|
|
551
359
|
const boundary = makeMultipartBoundary();
|
|
552
|
-
headers[
|
|
360
|
+
headers[HttpHeaderName.ContentType] = `${HttpContentType.Multipart}; boundary=${boundary}`;
|
|
553
361
|
return { body: this.encodeMultipartBody(boundary), headers };
|
|
554
362
|
}
|
|
555
363
|
return { body: void 0, headers };
|
|
@@ -608,6 +416,311 @@ function safeJsonReviver(key, value) {
|
|
|
608
416
|
return value;
|
|
609
417
|
}
|
|
610
418
|
|
|
419
|
+
// packages/http-client/src/runtime/http-client-errors.ts
|
|
420
|
+
function responseBodySizeExceeded(message = "HTTP response exceeded the maximum body size") {
|
|
421
|
+
return new ZLinkFrameworkException(4 /* Rejected */, message);
|
|
422
|
+
}
|
|
423
|
+
function redirectLimitExceeded(message = "HTTP request exceeded the redirect limit") {
|
|
424
|
+
return new ZLinkFrameworkException(8 /* ProtocolError */, message);
|
|
425
|
+
}
|
|
426
|
+
function redirectFormatError(message) {
|
|
427
|
+
return new ZLinkFrameworkException(8 /* ProtocolError */, message);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// packages/http-client/src/runtime/redirect-policy.ts
|
|
431
|
+
var RedirectStatus = {
|
|
432
|
+
MovedPermanently: 301,
|
|
433
|
+
Found: 302,
|
|
434
|
+
SeeOther: 303,
|
|
435
|
+
TemporaryRedirect: 307,
|
|
436
|
+
PermanentRedirect: 308
|
|
437
|
+
};
|
|
438
|
+
var REDIRECT_STATUSES = new Set(Object.values(RedirectStatus));
|
|
439
|
+
function isRedirectStatus(status) {
|
|
440
|
+
return REDIRECT_STATUSES.has(status);
|
|
441
|
+
}
|
|
442
|
+
function makeTarget(prefix, path) {
|
|
443
|
+
if (prefix.length === 0 || prefix === "/") {
|
|
444
|
+
return path;
|
|
445
|
+
}
|
|
446
|
+
return prefix.endsWith("/") ? prefix.slice(0, -1) + path : prefix + path;
|
|
447
|
+
}
|
|
448
|
+
function rewriteForRedirect(status, method, body) {
|
|
449
|
+
if (status === RedirectStatus.SeeOther || (status === RedirectStatus.MovedPermanently || status === RedirectStatus.Found) && method === "POST") {
|
|
450
|
+
return { method: "GET", body: void 0 };
|
|
451
|
+
}
|
|
452
|
+
return { method, body };
|
|
453
|
+
}
|
|
454
|
+
function resolveLocation(current, location) {
|
|
455
|
+
try {
|
|
456
|
+
if (location.startsWith("http://") || location.startsWith("https://")) {
|
|
457
|
+
return new URL(location);
|
|
458
|
+
}
|
|
459
|
+
if (location.startsWith("//")) {
|
|
460
|
+
return new URL(current.protocol + location);
|
|
461
|
+
}
|
|
462
|
+
if (location.startsWith("/")) {
|
|
463
|
+
return new URL(current.origin + location);
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
throw redirectFormatError(`HTTP redirect location is malformed: ${location}`);
|
|
467
|
+
}
|
|
468
|
+
throw redirectFormatError(`HTTP redirect location is not supported: ${location}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// packages/http-client/src/runtime/retry-policy.ts
|
|
472
|
+
var INITIAL_RETRY_DELAY_MS = 50;
|
|
473
|
+
var MAX_RETRY_DELAY_MS = 1e3;
|
|
474
|
+
var MAX_RETRY_DELAY_SHIFT = Math.ceil(Math.log2(MAX_RETRY_DELAY_MS / INITIAL_RETRY_DELAY_MS));
|
|
475
|
+
function delayMsFor(attempt) {
|
|
476
|
+
const ceiling = Math.min(
|
|
477
|
+
MAX_RETRY_DELAY_MS,
|
|
478
|
+
INITIAL_RETRY_DELAY_MS << Math.min(attempt, MAX_RETRY_DELAY_SHIFT)
|
|
479
|
+
);
|
|
480
|
+
return Math.floor(Math.random() * (ceiling + 1));
|
|
481
|
+
}
|
|
482
|
+
var RetryPolicy = class {
|
|
483
|
+
constructor(options) {
|
|
484
|
+
this.options = options;
|
|
485
|
+
}
|
|
486
|
+
async execute(spec, perform) {
|
|
487
|
+
const maxRetries = spec.sink !== void 0 || spec.bodyProvider !== void 0 ? 0 : this.options.retryAttempts;
|
|
488
|
+
const timeoutMs = spec.timeoutMs ?? this.options.timeoutMs;
|
|
489
|
+
for (let attempt = 0; ; attempt++) {
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const timer = setTimeout(() => {
|
|
492
|
+
controller.abort();
|
|
493
|
+
}, timeoutMs);
|
|
494
|
+
try {
|
|
495
|
+
return await perform(spec, controller);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
const failure = mapFailure(error, controller.signal.aborted, 1 /* Application */);
|
|
498
|
+
if (isRetriableHttpFailure(failure) && attempt < maxRetries) {
|
|
499
|
+
await delay(delayMsFor(attempt));
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
throw failure;
|
|
503
|
+
} finally {
|
|
504
|
+
clearTimeout(timer);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function mapFailure(error, aborted, stage) {
|
|
510
|
+
if (error instanceof ZLinkFrameworkException) {
|
|
511
|
+
return error;
|
|
512
|
+
}
|
|
513
|
+
if (stage === 0 /* Transport */ && aborted) {
|
|
514
|
+
const cause = new DOMException("HTTP request exceeded timeout", "TimeoutError");
|
|
515
|
+
return new ZLinkFrameworkException(
|
|
516
|
+
6 /* DeadlineExceeded */,
|
|
517
|
+
cause.message,
|
|
518
|
+
cause
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const message = error instanceof Error ? error.message : "HTTP execution failure";
|
|
522
|
+
return new ZLinkFrameworkException(
|
|
523
|
+
stage === 0 /* Transport */ ? 5 /* Unavailable */ : 11 /* InternalFailure */,
|
|
524
|
+
message,
|
|
525
|
+
error
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function isRetriableHttpFailure(error) {
|
|
529
|
+
return error.kind === 5 /* Unavailable */ || error.kind === 6 /* DeadlineExceeded */;
|
|
530
|
+
}
|
|
531
|
+
function delay(ms) {
|
|
532
|
+
return new Promise((resolve) => {
|
|
533
|
+
setTimeout(resolve, ms);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// packages/http-client/src/runtime/bounded-response-body.ts
|
|
538
|
+
async function readResponseBody(stream, maximumSize, consume) {
|
|
539
|
+
let total = 0;
|
|
540
|
+
for await (const chunk of stream) {
|
|
541
|
+
total += chunk.length;
|
|
542
|
+
if (total > maximumSize) {
|
|
543
|
+
throw responseBodySizeExceeded();
|
|
544
|
+
}
|
|
545
|
+
consume(chunk);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// packages/http-client/src/runtime/browser-runtime.ts
|
|
550
|
+
var HttpClientRuntime = class {
|
|
551
|
+
constructor(options) {
|
|
552
|
+
this.options = options;
|
|
553
|
+
rejectUnsupportedTransportOptions(options);
|
|
554
|
+
this.retryPolicy = new RetryPolicy(options);
|
|
555
|
+
}
|
|
556
|
+
retryPolicy;
|
|
557
|
+
async executeAsync(spec) {
|
|
558
|
+
return await this.retryPolicy.execute(
|
|
559
|
+
spec,
|
|
560
|
+
(request, controller) => this.perform(request, controller)
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
async close() {
|
|
564
|
+
}
|
|
565
|
+
async perform(spec, controller) {
|
|
566
|
+
const signal = controller.signal;
|
|
567
|
+
const baseUri = new URL(this.options.baseUrl);
|
|
568
|
+
const origin = baseUri.origin;
|
|
569
|
+
let current = new URL(origin + makeTarget(baseUri.pathname, spec.target));
|
|
570
|
+
let method = spec.method;
|
|
571
|
+
let body = spec.body;
|
|
572
|
+
let bodyProvider = spec.bodyProvider;
|
|
573
|
+
let redirectsLeft = this.options.followRedirects;
|
|
574
|
+
for (; ; ) {
|
|
575
|
+
const headers = this.buildHeaders(
|
|
576
|
+
spec,
|
|
577
|
+
current.origin === origin,
|
|
578
|
+
body !== void 0 || bodyProvider !== void 0
|
|
579
|
+
);
|
|
580
|
+
let response;
|
|
581
|
+
try {
|
|
582
|
+
const init = {
|
|
583
|
+
method,
|
|
584
|
+
headers,
|
|
585
|
+
body: body ?? bodyStream(bodyProvider, controller),
|
|
586
|
+
duplex: "half",
|
|
587
|
+
credentials: this.options.cookies ? "include" : "same-origin",
|
|
588
|
+
redirect: "manual",
|
|
589
|
+
signal
|
|
590
|
+
};
|
|
591
|
+
response = await fetch(current, init);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
throw mapFailure(error, signal.aborted, 0 /* Transport */);
|
|
594
|
+
}
|
|
595
|
+
const location = response.headers.get(HttpHeaderName.Location);
|
|
596
|
+
if (this.options.followRedirects > 0 && isRedirectStatus(response.status) && location !== null) {
|
|
597
|
+
if (redirectsLeft === 0) {
|
|
598
|
+
try {
|
|
599
|
+
await response.body?.cancel();
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw mapFailure(error, signal.aborted, 0 /* Transport */);
|
|
602
|
+
}
|
|
603
|
+
throw redirectLimitExceeded();
|
|
604
|
+
}
|
|
605
|
+
redirectsLeft--;
|
|
606
|
+
({ method, body } = rewriteForRedirect(response.status, method, body));
|
|
607
|
+
bodyProvider = void 0;
|
|
608
|
+
try {
|
|
609
|
+
await response.body?.cancel();
|
|
610
|
+
} catch (error) {
|
|
611
|
+
throw mapFailure(error, signal.aborted, 0 /* Transport */);
|
|
612
|
+
}
|
|
613
|
+
current = resolveLocation(current, location);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const headersResult = collectHeaders(response.headers);
|
|
617
|
+
if (spec.sink !== void 0) {
|
|
618
|
+
const sink = spec.sink;
|
|
619
|
+
await readResponseBody(
|
|
620
|
+
responseChunks(response, signal),
|
|
621
|
+
this.options.maxResponseBodySize,
|
|
622
|
+
(chunk) => {
|
|
623
|
+
try {
|
|
624
|
+
sink(chunk);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
throw mapFailure(error, signal.aborted, 1 /* Application */);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
return { status: response.status, headers: headersResult, body: "" };
|
|
631
|
+
}
|
|
632
|
+
const decoder = new TextDecoder();
|
|
633
|
+
const text = [];
|
|
634
|
+
await readResponseBody(
|
|
635
|
+
responseChunks(response, signal),
|
|
636
|
+
this.options.maxResponseBodySize,
|
|
637
|
+
(chunk) => {
|
|
638
|
+
text.push(decoder.decode(chunk, { stream: true }));
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
text.push(decoder.decode());
|
|
642
|
+
return { status: response.status, headers: headersResult, body: text.join("") };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
buildHeaders(spec, keepAuthorization, hasBody) {
|
|
646
|
+
const headers = {
|
|
647
|
+
[HttpHeaderName.Accept]: HttpContentType.Json
|
|
648
|
+
};
|
|
649
|
+
applyHeaders(headers, this.options.headers, keepAuthorization);
|
|
650
|
+
applyHeaders(headers, spec.headers, keepAuthorization);
|
|
651
|
+
if (!hasBody) delete headers[HttpHeaderName.ContentType];
|
|
652
|
+
return headers;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
function rejectUnsupportedTransportOptions(options) {
|
|
656
|
+
if (options.trustCertificateFile !== void 0 || options.clientCertificate !== void 0 || options.proxy !== void 0) {
|
|
657
|
+
throw new ZLinkFrameworkException(
|
|
658
|
+
8 /* ProtocolError */,
|
|
659
|
+
"Browser HTTP clients cannot configure certificate files or a transport proxy."
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function bodyStream(provider, abortController) {
|
|
664
|
+
if (provider === void 0) return void 0;
|
|
665
|
+
return new ReadableStream({
|
|
666
|
+
pull(controller) {
|
|
667
|
+
let chunk;
|
|
668
|
+
try {
|
|
669
|
+
chunk = provider();
|
|
670
|
+
} catch (error) {
|
|
671
|
+
const failure = mapFailure(
|
|
672
|
+
error,
|
|
673
|
+
abortController.signal.aborted,
|
|
674
|
+
1 /* Application */
|
|
675
|
+
);
|
|
676
|
+
abortController.abort(failure);
|
|
677
|
+
throw failure;
|
|
678
|
+
}
|
|
679
|
+
if (chunk === null) controller.close();
|
|
680
|
+
else controller.enqueue(chunk);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
async function* responseChunks(response, signal) {
|
|
685
|
+
if (response.body === null) return;
|
|
686
|
+
const reader = response.body.getReader();
|
|
687
|
+
try {
|
|
688
|
+
for (; ; ) {
|
|
689
|
+
let result;
|
|
690
|
+
try {
|
|
691
|
+
result = await reader.read();
|
|
692
|
+
} catch (error) {
|
|
693
|
+
throw mapFailure(error, signal.aborted, 0 /* Transport */);
|
|
694
|
+
}
|
|
695
|
+
if (result.done) return;
|
|
696
|
+
yield result.value;
|
|
697
|
+
}
|
|
698
|
+
} finally {
|
|
699
|
+
try {
|
|
700
|
+
try {
|
|
701
|
+
await reader.cancel();
|
|
702
|
+
} catch (error) {
|
|
703
|
+
throw mapFailure(error, signal.aborted, 0 /* Transport */);
|
|
704
|
+
}
|
|
705
|
+
} finally {
|
|
706
|
+
reader.releaseLock();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function collectHeaders(headers) {
|
|
711
|
+
const result = {};
|
|
712
|
+
headers.forEach((value, name) => {
|
|
713
|
+
result[name.toLowerCase()] = value;
|
|
714
|
+
});
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
function applyHeaders(target, source, keepAuthorization) {
|
|
718
|
+
for (const [name, value] of Object.entries(source)) {
|
|
719
|
+
const lower = name.toLowerCase();
|
|
720
|
+
if (keepAuthorization || lower !== HttpHeaderName.Authorization) target[lower] = value;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
611
724
|
// packages/http-client/src/client.ts
|
|
612
725
|
var ZLinkHttpClient = class {
|
|
613
726
|
constructor(runtimeInstance, executionScheduler) {
|
|
@@ -648,10 +761,13 @@ var ZLinkHttpClient = class {
|
|
|
648
761
|
await this.runtimeInstance.close();
|
|
649
762
|
}
|
|
650
763
|
};
|
|
764
|
+
var DEFAULT_HTTP_CLIENT_TIMEOUT_MS = 3e3;
|
|
765
|
+
var DEFAULT_HTTP_RESPONSE_BODY_SIZE = 16 * 1024 * 1024;
|
|
766
|
+
var DEFAULT_HTTP_MAX_REDIRECTS = 5;
|
|
651
767
|
var ZLinkHttpClientBuilder = class {
|
|
652
768
|
baseUrlValue = "";
|
|
653
|
-
timeoutMsValue =
|
|
654
|
-
maxResponseBodySizeValue =
|
|
769
|
+
timeoutMsValue = DEFAULT_HTTP_CLIENT_TIMEOUT_MS;
|
|
770
|
+
maxResponseBodySizeValue = DEFAULT_HTTP_RESPONSE_BODY_SIZE;
|
|
655
771
|
headersValue = {};
|
|
656
772
|
trustCertificateFileValue;
|
|
657
773
|
clientCertificateValue;
|
|
@@ -679,12 +795,12 @@ var ZLinkHttpClientBuilder = class {
|
|
|
679
795
|
}
|
|
680
796
|
basicAuth(user, password) {
|
|
681
797
|
requireNonBlank(user, "HTTP client basic auth user is required");
|
|
682
|
-
this.headersValue[
|
|
798
|
+
this.headersValue[HttpHeaderName.Authorization] = basicAuthorization(user, password);
|
|
683
799
|
return this;
|
|
684
800
|
}
|
|
685
801
|
bearerToken(token) {
|
|
686
802
|
requireNonBlank(token, "HTTP client bearer token is required");
|
|
687
|
-
this.headersValue[
|
|
803
|
+
this.headersValue[HttpHeaderName.Authorization] = `Bearer ${token}`;
|
|
688
804
|
return this;
|
|
689
805
|
}
|
|
690
806
|
maxResponseBodySize(bytes) {
|
|
@@ -708,7 +824,7 @@ var ZLinkHttpClientBuilder = class {
|
|
|
708
824
|
this.clientCertificateValue = { certificatePath, keyPath };
|
|
709
825
|
return this;
|
|
710
826
|
}
|
|
711
|
-
followRedirects(maxRedirects =
|
|
827
|
+
followRedirects(maxRedirects = DEFAULT_HTTP_MAX_REDIRECTS) {
|
|
712
828
|
if (!(maxRedirects > 0)) {
|
|
713
829
|
throw new ZLinkFrameworkException(
|
|
714
830
|
8 /* ProtocolError */,
|