@byndyusoft-ui/http-client 0.1.0-alpha.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.
Files changed (91) hide show
  1. package/README.md +479 -0
  2. package/dist/cjs/adapters/FetchAdapter.cjs +218 -0
  3. package/dist/cjs/adapters/XhrAdapter.cjs +357 -0
  4. package/dist/cjs/asserts/assertNonBlankString.cjs +11 -0
  5. package/dist/cjs/asserts/assertValidAdapter.cjs +15 -0
  6. package/dist/cjs/asserts/assertValidBaseUrl.cjs +10 -0
  7. package/dist/cjs/asserts/assertValidBody.cjs +17 -0
  8. package/dist/cjs/asserts/assertValidFetchAdapterOptions.cjs +70 -0
  9. package/dist/cjs/asserts/assertValidHeader.cjs +18 -0
  10. package/dist/cjs/asserts/assertValidHeaders.cjs +17 -0
  11. package/dist/cjs/asserts/assertValidHttpClientOptions.cjs +60 -0
  12. package/dist/cjs/asserts/assertValidMethod.cjs +13 -0
  13. package/dist/cjs/asserts/assertValidParam.cjs +23 -0
  14. package/dist/cjs/asserts/assertValidParams.cjs +17 -0
  15. package/dist/cjs/asserts/assertValidRequestConfig.cjs +55 -0
  16. package/dist/cjs/asserts/assertValidResponseType.cjs +13 -0
  17. package/dist/cjs/asserts/assertValidSignal.cjs +16 -0
  18. package/dist/cjs/asserts/assertValidTimeout.cjs +12 -0
  19. package/dist/cjs/asserts/assertValidUrl.cjs +10 -0
  20. package/dist/cjs/asserts/assertValidValidateStatus.cjs +13 -0
  21. package/dist/cjs/asserts/assertValidWithCredentials.cjs +12 -0
  22. package/dist/cjs/asserts/assertValidXhrAdapterOptions.cjs +39 -0
  23. package/dist/cjs/asserts/headerValueLineBreakPattern.cjs +5 -0
  24. package/dist/cjs/asserts/isRecord.cjs +11 -0
  25. package/dist/cjs/constants/httpMethods.cjs +13 -0
  26. package/dist/cjs/constants/httpResponseTypes.cjs +11 -0
  27. package/dist/cjs/constants/httpStatusCodes.cjs +68 -0
  28. package/dist/cjs/constants/requestBuilderErrorCodes.cjs +26 -0
  29. package/dist/cjs/core/HttpClient.cjs +141 -0
  30. package/dist/cjs/core/HttpRequestBuilder.cjs +129 -0
  31. package/dist/cjs/errors/AbortError.cjs +12 -0
  32. package/dist/cjs/errors/HttpClientError.cjs +18 -0
  33. package/dist/cjs/errors/HttpResponseError.cjs +26 -0
  34. package/dist/cjs/errors/NetworkError.cjs +12 -0
  35. package/dist/cjs/errors/ParseError.cjs +20 -0
  36. package/dist/cjs/errors/RequestBuilderError.cjs +18 -0
  37. package/dist/cjs/errors/RequestPreparationError.cjs +13 -0
  38. package/dist/cjs/errors/TimeoutError.cjs +18 -0
  39. package/dist/cjs/index.cjs +45 -0
  40. package/dist/cjs/utilities/buildUrl.cjs +94 -0
  41. package/dist/cjs/utilities/getErrorMessage.cjs +8 -0
  42. package/dist/cjs/utilities/isStatusAccepted.cjs +8 -0
  43. package/dist/cjs/utilities/mergeHeaders.cjs +30 -0
  44. package/dist/cjs/utilities/mergeParams.cjs +37 -0
  45. package/dist/cjs/utilities/prepareRequestBody.cjs +43 -0
  46. package/dist/esm/adapters/FetchAdapter.js +216 -0
  47. package/dist/esm/adapters/XhrAdapter.js +355 -0
  48. package/dist/esm/asserts/assertNonBlankString.js +9 -0
  49. package/dist/esm/asserts/assertValidAdapter.js +13 -0
  50. package/dist/esm/asserts/assertValidBaseUrl.js +8 -0
  51. package/dist/esm/asserts/assertValidBody.js +15 -0
  52. package/dist/esm/asserts/assertValidFetchAdapterOptions.js +68 -0
  53. package/dist/esm/asserts/assertValidHeader.js +16 -0
  54. package/dist/esm/asserts/assertValidHeaders.js +15 -0
  55. package/dist/esm/asserts/assertValidHttpClientOptions.js +58 -0
  56. package/dist/esm/asserts/assertValidMethod.js +11 -0
  57. package/dist/esm/asserts/assertValidParam.js +21 -0
  58. package/dist/esm/asserts/assertValidParams.js +15 -0
  59. package/dist/esm/asserts/assertValidRequestConfig.js +53 -0
  60. package/dist/esm/asserts/assertValidResponseType.js +11 -0
  61. package/dist/esm/asserts/assertValidSignal.js +14 -0
  62. package/dist/esm/asserts/assertValidTimeout.js +10 -0
  63. package/dist/esm/asserts/assertValidUrl.js +8 -0
  64. package/dist/esm/asserts/assertValidValidateStatus.js +11 -0
  65. package/dist/esm/asserts/assertValidWithCredentials.js +10 -0
  66. package/dist/esm/asserts/assertValidXhrAdapterOptions.js +37 -0
  67. package/dist/esm/asserts/headerValueLineBreakPattern.js +3 -0
  68. package/dist/esm/asserts/isRecord.js +9 -0
  69. package/dist/esm/constants/httpMethods.js +11 -0
  70. package/dist/esm/constants/httpResponseTypes.js +9 -0
  71. package/dist/esm/constants/httpStatusCodes.js +66 -0
  72. package/dist/esm/constants/requestBuilderErrorCodes.js +24 -0
  73. package/dist/esm/core/HttpClient.js +139 -0
  74. package/dist/esm/core/HttpRequestBuilder.js +127 -0
  75. package/dist/esm/errors/AbortError.js +9 -0
  76. package/dist/esm/errors/HttpClientError.js +15 -0
  77. package/dist/esm/errors/HttpResponseError.js +23 -0
  78. package/dist/esm/errors/NetworkError.js +9 -0
  79. package/dist/esm/errors/ParseError.js +17 -0
  80. package/dist/esm/errors/RequestBuilderError.js +15 -0
  81. package/dist/esm/errors/RequestPreparationError.js +10 -0
  82. package/dist/esm/errors/TimeoutError.js +15 -0
  83. package/dist/esm/index.js +16 -0
  84. package/dist/esm/utilities/buildUrl.js +92 -0
  85. package/dist/esm/utilities/getErrorMessage.js +6 -0
  86. package/dist/esm/utilities/isStatusAccepted.js +6 -0
  87. package/dist/esm/utilities/mergeHeaders.js +27 -0
  88. package/dist/esm/utilities/mergeParams.js +35 -0
  89. package/dist/esm/utilities/prepareRequestBody.js +41 -0
  90. package/dist/index.d.ts +354 -0
  91. package/package.json +53 -0
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ var mergeHeaders = require('./mergeHeaders.cjs');
4
+
5
+ function hasTag(value, tag) {
6
+ return Object.prototype.toString.call(value) === tag;
7
+ }
8
+ function isRequestBody(value) {
9
+ return (typeof value === 'string' ||
10
+ ArrayBuffer.isView(value) ||
11
+ hasTag(value, '[object ArrayBuffer]') ||
12
+ hasTag(value, '[object Blob]') ||
13
+ hasTag(value, '[object FormData]') ||
14
+ hasTag(value, '[object URLSearchParams]'));
15
+ }
16
+ function isUrlSearchParams(value) {
17
+ return hasTag(value, '[object URLSearchParams]');
18
+ }
19
+ /**
20
+ * Converts request data into a transport-compatible body.
21
+ * Mutates the provided headers by adding a default Content-Type for JSON and URLSearchParams bodies when absent.
22
+ * FormData is returned without setting Content-Type so the transport can add its boundary.
23
+ *
24
+ * @throws TypeError when data cannot be serialized as JSON.
25
+ */
26
+ function prepareRequestBody(data, headers) {
27
+ if (isRequestBody(data)) {
28
+ if (isUrlSearchParams(data) && !mergeHeaders.hasHeader(headers, 'Content-Type')) {
29
+ headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
30
+ }
31
+ return isUrlSearchParams(data) ? data.toString() : data;
32
+ }
33
+ if (!mergeHeaders.hasHeader(headers, 'Content-Type')) {
34
+ headers['Content-Type'] = 'application/json';
35
+ }
36
+ const body = JSON.stringify(data);
37
+ if (typeof body !== 'string') {
38
+ throw new TypeError('Request body cannot be serialized as JSON');
39
+ }
40
+ return body;
41
+ }
42
+
43
+ exports.prepareRequestBody = prepareRequestBody;
@@ -0,0 +1,216 @@
1
+ import { HTTP_METHODS } from '../constants/httpMethods.js';
2
+ import { HTTP_RESPONSE_TYPES } from '../constants/httpResponseTypes.js';
3
+ import { HttpClientError } from '../errors/HttpClientError.js';
4
+ import { HttpResponseError } from '../errors/HttpResponseError.js';
5
+ import { NetworkError } from '../errors/NetworkError.js';
6
+ import { ParseError } from '../errors/ParseError.js';
7
+ import { RequestPreparationError } from '../errors/RequestPreparationError.js';
8
+ import { TimeoutError } from '../errors/TimeoutError.js';
9
+ import { AbortError } from '../errors/AbortError.js';
10
+ import { assertValidFetchAdapterOptions } from '../asserts/assertValidFetchAdapterOptions.js';
11
+ import { buildUrl } from '../utilities/buildUrl.js';
12
+ import { getErrorMessage } from '../utilities/getErrorMessage.js';
13
+ import { mergeHeaders } from '../utilities/mergeHeaders.js';
14
+ import { prepareRequestBody } from '../utilities/prepareRequestBody.js';
15
+ import { isStatusAccepted } from '../utilities/isStatusAccepted.js';
16
+
17
+ function extractResponseHeaders(headers) {
18
+ const result = {};
19
+ headers.forEach((value, key) => {
20
+ result[key] = value;
21
+ });
22
+ return result;
23
+ }
24
+ function prepareFetchRequest(config) {
25
+ const { url, method, headers = {}, params, data, signal: userSignal, timeout, baseUrl: baseURL } = config;
26
+ let timeoutId;
27
+ let removeAbortListener;
28
+ const cleanup = () => {
29
+ if (timeoutId !== undefined) {
30
+ clearTimeout(timeoutId);
31
+ }
32
+ removeAbortListener?.();
33
+ };
34
+ try {
35
+ const fullUrl = buildUrl(baseURL, url, params);
36
+ const requestHeaders = mergeHeaders(headers);
37
+ const body = data !== undefined && method !== HTTP_METHODS.GET && method !== HTTP_METHODS.HEAD
38
+ ? prepareRequestBody(data, requestHeaders)
39
+ : undefined;
40
+ if (timeout) {
41
+ const controller = new AbortController();
42
+ timeoutId = setTimeout(() => controller.abort(), timeout);
43
+ if (userSignal) {
44
+ if (userSignal.aborted) {
45
+ throw new AbortError('Request was aborted', { cause: userSignal.reason, config });
46
+ }
47
+ const abort = () => controller.abort();
48
+ userSignal.addEventListener('abort', abort, { once: true });
49
+ removeAbortListener = () => userSignal.removeEventListener('abort', abort);
50
+ }
51
+ return {
52
+ fullUrl,
53
+ requestHeaders,
54
+ body,
55
+ signal: controller.signal,
56
+ userSignal,
57
+ timeout,
58
+ cleanup
59
+ };
60
+ }
61
+ return {
62
+ fullUrl,
63
+ requestHeaders,
64
+ body,
65
+ signal: userSignal,
66
+ userSignal,
67
+ timeout,
68
+ cleanup
69
+ };
70
+ }
71
+ catch (error) {
72
+ cleanup();
73
+ throw error;
74
+ }
75
+ }
76
+ function resolveCredentials(withCredentials, adapterCredentials) {
77
+ if (withCredentials === true) {
78
+ return 'include';
79
+ }
80
+ if (withCredentials === false) {
81
+ return 'same-origin';
82
+ }
83
+ return adapterCredentials ?? 'same-origin';
84
+ }
85
+ function createFetchError(error, request, config) {
86
+ if (error instanceof HttpClientError) {
87
+ return error;
88
+ }
89
+ if (request.userSignal?.aborted) {
90
+ return new AbortError('Request was aborted', { cause: request.userSignal.reason ?? error, config });
91
+ }
92
+ if (request.timeout && error instanceof DOMException && error.name === 'AbortError') {
93
+ return new TimeoutError(`Request timed out after ${request.timeout}ms`, {
94
+ cause: error,
95
+ config,
96
+ timeout: request.timeout
97
+ });
98
+ }
99
+ return new NetworkError(getErrorMessage(error, 'Network request failed'), { cause: error, config });
100
+ }
101
+ async function parseResponseBody(response, config, responseType) {
102
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
103
+ return undefined;
104
+ }
105
+ switch (responseType) {
106
+ case HTTP_RESPONSE_TYPES.TEXT:
107
+ return response.text();
108
+ case HTTP_RESPONSE_TYPES.BLOB:
109
+ return response.blob();
110
+ case HTTP_RESPONSE_TYPES.ARRAY_BUFFER:
111
+ return response.arrayBuffer();
112
+ case HTTP_RESPONSE_TYPES.STREAM:
113
+ if (response.body === null) {
114
+ throw new ParseError('Streaming response body is not available', {
115
+ config,
116
+ responseType: HTTP_RESPONSE_TYPES.STREAM
117
+ });
118
+ }
119
+ return response.body;
120
+ case HTTP_RESPONSE_TYPES.JSON:
121
+ default: {
122
+ const resolvedResponseType = responseType ?? HTTP_RESPONSE_TYPES.JSON;
123
+ const text = await response.text();
124
+ if (!text) {
125
+ return undefined;
126
+ }
127
+ try {
128
+ return JSON.parse(text);
129
+ }
130
+ catch (error) {
131
+ throw new ParseError('Failed to parse response body as JSON', {
132
+ cause: error,
133
+ config,
134
+ responseType: resolvedResponseType,
135
+ raw: text
136
+ });
137
+ }
138
+ }
139
+ }
140
+ }
141
+ class FetchAdapter {
142
+ options;
143
+ constructor(options = {}) {
144
+ assertValidFetchAdapterOptions(options);
145
+ this.options = { ...options };
146
+ }
147
+ async request(config) {
148
+ let request;
149
+ try {
150
+ request = prepareFetchRequest(config);
151
+ }
152
+ catch (error) {
153
+ if (error instanceof HttpClientError) {
154
+ throw error;
155
+ }
156
+ throw new RequestPreparationError('Failed to prepare HTTP request', { cause: error, config });
157
+ }
158
+ let response;
159
+ try {
160
+ response = await fetch(request.fullUrl, {
161
+ ...this.options,
162
+ method: config.method,
163
+ headers: request.requestHeaders,
164
+ body: request.body,
165
+ credentials: resolveCredentials(config.withCredentials, this.options.credentials),
166
+ signal: request.signal
167
+ });
168
+ }
169
+ catch (error) {
170
+ request.cleanup();
171
+ throw createFetchError(error, request, config);
172
+ }
173
+ let statusAccepted;
174
+ try {
175
+ statusAccepted = isStatusAccepted(response.status, config.validateStatus);
176
+ }
177
+ catch (error) {
178
+ request.cleanup();
179
+ throw error;
180
+ }
181
+ try {
182
+ if (!statusAccepted) {
183
+ let errorData;
184
+ try {
185
+ const text = await response.text();
186
+ try {
187
+ errorData = JSON.parse(text);
188
+ }
189
+ catch {
190
+ errorData = text || undefined;
191
+ }
192
+ }
193
+ catch {
194
+ errorData = undefined;
195
+ }
196
+ throw new HttpResponseError(`Request failed with status code ${response.status}`, response.status, response.statusText, extractResponseHeaders(response.headers), config, errorData);
197
+ }
198
+ const responseData = await parseResponseBody(response, config, config.responseType);
199
+ return {
200
+ data: responseData,
201
+ status: response.status,
202
+ statusText: response.statusText,
203
+ headers: extractResponseHeaders(response.headers),
204
+ config
205
+ };
206
+ }
207
+ catch (error) {
208
+ throw createFetchError(error, request, config);
209
+ }
210
+ finally {
211
+ request.cleanup();
212
+ }
213
+ }
214
+ }
215
+
216
+ export { FetchAdapter };
@@ -0,0 +1,355 @@
1
+ import { HTTP_METHODS } from '../constants/httpMethods.js';
2
+ import { HTTP_RESPONSE_TYPES } from '../constants/httpResponseTypes.js';
3
+ import { HttpClientError } from '../errors/HttpClientError.js';
4
+ import { HttpResponseError } from '../errors/HttpResponseError.js';
5
+ import { NetworkError } from '../errors/NetworkError.js';
6
+ import { ParseError } from '../errors/ParseError.js';
7
+ import { RequestPreparationError } from '../errors/RequestPreparationError.js';
8
+ import { TimeoutError } from '../errors/TimeoutError.js';
9
+ import { AbortError } from '../errors/AbortError.js';
10
+ import { assertValidXhrAdapterOptions } from '../asserts/assertValidXhrAdapterOptions.js';
11
+ import { buildUrl } from '../utilities/buildUrl.js';
12
+ import { getErrorMessage } from '../utilities/getErrorMessage.js';
13
+ import { mergeHeaders } from '../utilities/mergeHeaders.js';
14
+ import { prepareRequestBody } from '../utilities/prepareRequestBody.js';
15
+ import { isStatusAccepted } from '../utilities/isStatusAccepted.js';
16
+
17
+ function parseResponseHeaders(rawHeaders) {
18
+ const result = {};
19
+ const lines = rawHeaders.trim().split('\r\n');
20
+ for (const line of lines) {
21
+ const index = line.indexOf(': ');
22
+ if (index > 0) {
23
+ const key = line.slice(0, index).toLowerCase();
24
+ const value = line.slice(index + 2);
25
+ result[key] = value;
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ function createResponseStream(xhr) {
31
+ let controller;
32
+ let textLength = 0;
33
+ const encoder = new TextEncoder();
34
+ const data = new ReadableStream({
35
+ start(value) {
36
+ controller = value;
37
+ }
38
+ });
39
+ return {
40
+ data,
41
+ append() {
42
+ const text = xhr.responseText;
43
+ const chunk = text.slice(textLength);
44
+ if (chunk) {
45
+ controller?.enqueue(encoder.encode(chunk));
46
+ textLength = text.length;
47
+ }
48
+ },
49
+ close() {
50
+ controller?.close();
51
+ },
52
+ error(reason) {
53
+ controller?.error(reason);
54
+ }
55
+ };
56
+ }
57
+ async function readBlobAsText(blob) {
58
+ if (typeof blob.text === 'function') {
59
+ return blob.text();
60
+ }
61
+ return new Promise((resolve, reject) => {
62
+ const reader = new FileReader();
63
+ reader.onload = () => {
64
+ if (typeof reader.result !== 'string') {
65
+ reject(new TypeError('Failed to read response Blob as text'));
66
+ return;
67
+ }
68
+ resolve(reader.result);
69
+ };
70
+ reader.onerror = () => {
71
+ reject(reader.error ?? new TypeError('Failed to read response Blob as text'));
72
+ };
73
+ reader.readAsText(blob);
74
+ });
75
+ }
76
+ function hasEmptyResponseBody(xhr) {
77
+ return xhr.status === 204 || xhr.getResponseHeader('content-length') === '0';
78
+ }
79
+ function getResponseBody(xhr, config, responseType) {
80
+ if (hasEmptyResponseBody(xhr)) {
81
+ return undefined;
82
+ }
83
+ switch (responseType) {
84
+ case HTTP_RESPONSE_TYPES.ARRAY_BUFFER:
85
+ case HTTP_RESPONSE_TYPES.BLOB:
86
+ return xhr.response;
87
+ case HTTP_RESPONSE_TYPES.TEXT:
88
+ return xhr.response;
89
+ case HTTP_RESPONSE_TYPES.JSON:
90
+ default: {
91
+ const resolvedResponseType = responseType ?? HTTP_RESPONSE_TYPES.JSON;
92
+ const text = xhr.response;
93
+ if (!text) {
94
+ return undefined;
95
+ }
96
+ try {
97
+ return JSON.parse(text);
98
+ }
99
+ catch (error) {
100
+ throw new ParseError('Failed to parse response body as JSON', {
101
+ cause: error,
102
+ config,
103
+ responseType: resolvedResponseType,
104
+ raw: text
105
+ });
106
+ }
107
+ }
108
+ }
109
+ }
110
+ function setResponseType(xhr, responseType) {
111
+ if (responseType === HTTP_RESPONSE_TYPES.ARRAY_BUFFER) {
112
+ // XHR accepts only a lowercase DOM value.
113
+ xhr.responseType = 'arraybuffer';
114
+ }
115
+ else if (responseType === HTTP_RESPONSE_TYPES.BLOB) {
116
+ xhr.responseType = 'blob';
117
+ }
118
+ else {
119
+ xhr.responseType = 'text';
120
+ }
121
+ }
122
+ function assertStreamingSupported(responseType, config) {
123
+ if (responseType === HTTP_RESPONSE_TYPES.STREAM &&
124
+ (typeof ReadableStream === 'undefined' || typeof TextEncoder === 'undefined')) {
125
+ throw new RequestPreparationError('Streaming responses are not supported in this environment', { config });
126
+ }
127
+ }
128
+ function createResponse(xhr, config, data) {
129
+ return {
130
+ data,
131
+ status: xhr.status,
132
+ statusText: xhr.statusText,
133
+ headers: parseResponseHeaders(xhr.getAllResponseHeaders()),
134
+ config
135
+ };
136
+ }
137
+ async function getErrorResponseText(xhr) {
138
+ if (xhr.responseType === 'blob') {
139
+ const blob = xhr.response;
140
+ if (!(blob instanceof Blob)) {
141
+ return undefined;
142
+ }
143
+ return readBlobAsText(blob);
144
+ }
145
+ if (xhr.responseType === 'arraybuffer') {
146
+ const arrayBuffer = xhr.response;
147
+ if (typeof TextDecoder === 'undefined' || !(arrayBuffer instanceof ArrayBuffer)) {
148
+ return undefined;
149
+ }
150
+ return new TextDecoder().decode(arrayBuffer);
151
+ }
152
+ return xhr.responseText;
153
+ }
154
+ async function getErrorData(xhr) {
155
+ try {
156
+ const text = await getErrorResponseText(xhr);
157
+ if (!text) {
158
+ return undefined;
159
+ }
160
+ try {
161
+ return JSON.parse(text);
162
+ }
163
+ catch {
164
+ return text;
165
+ }
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ }
171
+ async function createResponseError(xhr, config) {
172
+ return new HttpResponseError(`Request failed with status code ${xhr.status}`, xhr.status, xhr.statusText, parseResponseHeaders(xhr.getAllResponseHeaders()), config, await getErrorData(xhr));
173
+ }
174
+ function resolveRequestConfig(config, options) {
175
+ return {
176
+ ...config,
177
+ ...(config.responseType === undefined && options.responseType !== undefined
178
+ ? { responseType: options.responseType }
179
+ : {}),
180
+ ...(config.timeout === undefined && options.timeout !== undefined ? { timeout: options.timeout } : {}),
181
+ ...(config.withCredentials === undefined && options.withCredentials !== undefined
182
+ ? { withCredentials: options.withCredentials }
183
+ : {})
184
+ };
185
+ }
186
+ function prepareXhrRequest(config, options) {
187
+ const { url, method, headers = {}, params, data, signal: userSignal, timeout, responseType, baseUrl: baseURL } = config;
188
+ const fullUrl = buildUrl(baseURL, url, params);
189
+ if (userSignal?.aborted) {
190
+ throw new AbortError('Request was aborted', { cause: userSignal.reason, config });
191
+ }
192
+ assertStreamingSupported(responseType, config);
193
+ const xhr = new XMLHttpRequest();
194
+ xhr.open(method, fullUrl, true);
195
+ xhr.withCredentials = config.withCredentials === true;
196
+ if (options.mimeType !== undefined) {
197
+ xhr.overrideMimeType(options.mimeType);
198
+ }
199
+ if (timeout !== undefined && timeout > 0) {
200
+ xhr.timeout = timeout;
201
+ }
202
+ setResponseType(xhr, responseType);
203
+ const requestHeaders = mergeHeaders(headers);
204
+ const body = data !== undefined && method !== HTTP_METHODS.GET && method !== HTTP_METHODS.HEAD
205
+ ? prepareRequestBody(data, requestHeaders)
206
+ : undefined;
207
+ for (const [key, value] of Object.entries(requestHeaders)) {
208
+ xhr.setRequestHeader(key, value);
209
+ }
210
+ const onAbort = () => xhr.abort();
211
+ userSignal?.addEventListener('abort', onAbort, { once: true });
212
+ return {
213
+ xhr,
214
+ body,
215
+ cleanup() {
216
+ userSignal?.removeEventListener('abort', onAbort);
217
+ }
218
+ };
219
+ }
220
+ function configureXhrEventHandlers(request, config, options, resolve, reject) {
221
+ const { xhr } = request;
222
+ const { signal: userSignal, timeout, responseType } = config;
223
+ const { onDownloadProgress, onUploadProgress } = options;
224
+ let responseStream;
225
+ let streamResponseResolved = false;
226
+ let statusValidation;
227
+ const isSuccessful = () => {
228
+ if (statusValidation === undefined) {
229
+ try {
230
+ statusValidation = { accepted: isStatusAccepted(xhr.status, config.validateStatus) };
231
+ }
232
+ catch (error) {
233
+ statusValidation = { error };
234
+ }
235
+ }
236
+ if ('error' in statusValidation) {
237
+ throw statusValidation.error;
238
+ }
239
+ return statusValidation.accepted;
240
+ };
241
+ const resolveStream = () => {
242
+ if (streamResponseResolved || !isSuccessful()) {
243
+ return;
244
+ }
245
+ responseStream = createResponseStream(xhr);
246
+ streamResponseResolved = true;
247
+ resolve(createResponse(xhr, config, responseStream.data));
248
+ };
249
+ const rejectOrFailStream = (error) => {
250
+ request.cleanup();
251
+ if (streamResponseResolved) {
252
+ responseStream?.error(error);
253
+ return;
254
+ }
255
+ reject(error);
256
+ };
257
+ if (responseType === HTTP_RESPONSE_TYPES.STREAM) {
258
+ xhr.onreadystatechange = () => {
259
+ if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
260
+ try {
261
+ resolveStream();
262
+ }
263
+ catch (error) {
264
+ request.cleanup();
265
+ reject(error);
266
+ xhr.abort();
267
+ }
268
+ }
269
+ };
270
+ }
271
+ if (responseType === HTTP_RESPONSE_TYPES.STREAM || onDownloadProgress !== undefined) {
272
+ xhr.onprogress = event => {
273
+ responseStream?.append();
274
+ onDownloadProgress?.(event, config);
275
+ };
276
+ }
277
+ if (request.body !== undefined && onUploadProgress !== undefined) {
278
+ xhr.upload.onprogress = event => {
279
+ onUploadProgress(event, config);
280
+ };
281
+ }
282
+ xhr.onload = async () => {
283
+ request.cleanup();
284
+ let successful;
285
+ try {
286
+ successful = isSuccessful();
287
+ }
288
+ catch (error) {
289
+ reject(error);
290
+ return;
291
+ }
292
+ if (!successful) {
293
+ reject(await createResponseError(xhr, config));
294
+ return;
295
+ }
296
+ if (responseType === HTTP_RESPONSE_TYPES.STREAM) {
297
+ resolveStream();
298
+ responseStream?.append();
299
+ responseStream?.close();
300
+ return;
301
+ }
302
+ try {
303
+ const responseData = getResponseBody(xhr, config, responseType);
304
+ resolve(createResponse(xhr, config, responseData));
305
+ }
306
+ catch (error) {
307
+ reject(error instanceof HttpClientError
308
+ ? error
309
+ : new NetworkError(getErrorMessage(error, 'Network request failed'), { cause: error, config }));
310
+ }
311
+ };
312
+ xhr.onerror = event => {
313
+ rejectOrFailStream(new NetworkError('Network request failed', { cause: event, config }));
314
+ };
315
+ xhr.onabort = event => {
316
+ rejectOrFailStream(new AbortError('Request was aborted', { cause: userSignal?.reason ?? event, config }));
317
+ };
318
+ xhr.ontimeout = event => {
319
+ const timeoutMs = timeout ?? 0;
320
+ rejectOrFailStream(new TimeoutError(`Request timed out after ${timeoutMs}ms`, {
321
+ cause: event,
322
+ config,
323
+ timeout: timeoutMs
324
+ }));
325
+ };
326
+ }
327
+ class XhrAdapter {
328
+ options;
329
+ constructor(options = {}) {
330
+ assertValidXhrAdapterOptions(options);
331
+ this.options = { ...options };
332
+ }
333
+ request(config) {
334
+ const resolvedConfig = resolveRequestConfig(config, this.options);
335
+ return new Promise((resolve, reject) => {
336
+ let request;
337
+ try {
338
+ request = prepareXhrRequest(resolvedConfig, this.options);
339
+ configureXhrEventHandlers(request, resolvedConfig, this.options, resolve, reject);
340
+ request.xhr.send(request.body);
341
+ }
342
+ catch (error) {
343
+ request?.cleanup();
344
+ reject(error instanceof HttpClientError
345
+ ? error
346
+ : new RequestPreparationError('Failed to prepare HTTP request', {
347
+ cause: error,
348
+ config: resolvedConfig
349
+ }));
350
+ }
351
+ });
352
+ }
353
+ }
354
+
355
+ export { XhrAdapter };
@@ -0,0 +1,9 @@
1
+ import { RequestBuilderError } from '../errors/RequestBuilderError.js';
2
+
3
+ function assertNonBlankString(value, message, code) {
4
+ if (typeof value !== 'string' || value.trim().length === 0) {
5
+ throw new RequestBuilderError(message, code);
6
+ }
7
+ }
8
+
9
+ export { assertNonBlankString };
@@ -0,0 +1,13 @@
1
+ import { REQUEST_BUILDER_ERROR_CODES } from '../constants/requestBuilderErrorCodes.js';
2
+ import { RequestBuilderError } from '../errors/RequestBuilderError.js';
3
+
4
+ /** Validates an HTTP client adapter. */
5
+ function assertValidAdapter(adapter) {
6
+ if (adapter === null ||
7
+ (typeof adapter !== 'object' && typeof adapter !== 'function') ||
8
+ typeof adapter.request !== 'function') {
9
+ throw new RequestBuilderError('Adapter must implement a request method', REQUEST_BUILDER_ERROR_CODES.INVALID_ADAPTER);
10
+ }
11
+ }
12
+
13
+ export { assertValidAdapter };
@@ -0,0 +1,8 @@
1
+ import { REQUEST_BUILDER_ERROR_CODES } from '../constants/requestBuilderErrorCodes.js';
2
+ import { assertNonBlankString } from './assertNonBlankString.js';
3
+
4
+ function assertValidBaseUrl(baseUrl) {
5
+ assertNonBlankString(baseUrl, 'Base URL must be a non-empty string', REQUEST_BUILDER_ERROR_CODES.INVALID_BASE_URL);
6
+ }
7
+
8
+ export { assertValidBaseUrl };
@@ -0,0 +1,15 @@
1
+ import { HTTP_METHODS } from '../constants/httpMethods.js';
2
+ import { REQUEST_BUILDER_ERROR_CODES } from '../constants/requestBuilderErrorCodes.js';
3
+ import { RequestBuilderError } from '../errors/RequestBuilderError.js';
4
+
5
+ /** Validates that a request method supports a body and that the body is explicitly defined. */
6
+ function assertValidBody(method, data) {
7
+ if (method === HTTP_METHODS.GET || method === HTTP_METHODS.HEAD) {
8
+ throw new RequestBuilderError('Body is not allowed for GET or HEAD requests', REQUEST_BUILDER_ERROR_CODES.INVALID_BODY);
9
+ }
10
+ if (data === undefined) {
11
+ throw new RequestBuilderError('Body must not be undefined', REQUEST_BUILDER_ERROR_CODES.INVALID_BODY);
12
+ }
13
+ }
14
+
15
+ export { assertValidBody };