@nu-art/http-client 0.401.1
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/core/HttpClient.d.ts +92 -0
- package/core/HttpClient.js +157 -0
- package/core/HttpRequest.d.ts +205 -0
- package/core/HttpRequest.js +494 -0
- package/exceptions/HttpException.d.ts +79 -0
- package/exceptions/HttpException.js +99 -0
- package/index.d.ts +8 -0
- package/index.js +17 -0
- package/package.json +62 -0
- package/types/api-types.d.ts +142 -0
- package/types/api-types.js +21 -0
- package/types/error-types.d.ts +46 -0
- package/types/error-types.js +6 -0
- package/types/types.d.ts +26 -0
- package/types/types.js +6 -0
- package/utils/http-codes.d.ts +197 -0
- package/utils/http-codes.js +125 -0
- package/utils/utils.d.ts +42 -0
- package/utils/utils.js +75 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @nu-art/thunderstorm-http - A robust and type-safe HTTP client for Thunderstorm applications
|
|
3
|
+
* Copyright (C) 2024 Adam van der Kruk aka TacB0sS
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
*/
|
|
6
|
+
// noinspection TypeScriptPreferShortImport
|
|
7
|
+
import { _keys, asArray, BadImplementationException, exists, isErrorOfType, Logger, MimeType_json } from '@nu-art/ts-common';
|
|
8
|
+
import { composeUrl } from '../utils/utils.js';
|
|
9
|
+
// Axios v1+ import style
|
|
10
|
+
import axios, { CanceledError } from 'axios';
|
|
11
|
+
import { HttpException } from '../exceptions/HttpException.js';
|
|
12
|
+
import { HttpMethod } from '../types/api-types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Typed HTTP request with fluent builder API and comprehensive logging.
|
|
15
|
+
*
|
|
16
|
+
* Provides a type-safe, chainable interface for building and executing HTTP requests.
|
|
17
|
+
* Extends Logger for built-in request lifecycle logging (verbose, debug, info, warning, error).
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Type-safe request/response handling via TypedApi generics
|
|
21
|
+
* - Fluent builder pattern for configuration
|
|
22
|
+
* - Automatic JSON parsing of responses
|
|
23
|
+
* - Request cancellation via AbortController
|
|
24
|
+
* - Callback chaining for error and completion handlers
|
|
25
|
+
* - Comprehensive logging at all stages
|
|
26
|
+
*
|
|
27
|
+
* @template API - Typed API definition specifying method, response, body, params, and error types
|
|
28
|
+
*/
|
|
29
|
+
export class HttpRequest extends Logger {
|
|
30
|
+
key;
|
|
31
|
+
requestData;
|
|
32
|
+
origin;
|
|
33
|
+
headers = {};
|
|
34
|
+
method = HttpMethod.GET;
|
|
35
|
+
timeout = 10000;
|
|
36
|
+
body;
|
|
37
|
+
url;
|
|
38
|
+
params = {};
|
|
39
|
+
responseType;
|
|
40
|
+
label;
|
|
41
|
+
onProgressListener;
|
|
42
|
+
aborted = false;
|
|
43
|
+
compress;
|
|
44
|
+
onCompleted;
|
|
45
|
+
onError;
|
|
46
|
+
response;
|
|
47
|
+
cancelController;
|
|
48
|
+
status;
|
|
49
|
+
requestOption = {};
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new HTTP request instance.
|
|
52
|
+
*
|
|
53
|
+
* Automatically extends timeout to 5 minutes in debug mode for development.
|
|
54
|
+
* Initializes AbortController for request cancellation support.
|
|
55
|
+
*
|
|
56
|
+
* @param requestKey - Identifier for this request (used in logging)
|
|
57
|
+
* @param requestData - Optional data associated with the request
|
|
58
|
+
* @param shouldCompress - Whether to enable compression (default: false)
|
|
59
|
+
*/
|
|
60
|
+
constructor(requestKey, requestData, shouldCompress) {
|
|
61
|
+
const label = `http request: ${requestKey}${requestData ? ` ${requestData}` : ''}`;
|
|
62
|
+
super(label);
|
|
63
|
+
this.key = requestKey;
|
|
64
|
+
this.requestData = requestData;
|
|
65
|
+
this.label = label;
|
|
66
|
+
this.compress = shouldCompress === undefined ? false : shouldCompress;
|
|
67
|
+
this.cancelController = new AbortController();
|
|
68
|
+
this.logVerbose('HttpRequest created', { key: requestKey, requestData, compress: this.compress, timeout: this.timeout });
|
|
69
|
+
}
|
|
70
|
+
resolveTypedException(exception) {
|
|
71
|
+
if (isErrorOfType(exception, HttpException))
|
|
72
|
+
return exception.errorResponse?.error;
|
|
73
|
+
}
|
|
74
|
+
getRequestData() {
|
|
75
|
+
return this.requestData;
|
|
76
|
+
}
|
|
77
|
+
setOrigin(origin) {
|
|
78
|
+
this.origin = origin;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
setOnProgressListener(onProgressListener) {
|
|
82
|
+
this.onProgressListener = onProgressListener;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
setLabel(label) {
|
|
86
|
+
this.label = label;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
setMethod(method) {
|
|
90
|
+
this.method = method;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
setResponseType(responseType) {
|
|
94
|
+
this.responseType = responseType;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
setUrlParams(params) {
|
|
98
|
+
if (!params)
|
|
99
|
+
return this;
|
|
100
|
+
_keys(params).forEach((key) => {
|
|
101
|
+
const param = params[key];
|
|
102
|
+
return param && typeof param === 'string' && this.setUrlParam(key, param);
|
|
103
|
+
});
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
setUrlParam(key, value) {
|
|
107
|
+
delete this.params[key];
|
|
108
|
+
this.params[key] = value;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
setUrl(url) {
|
|
112
|
+
this.url = url;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
getUrl() {
|
|
116
|
+
return this.url;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Sets a relative URL path, composing it with the origin.
|
|
120
|
+
*
|
|
121
|
+
* Automatically removes leading slashes from the relative path and combines
|
|
122
|
+
* with origin. Requires origin to be set first.
|
|
123
|
+
*
|
|
124
|
+
* @param relativeUrl - Relative path (leading slash is removed if present)
|
|
125
|
+
* @returns This instance for chaining
|
|
126
|
+
* @throws BadImplementationException if origin is not set
|
|
127
|
+
*/
|
|
128
|
+
setRelativeUrl(relativeUrl) {
|
|
129
|
+
if (!this.origin)
|
|
130
|
+
throw new BadImplementationException('if you want to use relative urls, you need to set an origin');
|
|
131
|
+
if (relativeUrl.startsWith('/'))
|
|
132
|
+
relativeUrl = relativeUrl.substring(1);
|
|
133
|
+
this.url = `${this.origin}/${relativeUrl}`;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
setTimeout(timeout) {
|
|
137
|
+
this.timeout = timeout;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
setHeaders(headers) {
|
|
141
|
+
if (!headers)
|
|
142
|
+
return this;
|
|
143
|
+
Object.keys(headers).forEach((key) => this.setHeader(key, headers[key]));
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
addHeaders(headers) {
|
|
147
|
+
if (!headers)
|
|
148
|
+
return this;
|
|
149
|
+
Object.keys(headers).forEach((key) => this.addHeader(key, headers[key]));
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Sets a header, replacing any existing values for the key.
|
|
154
|
+
*
|
|
155
|
+
* Header keys are normalized to lowercase. Multiple calls to setHeader
|
|
156
|
+
* for the same key will replace previous values.
|
|
157
|
+
*
|
|
158
|
+
* @param _key - Header name (case-insensitive, normalized to lowercase)
|
|
159
|
+
* @param value - Header value(s)
|
|
160
|
+
* @returns This instance for chaining
|
|
161
|
+
*/
|
|
162
|
+
setHeader(_key, value) {
|
|
163
|
+
const key = _key.toLowerCase();
|
|
164
|
+
delete this.headers[key];
|
|
165
|
+
return this.addHeader(key, value);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Adds a header value, appending to existing values if the key already exists.
|
|
169
|
+
*
|
|
170
|
+
* Header keys are normalized to lowercase. Multiple values for the same header
|
|
171
|
+
* are joined with '; ' when the request is executed.
|
|
172
|
+
*
|
|
173
|
+
* @param _key - Header name (case-insensitive, normalized to lowercase)
|
|
174
|
+
* @param value - Header value(s) to append
|
|
175
|
+
* @returns This instance for chaining
|
|
176
|
+
*/
|
|
177
|
+
addHeader(_key, value) {
|
|
178
|
+
const key = _key.toLowerCase();
|
|
179
|
+
return this._addHeaderImpl(key, value);
|
|
180
|
+
}
|
|
181
|
+
removeHeader(key) {
|
|
182
|
+
delete this.headers[key];
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
_addHeaderImpl(key, value) {
|
|
186
|
+
const values = asArray(value);
|
|
187
|
+
if (!this.headers[key])
|
|
188
|
+
this.headers[key] = values;
|
|
189
|
+
else
|
|
190
|
+
this.headers[key].push(...values);
|
|
191
|
+
this.logVerbose(`Added ${value} header`, value);
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
prepareJsonBody(bodyObject) {
|
|
195
|
+
return bodyObject;
|
|
196
|
+
}
|
|
197
|
+
setBodyAsJson(bodyObject, compress) {
|
|
198
|
+
this.setHeader('content-type', MimeType_json);
|
|
199
|
+
this.setBody(this.prepareJsonBody(bodyObject), compress);
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Sets the request body.
|
|
204
|
+
*
|
|
205
|
+
* If compression is enabled and body is a string, automatically adds
|
|
206
|
+
* 'Content-encoding: gzip' header.
|
|
207
|
+
*
|
|
208
|
+
* @param bodyAsString - Request body (any type)
|
|
209
|
+
* @param _compress - Override compression setting (optional)
|
|
210
|
+
* @returns This instance for chaining
|
|
211
|
+
*/
|
|
212
|
+
setBody(bodyAsString, _compress) {
|
|
213
|
+
this.body = bodyAsString;
|
|
214
|
+
this.compress = _compress === undefined ? this.compress : _compress;
|
|
215
|
+
if (typeof bodyAsString === 'string') {
|
|
216
|
+
if (this.compress)
|
|
217
|
+
this.setHeader('Content-encoding', 'gzip');
|
|
218
|
+
// Set content-type for plain text if not already set
|
|
219
|
+
if (!this.headers['content-type'] && !this.headers['Content-Type'])
|
|
220
|
+
this.setHeader('Content-Type', 'text/plain');
|
|
221
|
+
}
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
isValidStatus(statusCode) {
|
|
225
|
+
return statusCode >= 200 && statusCode < 300;
|
|
226
|
+
}
|
|
227
|
+
print() {
|
|
228
|
+
this.logInfo(`Url: ${this.url}`);
|
|
229
|
+
this.logInfo(`Params:`, this.params);
|
|
230
|
+
this.logInfo(`Headers:`, this.headers);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Executes the HTTP request and returns the typed response.
|
|
234
|
+
*
|
|
235
|
+
* Process:
|
|
236
|
+
* 1. Validates request isn't aborted
|
|
237
|
+
* 2. Composes full URL with query parameters
|
|
238
|
+
* 3. Prepares headers (joins multiple values with '; ')
|
|
239
|
+
* 4. Sends request via Axios
|
|
240
|
+
* 5. Validates response status (200-299 considered success)
|
|
241
|
+
* 6. Attempts JSON parsing (falls back to raw response if not JSON)
|
|
242
|
+
* 7. Calls completion/error callbacks
|
|
243
|
+
*
|
|
244
|
+
* Automatically handles:
|
|
245
|
+
* - Request cancellation (AbortController)
|
|
246
|
+
* - Error response parsing
|
|
247
|
+
* - JSON response parsing
|
|
248
|
+
* - Callback execution (onError for failures, onCompleted for success)
|
|
249
|
+
*
|
|
250
|
+
* @param print - If true, logs request details (URL, params, headers) before execution
|
|
251
|
+
* @returns Typed response data (API['R'])
|
|
252
|
+
* @throws HttpException if request fails or returns non-2xx status
|
|
253
|
+
*/
|
|
254
|
+
async execute(print = false) {
|
|
255
|
+
this.logVerbose('Executing HTTP request', { method: this.method, key: this.key });
|
|
256
|
+
if (print)
|
|
257
|
+
this.print();
|
|
258
|
+
if (this.aborted) {
|
|
259
|
+
this.logWarning('Request was aborted before execution');
|
|
260
|
+
const httpException = new HttpException(0, this);
|
|
261
|
+
await this.onError?.(httpException);
|
|
262
|
+
throw httpException;
|
|
263
|
+
}
|
|
264
|
+
const fullUrl = composeUrl(this.url, this.params);
|
|
265
|
+
const body = this.body;
|
|
266
|
+
this.logDebug('Composed URL', { url: this.url, params: this.params, fullUrl });
|
|
267
|
+
if (typeof body === 'string') {
|
|
268
|
+
this.addHeader('Content-Length', `${body.length}`);
|
|
269
|
+
}
|
|
270
|
+
const headers = Object.keys(this.headers).reduce((carry, headerKey) => {
|
|
271
|
+
carry[headerKey] = this.headers[headerKey].join('; ');
|
|
272
|
+
return carry;
|
|
273
|
+
}, {});
|
|
274
|
+
this.logDebug('Prepared headers', headers);
|
|
275
|
+
const options = {
|
|
276
|
+
...this.requestOption,
|
|
277
|
+
url: fullUrl,
|
|
278
|
+
method: this.method,
|
|
279
|
+
headers,
|
|
280
|
+
timeout: this.timeout,
|
|
281
|
+
signal: this.cancelController.signal,
|
|
282
|
+
};
|
|
283
|
+
if (body) {
|
|
284
|
+
options.data = body;
|
|
285
|
+
this.logVerbose('Request body set', { bodyType: typeof body, bodyLength: typeof body === 'string' ? body.length : 'object' });
|
|
286
|
+
}
|
|
287
|
+
if (this.responseType) {
|
|
288
|
+
options.responseType = this.responseType;
|
|
289
|
+
this.logVerbose(`Response type set: ${this.responseType}`);
|
|
290
|
+
}
|
|
291
|
+
// Set up progress listener if configured
|
|
292
|
+
if (this.onProgressListener) {
|
|
293
|
+
options.onDownloadProgress = (progressEvent) => {
|
|
294
|
+
this.onProgressListener({
|
|
295
|
+
loaded: progressEvent.loaded || 0,
|
|
296
|
+
total: progressEvent.total || 0,
|
|
297
|
+
lengthComputable: progressEvent.lengthComputable !== false,
|
|
298
|
+
target: progressEvent
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
options.onUploadProgress = (progressEvent) => {
|
|
302
|
+
this.onProgressListener({
|
|
303
|
+
loaded: progressEvent.loaded || 0,
|
|
304
|
+
total: progressEvent.total || 0,
|
|
305
|
+
lengthComputable: progressEvent.lengthComputable !== false,
|
|
306
|
+
target: progressEvent
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
this.logDebug('Request options', options);
|
|
311
|
+
this.logInfo(`Calling: ${this.method} - ${fullUrl}`);
|
|
312
|
+
try {
|
|
313
|
+
this.response = await axios.request(options);
|
|
314
|
+
this.status = this.response?.status ?? 200;
|
|
315
|
+
this.logVerbose('Response received', { status: this.status, headers: this.response.headers });
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
// cancellation path in v1
|
|
319
|
+
if (e instanceof CanceledError || e?.code === 'ERR_CANCELED') {
|
|
320
|
+
this.aborted = true;
|
|
321
|
+
this.logWarning('Request cancelled', e.message);
|
|
322
|
+
const httpException = new HttpException(0, this);
|
|
323
|
+
await this.onError?.(httpException);
|
|
324
|
+
throw httpException;
|
|
325
|
+
}
|
|
326
|
+
// Extract response and status from Axios error
|
|
327
|
+
this.response = e?.response;
|
|
328
|
+
this.status = this.response?.status ?? 500;
|
|
329
|
+
this.logError('Request failed', { status: this.status, error: e.message });
|
|
330
|
+
// Convert AxiosError to HttpException for non-2xx status codes
|
|
331
|
+
if (!this.isValidStatus(this.status)) {
|
|
332
|
+
const errorResponse = this.getErrorResponse();
|
|
333
|
+
const httpException = new HttpException(this.status, this, errorResponse);
|
|
334
|
+
await this.onError?.(httpException);
|
|
335
|
+
throw httpException;
|
|
336
|
+
}
|
|
337
|
+
// For other errors (network, timeout, etc.), still throw HttpException
|
|
338
|
+
const errorResponse = this.getErrorResponse();
|
|
339
|
+
const httpException = new HttpException(this.status, this, errorResponse);
|
|
340
|
+
await this.onError?.(httpException);
|
|
341
|
+
throw httpException;
|
|
342
|
+
}
|
|
343
|
+
const status = this.getStatus();
|
|
344
|
+
this.logDebug('Response status', { status });
|
|
345
|
+
if (this.aborted) {
|
|
346
|
+
this.logWarning('Request was aborted after response');
|
|
347
|
+
const httpException = new HttpException(status, this);
|
|
348
|
+
await this.onError?.(httpException);
|
|
349
|
+
throw httpException;
|
|
350
|
+
}
|
|
351
|
+
// Status validation is already handled in catch block for errors
|
|
352
|
+
// This check is for successful responses that might have invalid status (shouldn't happen)
|
|
353
|
+
if (!this.isValidStatus(status)) {
|
|
354
|
+
this.logWarning('Invalid response status', { status, expectedRange: '200-299' });
|
|
355
|
+
const errorResponse = this.getErrorResponse();
|
|
356
|
+
const httpException = new HttpException(status, this, errorResponse);
|
|
357
|
+
await this.onError?.(httpException);
|
|
358
|
+
throw httpException;
|
|
359
|
+
}
|
|
360
|
+
let response = this.getResponse();
|
|
361
|
+
const requestData = this.body || this.params;
|
|
362
|
+
if (!exists(response)) {
|
|
363
|
+
this.logVerbose('Empty response received');
|
|
364
|
+
await this.onCompleted?.(response, requestData, this);
|
|
365
|
+
return response;
|
|
366
|
+
}
|
|
367
|
+
// Convert Buffer to ArrayBuffer for arraybuffer response type (Node.js compatibility)
|
|
368
|
+
if (this.responseType === 'arraybuffer' && Buffer.isBuffer(response)) {
|
|
369
|
+
const buffer = response;
|
|
370
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
371
|
+
response = arrayBuffer;
|
|
372
|
+
this.logVerbose('Converted Buffer to ArrayBuffer for arraybuffer response type');
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
response = JSON.parse(response);
|
|
376
|
+
this.logVerbose('Response parsed as JSON');
|
|
377
|
+
}
|
|
378
|
+
catch (ignore) {
|
|
379
|
+
this.logVerbose('Response is not JSON, returning as-is');
|
|
380
|
+
}
|
|
381
|
+
this.logInfo(`Request completed successfully`, { status, method: this.method, url: fullUrl });
|
|
382
|
+
await this.onCompleted?.(response, requestData, this);
|
|
383
|
+
return response;
|
|
384
|
+
}
|
|
385
|
+
clearOnCompleted = () => {
|
|
386
|
+
delete this.onCompleted;
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Generic callback chaining helper.
|
|
390
|
+
*
|
|
391
|
+
* Chains multiple callbacks of the same type, executing them in order.
|
|
392
|
+
* If an existing callback exists, both are executed (existing first, then new).
|
|
393
|
+
*
|
|
394
|
+
* @template T - Callback function type
|
|
395
|
+
* @param existingCallback - Previously set callback (if any)
|
|
396
|
+
* @param newCallback - New callback to add
|
|
397
|
+
* @param setter - Function to set the final callback
|
|
398
|
+
* @returns This instance for chaining
|
|
399
|
+
*/
|
|
400
|
+
setCallback(existingCallback, newCallback, setter) {
|
|
401
|
+
if (!newCallback)
|
|
402
|
+
return this;
|
|
403
|
+
if (existingCallback && newCallback) {
|
|
404
|
+
const _existing = existingCallback;
|
|
405
|
+
setter((async (...args) => {
|
|
406
|
+
await _existing(...args);
|
|
407
|
+
await newCallback(...args);
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
else
|
|
411
|
+
setter(newCallback);
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
414
|
+
setOnCompleted = (onCompleted) => {
|
|
415
|
+
return this.setCallback(this.onCompleted, onCompleted, (callback) => {
|
|
416
|
+
this.onCompleted = callback;
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
setOnError(onError) {
|
|
420
|
+
return this.setCallback(this.onError, onError, (callback) => {
|
|
421
|
+
this.onError = callback;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
getStatus() {
|
|
425
|
+
if (!this.status)
|
|
426
|
+
throw new BadImplementationException('Missing status..');
|
|
427
|
+
return this.status;
|
|
428
|
+
}
|
|
429
|
+
getResponse() {
|
|
430
|
+
return this.response?.data;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Gets the full Axios response object.
|
|
434
|
+
*
|
|
435
|
+
* Returns the complete Axios response including:
|
|
436
|
+
* - status, statusText
|
|
437
|
+
* - headers (full object)
|
|
438
|
+
* - data (response body)
|
|
439
|
+
* - config (request configuration)
|
|
440
|
+
*
|
|
441
|
+
* Useful when you need access to response metadata beyond just the body,
|
|
442
|
+
* such as response headers, status text, or the full request configuration.
|
|
443
|
+
*
|
|
444
|
+
* @returns Full Axios response object
|
|
445
|
+
* @throws BadImplementationException if response is not yet available (execute() hasn't been called)
|
|
446
|
+
*/
|
|
447
|
+
getRawResponse() {
|
|
448
|
+
if (!this.response)
|
|
449
|
+
throw new BadImplementationException('Response not available. Call execute() first.');
|
|
450
|
+
return this.response;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Extracts error response from the HTTP response.
|
|
454
|
+
*
|
|
455
|
+
* Returns a basic error response structure with the raw response data
|
|
456
|
+
* as the debug message. Used when status validation fails.
|
|
457
|
+
*
|
|
458
|
+
* @returns Error response structure with debug message
|
|
459
|
+
*/
|
|
460
|
+
getErrorResponse() {
|
|
461
|
+
return { debugMessage: this.getResponse() };
|
|
462
|
+
}
|
|
463
|
+
abortImpl() {
|
|
464
|
+
this.cancelController.abort();
|
|
465
|
+
}
|
|
466
|
+
setRequestOption(requestOption) {
|
|
467
|
+
this.requestOption = requestOption;
|
|
468
|
+
return this;
|
|
469
|
+
}
|
|
470
|
+
_getResponseHeader(headerKey) {
|
|
471
|
+
if (!this.response)
|
|
472
|
+
throw new BadImplementationException(`axios didn't return yet`);
|
|
473
|
+
return this.response.headers?.[headerKey];
|
|
474
|
+
}
|
|
475
|
+
getResponseHeader(headerKey) {
|
|
476
|
+
try {
|
|
477
|
+
return this._getResponseHeader(headerKey);
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
this.logError(`Response headers not available yet. Request may not have completed.`, e);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Aborts the HTTP request.
|
|
485
|
+
*
|
|
486
|
+
* Marks the request as aborted and triggers the AbortController signal,
|
|
487
|
+
* which cancels the underlying Axios request. If executed before the request
|
|
488
|
+
* completes, the execute() method will throw an HttpException with status 0.
|
|
489
|
+
*/
|
|
490
|
+
abort() {
|
|
491
|
+
this.aborted = true;
|
|
492
|
+
this.abortImpl();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CustomException } from '@nu-art/ts-common';
|
|
2
|
+
import type { HttpRequest } from '../core/HttpRequest.js';
|
|
3
|
+
import type { ApiError_GeneralErrorMessage, ApiErrorResponse, ResponseError } from '../types/error-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* HTTP exception containing error details and the original request.
|
|
6
|
+
*
|
|
7
|
+
* Provides complete context for error handling including the request that failed,
|
|
8
|
+
* allowing error handlers to retry, inspect request details, or perform recovery operations.
|
|
9
|
+
*
|
|
10
|
+
* @template E - Response error type
|
|
11
|
+
*/
|
|
12
|
+
export declare class HttpException<E extends ResponseError = ResponseError> extends CustomException {
|
|
13
|
+
/** HTTP status code */
|
|
14
|
+
responseCode: number;
|
|
15
|
+
/** Error response from server (if available) */
|
|
16
|
+
errorResponse?: ApiErrorResponse<E>;
|
|
17
|
+
/** The HTTP request that failed - provides access to method, headers, URL, body, params, etc. */
|
|
18
|
+
request: HttpRequest<any>;
|
|
19
|
+
constructor(responseCode: number, request: HttpRequest<any>, errorResponse?: ApiErrorResponse<E>);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Exception class for API errors with HTTP response codes and structured error bodies.
|
|
23
|
+
*
|
|
24
|
+
* Used for errors that need to be returned to API clients with:
|
|
25
|
+
* - HTTP status code
|
|
26
|
+
* - Structured error response body
|
|
27
|
+
* - Debug message for server-side logging
|
|
28
|
+
*
|
|
29
|
+
* The constructor accepts `causeOrMessage` as either a string (message) or Error (cause),
|
|
30
|
+
* allowing flexible error construction. If both `causeOrMessage` (as Error) and `cause`
|
|
31
|
+
* are provided, `causeOrMessage` takes precedence.
|
|
32
|
+
*
|
|
33
|
+
* @template Err - Type of the error body (must extend ResponseError)
|
|
34
|
+
*
|
|
35
|
+
* @category Exceptions
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // With message string
|
|
40
|
+
* throw new ApiException(404, 'Resource not found');
|
|
41
|
+
*
|
|
42
|
+
* // With cause Error
|
|
43
|
+
* throw new ApiException(500, originalError);
|
|
44
|
+
*
|
|
45
|
+
* // With both message and cause
|
|
46
|
+
* throw new ApiException(400, 'Invalid input', validationError);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare class ApiException<Err extends ResponseError = ApiError_GeneralErrorMessage> extends CustomException {
|
|
50
|
+
/** Structured error response body for API clients */
|
|
51
|
+
readonly responseBody: ApiErrorResponse<Err>;
|
|
52
|
+
/** HTTP status code for the error response */
|
|
53
|
+
readonly responseCode: number;
|
|
54
|
+
/**
|
|
55
|
+
* Sets the error body and returns this instance for method chaining.
|
|
56
|
+
*
|
|
57
|
+
* @param errorBody - Error body object to include in the response
|
|
58
|
+
* @returns This instance for chaining
|
|
59
|
+
*/
|
|
60
|
+
readonly setErrorBody: (errorBody: Err) => this;
|
|
61
|
+
/**
|
|
62
|
+
* Creates a new ApiException.
|
|
63
|
+
*
|
|
64
|
+
* @param responseCode - HTTP status code (e.g., 404, 500)
|
|
65
|
+
* @param causeOrMessage - Either a message string or an Error object (as cause)
|
|
66
|
+
* @param cause - Optional additional cause Error (only used if causeOrMessage is a string)
|
|
67
|
+
*/
|
|
68
|
+
constructor(responseCode: number, causeOrMessage?: string | Error, cause?: Error);
|
|
69
|
+
/**
|
|
70
|
+
* Extracts message string from causeOrMessage if it's a string.
|
|
71
|
+
*/
|
|
72
|
+
private static getMessage;
|
|
73
|
+
/**
|
|
74
|
+
* Extracts cause Error from parameters.
|
|
75
|
+
* If causeOrMessage is an Error, it's used as the cause.
|
|
76
|
+
* Otherwise, the cause parameter is used.
|
|
77
|
+
*/
|
|
78
|
+
private static getCause;
|
|
79
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @nu-art/thunderstorm-http - A robust and type-safe HTTP client for Thunderstorm applications
|
|
3
|
+
* Copyright (C) 2024 Adam van der Kruk aka TacB0sS
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
*/
|
|
6
|
+
import { _logger_logException, CustomException } from '@nu-art/ts-common';
|
|
7
|
+
/**
|
|
8
|
+
* HTTP exception containing error details and the original request.
|
|
9
|
+
*
|
|
10
|
+
* Provides complete context for error handling including the request that failed,
|
|
11
|
+
* allowing error handlers to retry, inspect request details, or perform recovery operations.
|
|
12
|
+
*
|
|
13
|
+
* @template E - Response error type
|
|
14
|
+
*/
|
|
15
|
+
export class HttpException extends CustomException {
|
|
16
|
+
/** HTTP status code */
|
|
17
|
+
responseCode;
|
|
18
|
+
/** Error response from server (if available) */
|
|
19
|
+
errorResponse;
|
|
20
|
+
/** The HTTP request that failed - provides access to method, headers, URL, body, params, etc. */
|
|
21
|
+
request;
|
|
22
|
+
constructor(responseCode, request, errorResponse) {
|
|
23
|
+
const url = request.getUrl();
|
|
24
|
+
super(HttpException, `${responseCode} - ${url}`);
|
|
25
|
+
this.responseCode = responseCode;
|
|
26
|
+
this.errorResponse = errorResponse;
|
|
27
|
+
this.request = request;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Exception class for API errors with HTTP response codes and structured error bodies.
|
|
32
|
+
*
|
|
33
|
+
* Used for errors that need to be returned to API clients with:
|
|
34
|
+
* - HTTP status code
|
|
35
|
+
* - Structured error response body
|
|
36
|
+
* - Debug message for server-side logging
|
|
37
|
+
*
|
|
38
|
+
* The constructor accepts `causeOrMessage` as either a string (message) or Error (cause),
|
|
39
|
+
* allowing flexible error construction. If both `causeOrMessage` (as Error) and `cause`
|
|
40
|
+
* are provided, `causeOrMessage` takes precedence.
|
|
41
|
+
*
|
|
42
|
+
* @template Err - Type of the error body (must extend ResponseError)
|
|
43
|
+
*
|
|
44
|
+
* @category Exceptions
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* // With message string
|
|
49
|
+
* throw new ApiException(404, 'Resource not found');
|
|
50
|
+
*
|
|
51
|
+
* // With cause Error
|
|
52
|
+
* throw new ApiException(500, originalError);
|
|
53
|
+
*
|
|
54
|
+
* // With both message and cause
|
|
55
|
+
* throw new ApiException(400, 'Invalid input', validationError);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class ApiException extends CustomException {
|
|
59
|
+
/** Structured error response body for API clients */
|
|
60
|
+
responseBody = {};
|
|
61
|
+
/** HTTP status code for the error response */
|
|
62
|
+
responseCode;
|
|
63
|
+
/**
|
|
64
|
+
* Sets the error body and returns this instance for method chaining.
|
|
65
|
+
*
|
|
66
|
+
* @param errorBody - Error body object to include in the response
|
|
67
|
+
* @returns This instance for chaining
|
|
68
|
+
*/
|
|
69
|
+
setErrorBody = (errorBody) => {
|
|
70
|
+
this.responseBody.error = errorBody;
|
|
71
|
+
return this;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Creates a new ApiException.
|
|
75
|
+
*
|
|
76
|
+
* @param responseCode - HTTP status code (e.g., 404, 500)
|
|
77
|
+
* @param causeOrMessage - Either a message string or an Error object (as cause)
|
|
78
|
+
* @param cause - Optional additional cause Error (only used if causeOrMessage is a string)
|
|
79
|
+
*/
|
|
80
|
+
constructor(responseCode, causeOrMessage, cause) {
|
|
81
|
+
super(ApiException, `${responseCode}${ApiException.getMessage(causeOrMessage)}`, ApiException.getCause(causeOrMessage, cause));
|
|
82
|
+
this.responseCode = responseCode;
|
|
83
|
+
this.responseBody.debugMessage = _logger_logException(this);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extracts message string from causeOrMessage if it's a string.
|
|
87
|
+
*/
|
|
88
|
+
static getMessage(causeOrMessage) {
|
|
89
|
+
return typeof causeOrMessage === 'string' ? `-${JSON.stringify(causeOrMessage)}` : '';
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extracts cause Error from parameters.
|
|
93
|
+
* If causeOrMessage is an Error, it's used as the cause.
|
|
94
|
+
* Otherwise, the cause parameter is used.
|
|
95
|
+
*/
|
|
96
|
+
static getCause(causeOrMessage, cause) {
|
|
97
|
+
return typeof causeOrMessage != 'string' ? causeOrMessage : cause;
|
|
98
|
+
}
|
|
99
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './core/HttpClient.js';
|
|
2
|
+
export * from './core/HttpRequest.js';
|
|
3
|
+
export * from './types/api-types.js';
|
|
4
|
+
export * from './types/error-types.js';
|
|
5
|
+
export * from './types/types.js';
|
|
6
|
+
export * from './exceptions/HttpException.js';
|
|
7
|
+
export * from './utils/http-codes.js';
|
|
8
|
+
export * from './utils/utils.js';
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @nu-art/thunderstorm-http - A robust and type-safe HTTP client for Thunderstorm applications
|
|
3
|
+
* Copyright (C) 2024 Adam van der Kruk aka TacB0sS
|
|
4
|
+
* Licensed under the Apache License, Version 2.0
|
|
5
|
+
*/
|
|
6
|
+
// Core classes
|
|
7
|
+
export * from './core/HttpClient.js';
|
|
8
|
+
export * from './core/HttpRequest.js';
|
|
9
|
+
// Types
|
|
10
|
+
export * from './types/api-types.js';
|
|
11
|
+
export * from './types/error-types.js';
|
|
12
|
+
export * from './types/types.js';
|
|
13
|
+
// Exceptions
|
|
14
|
+
export * from './exceptions/HttpException.js';
|
|
15
|
+
// Utils
|
|
16
|
+
export * from './utils/http-codes.js';
|
|
17
|
+
export * from './utils/utils.js';
|