@zlink-systems/http-client 0.25.0 → 0.27.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/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HttpClientRuntime } from './runtime/runtime';
|
|
2
1
|
import { ZLinkHttpRequestBuilder } from './request-builder';
|
|
2
|
+
import { HttpClientRuntime } from './runtime/runtime';
|
|
3
3
|
import type { ZLinkHttpExecutionScheduler } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* ZLink-style fluent HTTP client. Wraps undici behind a builder so transport types never leak into
|
package/dist/client.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.ZLinkHttpClientBuilder = exports.ZLinkHttpClient = void 0;
|
|
5
|
-
const framework_1 = require("@zlink-systems/framework");
|
|
6
|
-
const runtime_1 = require("./runtime/runtime");
|
|
7
5
|
const text_1 = require("./runtime/text");
|
|
6
|
+
const framework_1 = require("@zlink-systems/framework");
|
|
8
7
|
const request_builder_1 = require("./request-builder");
|
|
8
|
+
const runtime_1 = require("./runtime/runtime");
|
|
9
9
|
/**
|
|
10
10
|
* ZLink-style fluent HTTP client. Wraps undici behind a builder so transport types never leak into
|
|
11
11
|
* application code. A general HTTP client; the typed-JSON path (`body(dto)` / `submit<T>()`) is a
|
|
@@ -53,11 +53,14 @@ class ZLinkHttpClient {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
exports.ZLinkHttpClient = ZLinkHttpClient;
|
|
56
|
+
const DEFAULT_HTTP_CLIENT_TIMEOUT_MS = 3000;
|
|
57
|
+
const DEFAULT_HTTP_RESPONSE_BODY_SIZE = 16 * 1024 * 1024;
|
|
58
|
+
const DEFAULT_HTTP_MAX_REDIRECTS = 5;
|
|
56
59
|
/** Fluent builder for {@link ZLinkHttpClient}. Mirrors the C++ `client_builder_t`. */
|
|
57
60
|
class ZLinkHttpClientBuilder {
|
|
58
61
|
baseUrlValue = '';
|
|
59
|
-
timeoutMsValue =
|
|
60
|
-
maxResponseBodySizeValue =
|
|
62
|
+
timeoutMsValue = DEFAULT_HTTP_CLIENT_TIMEOUT_MS;
|
|
63
|
+
maxResponseBodySizeValue = DEFAULT_HTTP_RESPONSE_BODY_SIZE;
|
|
61
64
|
headersValue = {};
|
|
62
65
|
trustCertificateFileValue;
|
|
63
66
|
clientCertificateValue;
|
|
@@ -85,12 +88,12 @@ class ZLinkHttpClientBuilder {
|
|
|
85
88
|
}
|
|
86
89
|
basicAuth(user, password) {
|
|
87
90
|
(0, text_1.requireNonBlank)(user, 'HTTP client basic auth user is required');
|
|
88
|
-
this.headersValue[
|
|
91
|
+
this.headersValue[text_1.HttpHeaderName.Authorization] = (0, text_1.basicAuthorization)(user, password);
|
|
89
92
|
return this;
|
|
90
93
|
}
|
|
91
94
|
bearerToken(token) {
|
|
92
95
|
(0, text_1.requireNonBlank)(token, 'HTTP client bearer token is required');
|
|
93
|
-
this.headersValue[
|
|
96
|
+
this.headersValue[text_1.HttpHeaderName.Authorization] = `Bearer ${token}`;
|
|
94
97
|
return this;
|
|
95
98
|
}
|
|
96
99
|
maxResponseBodySize(bytes) {
|
|
@@ -111,7 +114,7 @@ class ZLinkHttpClientBuilder {
|
|
|
111
114
|
this.clientCertificateValue = { certificatePath, keyPath };
|
|
112
115
|
return this;
|
|
113
116
|
}
|
|
114
|
-
followRedirects(maxRedirects =
|
|
117
|
+
followRedirects(maxRedirects = DEFAULT_HTTP_MAX_REDIRECTS) {
|
|
115
118
|
if (!(maxRedirects > 0)) {
|
|
116
119
|
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'HTTP client follow_redirects must be greater than zero');
|
|
117
120
|
}
|
|
@@ -21,7 +21,7 @@ export declare class ZLinkHttpRequestBuilder {
|
|
|
21
21
|
private consumed;
|
|
22
22
|
constructor(client: ZLinkHttpClient | undefined, method: ZLinkHttpMethod, path: string, clientFactory?: ZLinkHttpClientBuilder);
|
|
23
23
|
private resolveClient;
|
|
24
|
-
private
|
|
24
|
+
private execute;
|
|
25
25
|
header(name: string, value: string): this;
|
|
26
26
|
query(name: string, value: string): this;
|
|
27
27
|
timeout(milliseconds: number): this;
|
|
@@ -46,6 +46,7 @@ export declare class ZLinkHttpRequestBuilder {
|
|
|
46
46
|
submit<T>(): Promise<HttpResponse<T>>;
|
|
47
47
|
submit<T>(callback: ZLinkHttpCallback<T>): void;
|
|
48
48
|
protected executeTyped<T>(): Promise<HttpResponse<T>>;
|
|
49
|
+
private decodeResponse;
|
|
49
50
|
/** Returns only the decoded body for client-side scenarios that do not need the HTTP envelope. */
|
|
50
51
|
fetch<T>(): Promise<T>;
|
|
51
52
|
private completeCallback;
|
package/dist/request-builder.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.ZLinkHttpRequestBuilder = void 0;
|
|
|
5
5
|
exports.createZLinkHttpRequestBuilder = createZLinkHttpRequestBuilder;
|
|
6
6
|
const framework_1 = require("@zlink-systems/framework");
|
|
7
7
|
const text_1 = require("./runtime/text");
|
|
8
|
+
const HTTP_FAILURE_STATUS_MIN = 400;
|
|
8
9
|
/**
|
|
9
10
|
* Fluent builder for a single request. Mirrors the C++ `request_builder_t`. Submission returns a
|
|
10
11
|
* `Promise`; the event loop is never blocked while the request is in flight.
|
|
@@ -53,11 +54,21 @@ class ZLinkHttpRequestBuilder {
|
|
|
53
54
|
}
|
|
54
55
|
return this.clientInstance;
|
|
55
56
|
}
|
|
56
|
-
async
|
|
57
|
-
if (this.ownsClient
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
async execute(client, operation) {
|
|
58
|
+
if (!this.ownsClient)
|
|
59
|
+
return operation;
|
|
60
|
+
const [outcome] = await Promise.allSettled([operation]);
|
|
61
|
+
try {
|
|
62
|
+
await client.close();
|
|
63
|
+
}
|
|
64
|
+
catch (closeError) {
|
|
65
|
+
// HTTP client builder §1: a request failure takes precedence over close.
|
|
66
|
+
if (outcome.status === 'fulfilled')
|
|
67
|
+
throw closeError;
|
|
60
68
|
}
|
|
69
|
+
if (outcome.status === 'rejected')
|
|
70
|
+
throw outcome.reason;
|
|
71
|
+
return outcome.value;
|
|
61
72
|
}
|
|
62
73
|
header(name, value) {
|
|
63
74
|
(0, text_1.requireNonBlank)(name, 'HTTP request header name is required');
|
|
@@ -81,11 +92,11 @@ class ZLinkHttpRequestBuilder {
|
|
|
81
92
|
}
|
|
82
93
|
(0, text_1.requireNonBlank)(contentType, 'HTTP request body content type is required');
|
|
83
94
|
this.bodyValue = value;
|
|
84
|
-
this.headersValue[
|
|
95
|
+
this.headersValue[text_1.HttpHeaderName.ContentType] = contentType;
|
|
85
96
|
return this;
|
|
86
97
|
}
|
|
87
98
|
this.bodyValue = JSON.stringify(value);
|
|
88
|
-
this.headersValue[
|
|
99
|
+
this.headersValue[text_1.HttpHeaderName.ContentType] ??= text_1.HttpContentType.Json;
|
|
89
100
|
return this;
|
|
90
101
|
}
|
|
91
102
|
/**
|
|
@@ -98,7 +109,7 @@ class ZLinkHttpRequestBuilder {
|
|
|
98
109
|
}
|
|
99
110
|
(0, text_1.requireNonBlank)(contentType, 'HTTP request body content type is required');
|
|
100
111
|
this.bodyProviderValue = provider;
|
|
101
|
-
this.headersValue[
|
|
112
|
+
this.headersValue[text_1.HttpHeaderName.ContentType] = contentType;
|
|
102
113
|
return this;
|
|
103
114
|
}
|
|
104
115
|
form(name, value) {
|
|
@@ -122,12 +133,7 @@ class ZLinkHttpRequestBuilder {
|
|
|
122
133
|
async submitRaw() {
|
|
123
134
|
const request = this.makeRequest(undefined);
|
|
124
135
|
const client = this.resolveClient();
|
|
125
|
-
|
|
126
|
-
return await client.runtime.executeAsync(request);
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
await this.closeIfOwned();
|
|
130
|
-
}
|
|
136
|
+
return this.execute(client, client.runtime.executeAsync(request));
|
|
131
137
|
}
|
|
132
138
|
/**
|
|
133
139
|
* Streams the response body to `sink` chunk by chunk instead of buffering it; the returned
|
|
@@ -139,12 +145,7 @@ class ZLinkHttpRequestBuilder {
|
|
|
139
145
|
}
|
|
140
146
|
const request = this.makeRequest(sink);
|
|
141
147
|
const client = this.resolveClient();
|
|
142
|
-
|
|
143
|
-
return await client.runtime.executeAsync(request);
|
|
144
|
-
}
|
|
145
|
-
finally {
|
|
146
|
-
await this.closeIfOwned();
|
|
147
|
-
}
|
|
148
|
+
return this.execute(client, client.runtime.executeAsync(request));
|
|
148
149
|
}
|
|
149
150
|
submit(callback) {
|
|
150
151
|
const pending = this.executeTyped();
|
|
@@ -154,8 +155,13 @@ class ZLinkHttpRequestBuilder {
|
|
|
154
155
|
void pending.then((response) => this.completeCallback(() => callback(undefined, response)), (error) => this.completeCallback(() => callback(error, undefined)));
|
|
155
156
|
}
|
|
156
157
|
async executeTyped() {
|
|
157
|
-
const
|
|
158
|
-
|
|
158
|
+
const request = this.makeRequest(undefined);
|
|
159
|
+
const client = this.resolveClient();
|
|
160
|
+
return this.execute(client, this.decodeResponse(client.runtime.executeAsync(request)));
|
|
161
|
+
}
|
|
162
|
+
async decodeResponse(pending) {
|
|
163
|
+
const raw = await pending;
|
|
164
|
+
if (raw.status >= HTTP_FAILURE_STATUS_MIN) {
|
|
159
165
|
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.InternalFailure, `HTTP request failed with status ${raw.status}`);
|
|
160
166
|
}
|
|
161
167
|
let body;
|
|
@@ -216,12 +222,12 @@ class ZLinkHttpRequestBuilder {
|
|
|
216
222
|
return { body: this.bodyValue, headers };
|
|
217
223
|
}
|
|
218
224
|
if (this.formValue.length > 0) {
|
|
219
|
-
headers[
|
|
225
|
+
headers[text_1.HttpHeaderName.ContentType] = text_1.HttpContentType.UrlEncoded;
|
|
220
226
|
return { body: this.encodeFormBody(), headers };
|
|
221
227
|
}
|
|
222
228
|
if (this.multipartValue.length > 0) {
|
|
223
229
|
const boundary = (0, text_1.makeMultipartBoundary)();
|
|
224
|
-
headers[
|
|
230
|
+
headers[text_1.HttpHeaderName.ContentType] = `${text_1.HttpContentType.Multipart}; boundary=${boundary}`;
|
|
225
231
|
return { body: this.encodeMultipartBody(boundary), headers };
|
|
226
232
|
}
|
|
227
233
|
return { body: undefined, headers };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.readResponseBody = readResponseBody;
|
|
5
|
+
const http_client_errors_1 = require("./http-client-errors");
|
|
6
|
+
/** Owns the received-byte limit for buffered and streaming responses on both transports. */
|
|
7
|
+
async function readResponseBody(stream, maximumSize, consume) {
|
|
8
|
+
let total = 0;
|
|
9
|
+
for await (const chunk of stream) {
|
|
10
|
+
total += chunk.length;
|
|
11
|
+
if (total > maximumSize) {
|
|
12
|
+
throw (0, http_client_errors_1.responseBodySizeExceeded)();
|
|
13
|
+
}
|
|
14
|
+
consume(chunk);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.HttpClientRuntime = void 0;
|
|
5
|
+
const text_1 = require("./text");
|
|
5
6
|
const framework_1 = require("@zlink-systems/framework");
|
|
6
7
|
const redirect_policy_1 = require("./redirect-policy");
|
|
7
8
|
const retry_policy_1 = require("./retry-policy");
|
|
8
9
|
const http_client_errors_1 = require("./http-client-errors");
|
|
10
|
+
const bounded_response_body_1 = require("./bounded-response-body");
|
|
9
11
|
/** Browser transport for the same public client surface used by the Node runtime. */
|
|
10
12
|
class HttpClientRuntime {
|
|
11
13
|
options;
|
|
@@ -16,10 +18,11 @@ class HttpClientRuntime {
|
|
|
16
18
|
this.retryPolicy = new retry_policy_1.RetryPolicy(options);
|
|
17
19
|
}
|
|
18
20
|
async executeAsync(spec) {
|
|
19
|
-
return await this.retryPolicy.execute(spec, (request,
|
|
21
|
+
return await this.retryPolicy.execute(spec, (request, controller) => this.perform(request, controller));
|
|
20
22
|
}
|
|
21
23
|
async close() { }
|
|
22
|
-
async perform(spec,
|
|
24
|
+
async perform(spec, controller) {
|
|
25
|
+
const signal = controller.signal;
|
|
23
26
|
const baseUri = new URL(this.options.baseUrl);
|
|
24
27
|
const origin = baseUri.origin;
|
|
25
28
|
let current = new URL(origin + (0, redirect_policy_1.makeTarget)(baseUri.pathname, spec.target));
|
|
@@ -29,47 +32,77 @@ class HttpClientRuntime {
|
|
|
29
32
|
let redirectsLeft = this.options.followRedirects;
|
|
30
33
|
for (;;) {
|
|
31
34
|
const headers = this.buildHeaders(spec, current.origin === origin, body !== undefined || bodyProvider !== undefined);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
const init = {
|
|
38
|
+
method,
|
|
39
|
+
headers,
|
|
40
|
+
body: body ?? bodyStream(bodyProvider, controller),
|
|
41
|
+
duplex: 'half',
|
|
42
|
+
credentials: this.options.cookies ? 'include' : 'same-origin',
|
|
43
|
+
redirect: 'manual',
|
|
44
|
+
signal
|
|
45
|
+
};
|
|
46
|
+
response = await fetch(current, init);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw (0, retry_policy_1.mapFailure)(error, signal.aborted, retry_policy_1.HttpFailureStage.Transport);
|
|
50
|
+
}
|
|
51
|
+
const location = response.headers.get(text_1.HttpHeaderName.Location);
|
|
41
52
|
if (this.options.followRedirects > 0 &&
|
|
42
53
|
(0, redirect_policy_1.isRedirectStatus)(response.status) &&
|
|
43
54
|
location !== null) {
|
|
44
55
|
if (redirectsLeft === 0) {
|
|
45
|
-
|
|
56
|
+
try {
|
|
57
|
+
await response.body?.cancel();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw (0, retry_policy_1.mapFailure)(error, signal.aborted, retry_policy_1.HttpFailureStage.Transport);
|
|
61
|
+
}
|
|
46
62
|
throw (0, http_client_errors_1.redirectLimitExceeded)();
|
|
47
63
|
}
|
|
48
64
|
redirectsLeft--;
|
|
49
65
|
({ method, body } = (0, redirect_policy_1.rewriteForRedirect)(response.status, method, body));
|
|
50
66
|
bodyProvider = undefined;
|
|
51
|
-
|
|
67
|
+
try {
|
|
68
|
+
await response.body?.cancel();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw (0, retry_policy_1.mapFailure)(error, signal.aborted, retry_policy_1.HttpFailureStage.Transport);
|
|
72
|
+
}
|
|
52
73
|
current = (0, redirect_policy_1.resolveLocation)(current, location);
|
|
53
74
|
continue;
|
|
54
75
|
}
|
|
55
76
|
const headersResult = collectHeaders(response.headers);
|
|
56
77
|
if (spec.sink !== undefined) {
|
|
57
|
-
|
|
78
|
+
const sink = spec.sink;
|
|
79
|
+
await (0, bounded_response_body_1.readResponseBody)(responseChunks(response, signal), this.options.maxResponseBodySize, (chunk) => {
|
|
80
|
+
try {
|
|
81
|
+
sink(chunk);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw (0, retry_policy_1.mapFailure)(error, signal.aborted, retry_policy_1.HttpFailureStage.Application);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
58
87
|
return { status: response.status, headers: headersResult, body: '' };
|
|
59
88
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
const decoder = new TextDecoder();
|
|
90
|
+
const text = [];
|
|
91
|
+
await (0, bounded_response_body_1.readResponseBody)(responseChunks(response, signal), this.options.maxResponseBodySize, (chunk) => {
|
|
92
|
+
text.push(decoder.decode(chunk, { stream: true }));
|
|
93
|
+
});
|
|
94
|
+
text.push(decoder.decode());
|
|
95
|
+
return { status: response.status, headers: headersResult, body: text.join('') };
|
|
65
96
|
}
|
|
66
97
|
}
|
|
67
98
|
buildHeaders(spec, keepAuthorization, hasBody) {
|
|
68
|
-
const headers = {
|
|
99
|
+
const headers = {
|
|
100
|
+
[text_1.HttpHeaderName.Accept]: text_1.HttpContentType.Json
|
|
101
|
+
};
|
|
69
102
|
applyHeaders(headers, this.options.headers, keepAuthorization);
|
|
70
103
|
applyHeaders(headers, spec.headers, keepAuthorization);
|
|
71
104
|
if (!hasBody)
|
|
72
|
-
delete headers[
|
|
105
|
+
delete headers[text_1.HttpHeaderName.ContentType];
|
|
73
106
|
return headers;
|
|
74
107
|
}
|
|
75
108
|
}
|
|
@@ -81,12 +114,20 @@ function rejectUnsupportedTransportOptions(options) {
|
|
|
81
114
|
throw new framework_1.ZLinkFrameworkException(framework_1.ZLinkFrameworkErrorKind.ProtocolError, 'Browser HTTP clients cannot configure certificate files or a transport proxy.');
|
|
82
115
|
}
|
|
83
116
|
}
|
|
84
|
-
function bodyStream(provider) {
|
|
117
|
+
function bodyStream(provider, abortController) {
|
|
85
118
|
if (provider === undefined)
|
|
86
119
|
return undefined;
|
|
87
120
|
return new ReadableStream({
|
|
88
121
|
pull(controller) {
|
|
89
|
-
|
|
122
|
+
let chunk;
|
|
123
|
+
try {
|
|
124
|
+
chunk = provider();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const failure = (0, retry_policy_1.mapFailure)(error, abortController.signal.aborted, retry_policy_1.HttpFailureStage.Application);
|
|
128
|
+
abortController.abort(failure);
|
|
129
|
+
throw failure;
|
|
130
|
+
}
|
|
90
131
|
if (chunk === null)
|
|
91
132
|
controller.close();
|
|
92
133
|
else
|
|
@@ -94,21 +135,36 @@ function bodyStream(provider) {
|
|
|
94
135
|
}
|
|
95
136
|
});
|
|
96
137
|
}
|
|
97
|
-
async function
|
|
138
|
+
async function* responseChunks(response, signal) {
|
|
98
139
|
if (response.body === null)
|
|
99
140
|
return;
|
|
100
141
|
const reader = response.body.getReader();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
try {
|
|
143
|
+
for (;;) {
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await reader.read();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
throw (0, retry_policy_1.mapFailure)(error, signal.aborted, retry_policy_1.HttpFailureStage.Transport);
|
|
150
|
+
}
|
|
151
|
+
if (result.done)
|
|
152
|
+
return;
|
|
153
|
+
yield result.value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
try {
|
|
158
|
+
try {
|
|
159
|
+
await reader.cancel();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw (0, retry_policy_1.mapFailure)(error, signal.aborted, retry_policy_1.HttpFailureStage.Transport);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
reader.releaseLock();
|
|
110
167
|
}
|
|
111
|
-
sink(result.value);
|
|
112
168
|
}
|
|
113
169
|
}
|
|
114
170
|
function collectHeaders(headers) {
|
|
@@ -121,7 +177,7 @@ function collectHeaders(headers) {
|
|
|
121
177
|
function applyHeaders(target, source, keepAuthorization) {
|
|
122
178
|
for (const [name, value] of Object.entries(source)) {
|
|
123
179
|
const lower = name.toLowerCase();
|
|
124
|
-
if (keepAuthorization || lower !==
|
|
180
|
+
if (keepAuthorization || lower !== text_1.HttpHeaderName.Authorization)
|
|
125
181
|
target[lower] = value;
|
|
126
182
|
}
|
|
127
183
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare const CONTENT_ENCODING: Readonly<{
|
|
2
|
+
readonly Gzip: "gzip";
|
|
3
|
+
readonly Deflate: "deflate";
|
|
4
|
+
}>;
|
|
5
|
+
export declare const HTTP_ACCEPTED_CONTENT_ENCODINGS: string;
|
|
1
6
|
/**
|
|
2
7
|
* Wrapper-controlled response decompression mirroring the C++ `compression.cpp`: gzip and deflate
|
|
3
8
|
* are decoded, the decoded size is bounded by the configured body limit (enforced by zlib's
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.HTTP_ACCEPTED_CONTENT_ENCODINGS = exports.CONTENT_ENCODING = void 0;
|
|
4
5
|
exports.gunzip = gunzip;
|
|
5
6
|
exports.inflateDeflate = inflateDeflate;
|
|
6
7
|
const node_zlib_1 = require("node:zlib");
|
|
7
8
|
const node_util_1 = require("node:util");
|
|
8
9
|
const framework_1 = require("@zlink-systems/framework");
|
|
9
10
|
const http_client_errors_1 = require("./http-client-errors");
|
|
11
|
+
exports.CONTENT_ENCODING = Object.freeze({ Gzip: 'gzip', Deflate: 'deflate' });
|
|
12
|
+
exports.HTTP_ACCEPTED_CONTENT_ENCODINGS = `${exports.CONTENT_ENCODING.Gzip}, ${exports.CONTENT_ENCODING.Deflate}`;
|
|
13
|
+
const HTTP_DECOMPRESSION_CHUNK_SIZE = 1 << 20;
|
|
10
14
|
const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
|
|
11
15
|
const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
|
|
12
16
|
const inflateRawAsync = (0, node_util_1.promisify)(node_zlib_1.inflateRaw);
|
|
@@ -20,14 +24,17 @@ const inflateRawAsync = (0, node_util_1.promisify)(node_zlib_1.inflateRaw);
|
|
|
20
24
|
* synchronous variants would block the event loop for the whole decode of a large body.
|
|
21
25
|
*/
|
|
22
26
|
function gunzip(input, maxBytes) {
|
|
23
|
-
return decode(() => gunzipAsync(input, { maxOutputLength: maxBytes, chunkSize:
|
|
27
|
+
return decode(() => gunzipAsync(input, { maxOutputLength: maxBytes, chunkSize: HTTP_DECOMPRESSION_CHUNK_SIZE }));
|
|
24
28
|
}
|
|
25
29
|
function inflateDeflate(input, maxBytes) {
|
|
26
30
|
// Detect a zlib-wrapped stream (CMF/FLG: method deflate, header a multiple of 31) vs raw deflate.
|
|
27
31
|
const zlibWrapped = input.length >= 2 && (input[0] & 0x0f) === 8 && ((input[0] << 8) | input[1]) % 31 === 0;
|
|
28
32
|
return decode(() => zlibWrapped
|
|
29
|
-
? inflateAsync(input, { maxOutputLength: maxBytes, chunkSize:
|
|
30
|
-
: inflateRawAsync(input, {
|
|
33
|
+
? inflateAsync(input, { maxOutputLength: maxBytes, chunkSize: HTTP_DECOMPRESSION_CHUNK_SIZE })
|
|
34
|
+
: inflateRawAsync(input, {
|
|
35
|
+
maxOutputLength: maxBytes,
|
|
36
|
+
chunkSize: HTTP_DECOMPRESSION_CHUNK_SIZE
|
|
37
|
+
}));
|
|
31
38
|
}
|
|
32
39
|
async function decode(run) {
|
|
33
40
|
try {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.CookieJar = void 0;
|
|
5
|
+
const COOKIE_ATTRIBUTE = Object.freeze({
|
|
6
|
+
Path: 'path',
|
|
7
|
+
Secure: 'secure',
|
|
8
|
+
MaxAge: 'max-age'
|
|
9
|
+
});
|
|
5
10
|
const MAX_COOKIES_PER_HOST = 128;
|
|
6
11
|
class CookieJar {
|
|
7
12
|
cookies = [];
|
|
@@ -25,13 +30,14 @@ class CookieJar {
|
|
|
25
30
|
const attrEquals = attribute.indexOf('=');
|
|
26
31
|
const attrName = (attrEquals < 0 ? attribute : attribute.slice(0, attrEquals)).trim();
|
|
27
32
|
const attrValue = attrEquals < 0 ? '' : attribute.slice(attrEquals + 1).trim();
|
|
28
|
-
|
|
33
|
+
const normalizedName = attrName.toLowerCase();
|
|
34
|
+
if (normalizedName === COOKIE_ATTRIBUTE.Path && attrValue.length > 0) {
|
|
29
35
|
path = attrValue;
|
|
30
36
|
}
|
|
31
|
-
else if (
|
|
37
|
+
else if (normalizedName === COOKIE_ATTRIBUTE.Secure) {
|
|
32
38
|
secure = true;
|
|
33
39
|
}
|
|
34
|
-
else if (
|
|
40
|
+
else if (normalizedName === COOKIE_ATTRIBUTE.MaxAge) {
|
|
35
41
|
const maxAge = Number.parseInt(attrValue, 10);
|
|
36
42
|
if (!Number.isNaN(maxAge)) {
|
|
37
43
|
expired = maxAge <= 0;
|
|
@@ -10,7 +10,14 @@ const http_client_errors_1 = require("./http-client-errors");
|
|
|
10
10
|
* Stateless redirect and target-URL rules for the wrapper-owned redirect loop. Isolates the redirect
|
|
11
11
|
* contract (allowed statuses, location resolution, method rewrite) from the request flow.
|
|
12
12
|
*/
|
|
13
|
-
const
|
|
13
|
+
const RedirectStatus = {
|
|
14
|
+
MovedPermanently: 301,
|
|
15
|
+
Found: 302,
|
|
16
|
+
SeeOther: 303,
|
|
17
|
+
TemporaryRedirect: 307,
|
|
18
|
+
PermanentRedirect: 308
|
|
19
|
+
};
|
|
20
|
+
const REDIRECT_STATUSES = new Set(Object.values(RedirectStatus));
|
|
14
21
|
function isRedirectStatus(status) {
|
|
15
22
|
return REDIRECT_STATUSES.has(status);
|
|
16
23
|
}
|
|
@@ -26,7 +33,9 @@ function makeTarget(prefix, path) {
|
|
|
26
33
|
* bodyless GET; all other redirects preserve the method and body.
|
|
27
34
|
*/
|
|
28
35
|
function rewriteForRedirect(status, method, body) {
|
|
29
|
-
if (status ===
|
|
36
|
+
if (status === RedirectStatus.SeeOther ||
|
|
37
|
+
((status === RedirectStatus.MovedPermanently || status === RedirectStatus.Found) &&
|
|
38
|
+
method === 'POST')) {
|
|
30
39
|
return { method: 'GET', body: undefined };
|
|
31
40
|
}
|
|
32
41
|
return { method, body };
|