fix(core): Improve streaming support and timeout handling; add browser streaming & timeout tests and README clarifications
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartrequest',
|
||||
version: '4.3.0',
|
||||
version: '4.3.1',
|
||||
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
|
||||
}
|
||||
|
@@ -164,7 +164,7 @@ export class SmartRequest<T = any> {
|
||||
/**
|
||||
* Provide a custom function to handle raw request streaming
|
||||
* This gives full control over the request body streaming
|
||||
* Note: Only works in Node.js environment
|
||||
* Note: Only works in Node.js environment, not supported in browsers
|
||||
*/
|
||||
raw(streamFunc: RawStreamFunction): this {
|
||||
// Store the raw streaming function to be used later
|
||||
|
@@ -9,6 +9,9 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
types.ICoreRequestOptions,
|
||||
CoreResponse
|
||||
> {
|
||||
private timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
|
||||
constructor(url: string, options: types.ICoreRequestOptions = {}) {
|
||||
super(url, options);
|
||||
|
||||
@@ -61,11 +64,19 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
if (
|
||||
typeof this.options.requestBody === 'string' ||
|
||||
this.options.requestBody instanceof ArrayBuffer ||
|
||||
this.options.requestBody instanceof Uint8Array ||
|
||||
this.options.requestBody instanceof FormData ||
|
||||
this.options.requestBody instanceof URLSearchParams ||
|
||||
this.options.requestBody instanceof ReadableStream
|
||||
this.options.requestBody instanceof ReadableStream ||
|
||||
// Check for Buffer (Node.js polyfills in browser may provide this)
|
||||
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
|
||||
) {
|
||||
fetchOptions.body = this.options.requestBody;
|
||||
|
||||
// If streaming, we need to set duplex mode
|
||||
if (this.options.requestBody instanceof ReadableStream) {
|
||||
(fetchOptions as any).duplex = 'half';
|
||||
}
|
||||
} else {
|
||||
// Convert objects to JSON
|
||||
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
||||
@@ -92,9 +103,13 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
||||
const timeout =
|
||||
this.options.hardDataCuttingTimeout || this.options.timeout;
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
fetchOptions.signal = controller.signal;
|
||||
this.abortController = new AbortController();
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
}, timeout);
|
||||
fetchOptions.signal = this.abortController.signal;
|
||||
}
|
||||
|
||||
return fetchOptions;
|
||||
@@ -117,8 +132,12 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
// Clear timeout on successful response
|
||||
this.clearTimeout();
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Clear timeout on error
|
||||
this.clearTimeout();
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out');
|
||||
}
|
||||
@@ -126,6 +145,19 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the timeout and abort controller
|
||||
*/
|
||||
private clearTimeout(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
if (this.abortController) {
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
|
@@ -119,10 +119,11 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
}
|
||||
|
||||
// Perform the request
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
const request = requestModule.request(this.options, async (response) => {
|
||||
// Handle hard timeout
|
||||
if (this.options.hardDataCuttingTimeout) {
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
response.destroy();
|
||||
done.reject(new Error('Request timed out'));
|
||||
}, this.options.hardDataCuttingTimeout);
|
||||
@@ -132,6 +133,14 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
done.resolve(response);
|
||||
});
|
||||
|
||||
// Set request timeout (Node.js built-in timeout)
|
||||
if (this.options.timeout) {
|
||||
request.setTimeout(this.options.timeout, () => {
|
||||
request.destroy();
|
||||
done.reject(new Error('Request timed out'));
|
||||
});
|
||||
}
|
||||
|
||||
// Write request body
|
||||
if (this.options.requestBody) {
|
||||
if (this.options.requestBody instanceof plugins.formData) {
|
||||
@@ -159,11 +168,23 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
request.on('error', (e) => {
|
||||
console.error(e);
|
||||
request.destroy();
|
||||
// Clear timeout on error
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
done.reject(e);
|
||||
});
|
||||
|
||||
// Get response and handle response errors
|
||||
const response = await done.promise;
|
||||
|
||||
// Clear timeout on successful response
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
response.on('error', (err) => {
|
||||
console.error(err);
|
||||
response.destroy();
|
||||
|
Reference in New Issue
Block a user