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