@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/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 = 3000;
60
- maxResponseBodySizeValue = 16 * 1024 * 1024;
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['authorization'] = (0, text_1.basicAuthorization)(user, password);
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['authorization'] = `Bearer ${token}`;
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 = 5) {
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 closeIfOwned;
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;
@@ -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 closeIfOwned() {
57
- if (this.ownsClient && this.clientInstance !== undefined) {
58
- // A one-shot cleanup failure must not mask the request result.
59
- await this.clientInstance.close().catch(() => undefined);
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['content-type'] = contentType;
95
+ this.headersValue[text_1.HttpHeaderName.ContentType] = contentType;
85
96
  return this;
86
97
  }
87
98
  this.bodyValue = JSON.stringify(value);
88
- this.headersValue['content-type'] ??= 'application/json';
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['content-type'] = contentType;
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
- try {
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
- try {
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 raw = await this.submitRaw();
158
- if (raw.status >= 400) {
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['content-type'] = 'application/x-www-form-urlencoded';
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['content-type'] = `multipart/form-data; boundary=${boundary}`;
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,2 @@
1
+ /** Owns the received-byte limit for buffered and streaming responses on both transports. */
2
+ export declare function readResponseBody<T extends Uint8Array>(stream: AsyncIterable<T>, maximumSize: number, consume: (chunk: T) => void): Promise<void>;
@@ -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, signal) => this.perform(request, signal));
21
+ return await this.retryPolicy.execute(spec, (request, controller) => this.perform(request, controller));
20
22
  }
21
23
  async close() { }
22
- async perform(spec, signal) {
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
- const response = await fetch(current, {
33
- method,
34
- headers,
35
- body: body ?? bodyStream(bodyProvider),
36
- credentials: this.options.cookies ? 'include' : 'same-origin',
37
- redirect: 'manual',
38
- signal
39
- });
40
- const location = response.headers.get('location');
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
- await response.body?.cancel();
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
- await response.body?.cancel();
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
- await streamResponse(response, spec.sink, this.options.maxResponseBodySize);
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 text = await response.text();
61
- if (new TextEncoder().encode(text).length > this.options.maxResponseBodySize) {
62
- throw (0, http_client_errors_1.responseBodySizeExceeded)();
63
- }
64
- return { status: response.status, headers: headersResult, body: text };
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 = { accept: 'application/json' };
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['content-type'];
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
- const chunk = provider();
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 streamResponse(response, sink, maximumSize) {
138
+ async function* responseChunks(response, signal) {
98
139
  if (response.body === null)
99
140
  return;
100
141
  const reader = response.body.getReader();
101
- let total = 0;
102
- for (;;) {
103
- const result = await reader.read();
104
- if (result.done)
105
- return;
106
- total += result.value.length;
107
- if (total > maximumSize) {
108
- await reader.cancel();
109
- throw (0, http_client_errors_1.responseBodySizeExceeded)();
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 !== 'authorization')
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: 1 << 20 }));
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: 1 << 20 })
30
- : inflateRawAsync(input, { maxOutputLength: maxBytes, chunkSize: 1 << 20 }));
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
- if (attrName.toLowerCase() === 'path' && attrValue.length > 0) {
33
+ const normalizedName = attrName.toLowerCase();
34
+ if (normalizedName === COOKIE_ATTRIBUTE.Path && attrValue.length > 0) {
29
35
  path = attrValue;
30
36
  }
31
- else if (attrName.toLowerCase() === 'secure') {
37
+ else if (normalizedName === COOKIE_ATTRIBUTE.Secure) {
32
38
  secure = true;
33
39
  }
34
- else if (attrName.toLowerCase() === 'max-age') {
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 REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
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 === 303 || ((status === 301 || status === 302) && method === 'POST')) {
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 };