Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
ffabcf7bdb | |||
361d97f440 | |||
35867d9148 | |||
d455a34632 | |||
9c5a939499 |
22
changelog.md
22
changelog.md
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-19 - 4.3.1 - fix(core)
|
||||
Improve streaming support and timeout handling; add browser streaming & timeout tests and README clarifications
|
||||
|
||||
- core_fetch: accept Uint8Array and Buffer-like bodies; set fetch duplex for ReadableStream bodies so streaming requests work in environments that require duplex
|
||||
- core_fetch: implement AbortController-based timeouts and ensure timeouts are cleared on success/error to avoid hanging timers
|
||||
- core_node: add explicit request timeout handling (request.setTimeout) and hard-data-cutting timeout tracking with proper timeoutId clear on success/error
|
||||
- client: document that raw(streamFunc) is Node-only (not supported in browsers)
|
||||
- tests: add browser streaming tests (test/test.streaming.browser.ts) that exercise buffer() and web ReadableStream via stream()
|
||||
- tests: add timeout tests (test/test.timeout.ts) to validate clearing timers, enforcing timeouts, and preventing timer leaks across multiple requests
|
||||
- docs: update README streaming section to clarify cross-platform behavior of buffer(), stream(), and raw() methods
|
||||
|
||||
## 2025-08-18 - 4.3.0 - feat(client/smartrequest)
|
||||
Add streaming and raw buffer support to SmartRequest (buffer, stream, raw); update docs and tests
|
||||
|
||||
- Add SmartRequest.buffer(data, contentType?) to send Buffer or Uint8Array bodies with Content-Type header.
|
||||
- Add SmartRequest.stream(stream, contentType?) to accept Node.js Readable streams or web ReadableStream and set Content-Type when provided.
|
||||
- Add SmartRequest.raw(streamFunc) to allow custom raw streaming functions (Node.js only) and a RawStreamFunction type.
|
||||
- Wire Node.js stream handling into CoreRequest by passing a requestDataFunc when creating CoreRequest instances.
|
||||
- Add comprehensive streaming examples and documentation to README describing buffer/stream/raw usage and streaming methods.
|
||||
- Add tests for streaming behavior (test/test.streaming.ts) covering buffer, stream, raw, and Uint8Array usage.
|
||||
- Update client exports and plugins to support streaming features and FormData usage where needed.
|
||||
|
||||
## 2025-08-18 - 4.2.2 - fix(client)
|
||||
Fix CI configuration, prevent socket hangs with auto-drain, and apply various client/core TypeScript fixes and test updates
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartrequest",
|
||||
"version": "4.2.2",
|
||||
"version": "4.3.1",
|
||||
"private": false,
|
||||
"description": "A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.",
|
||||
"exports": {
|
||||
|
99
readme.md
99
readme.md
@@ -25,7 +25,7 @@ yarn add @push.rocks/smartrequest
|
||||
- ⚡ **Keep-Alive Connections** - Efficient connection pooling in Node.js
|
||||
- 🛡️ **TypeScript First** - Full type safety and IntelliSense support
|
||||
- 🎯 **Zero Magic Defaults** - Explicit configuration following fetch API principles
|
||||
- 📡 **Streaming Support** - Handle large files and real-time data
|
||||
- 📡 **Streaming Support** - Stream buffers, files, and custom data without loading into memory
|
||||
- 🔧 **Highly Configurable** - Timeouts, retries, headers, and more
|
||||
|
||||
## Architecture
|
||||
@@ -303,6 +303,103 @@ async function uploadMultipleFiles(
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Request Bodies
|
||||
|
||||
SmartRequest provides multiple ways to stream data in requests, making it easy to upload large files or send real-time data without loading everything into memory:
|
||||
|
||||
```typescript
|
||||
import { SmartRequest } from '@push.rocks/smartrequest';
|
||||
import * as fs from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
// Stream a Buffer directly
|
||||
async function uploadBuffer() {
|
||||
const buffer = Buffer.from('Hello, World!');
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/upload')
|
||||
.buffer(buffer, 'text/plain')
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Stream a file using Node.js streams
|
||||
async function uploadLargeFile(filePath: string) {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/upload')
|
||||
.stream(fileStream, 'application/octet-stream')
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Stream data from any readable source
|
||||
async function streamData(dataSource: Readable) {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/stream')
|
||||
.stream(dataSource)
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Advanced: Full control over request streaming (Node.js only)
|
||||
async function customStreaming() {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/stream')
|
||||
.raw((request) => {
|
||||
// Custom streaming logic - you have full control
|
||||
request.write('chunk1');
|
||||
request.write('chunk2');
|
||||
|
||||
// Stream from another source
|
||||
someReadableStream.pipe(request);
|
||||
})
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Send Uint8Array (works in both Node.js and browser)
|
||||
async function uploadBinaryData() {
|
||||
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
|
||||
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://api.example.com/binary')
|
||||
.buffer(data, 'application/octet-stream')
|
||||
.post();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
#### Streaming Methods
|
||||
|
||||
- **`.buffer(data, contentType?)`** - Stream a Buffer or Uint8Array directly
|
||||
- `data`: Buffer (Node.js) or Uint8Array (both platforms) to send
|
||||
- `contentType`: Optional content type (defaults to 'application/octet-stream')
|
||||
- ✅ Works in both Node.js and browsers
|
||||
|
||||
- **`.stream(stream, contentType?)`** - Stream from ReadableStream
|
||||
- `stream`: Web ReadableStream (both platforms) or Node.js stream (Node.js only)
|
||||
- `contentType`: Optional content type
|
||||
- ✅ Web ReadableStream works in both Node.js and browsers
|
||||
- ⚠️ Node.js streams only work in Node.js environment
|
||||
|
||||
- **`.raw(streamFunc)`** - Advanced control over request streaming
|
||||
- `streamFunc`: Function that receives the raw request object for custom streaming
|
||||
- ❌ **Node.js only** - not supported in browsers
|
||||
- Use for advanced scenarios like chunked transfer encoding
|
||||
|
||||
These methods are particularly useful for:
|
||||
- Uploading large files without loading them into memory
|
||||
- Streaming real-time data to servers
|
||||
- Proxying data between services
|
||||
- Implementing chunked transfer encoding
|
||||
|
||||
### Unix Socket Support (Node.js only)
|
||||
|
||||
```typescript
|
||||
|
41
test/test.streaming.browser.ts
Normal file
41
test/test.streaming.browser.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartRequest } from '../ts/index.js';
|
||||
|
||||
tap.test('browser: should send Uint8Array using buffer() method', async () => {
|
||||
const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII
|
||||
|
||||
const smartRequest = SmartRequest.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.buffer(testData, 'application/octet-stream')
|
||||
.method('POST');
|
||||
|
||||
const response = await smartRequest.post();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toHaveProperty('data');
|
||||
expect(data.headers['Content-Type']).toEqual('application/octet-stream');
|
||||
});
|
||||
|
||||
tap.test('browser: should send web ReadableStream using stream() method', async () => {
|
||||
// Create a web ReadableStream
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode('Test stream data'));
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const smartRequest = SmartRequest.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.stream(stream, 'text/plain')
|
||||
.method('POST');
|
||||
|
||||
const response = await smartRequest.post();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toHaveProperty('data');
|
||||
// httpbin should receive the streamed data
|
||||
});
|
||||
|
||||
export default tap.start();
|
74
test/test.streaming.ts
Normal file
74
test/test.streaming.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as fs from 'fs';
|
||||
import { SmartRequest } from '../ts/index.js';
|
||||
|
||||
tap.test('should send a buffer using buffer() method', async () => {
|
||||
const testBuffer = Buffer.from('Hello, World!');
|
||||
|
||||
const smartRequest = SmartRequest.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.buffer(testBuffer, 'text/plain')
|
||||
.method('POST');
|
||||
|
||||
const response = await smartRequest.post();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toHaveProperty('data');
|
||||
expect(data.data).toEqual('Hello, World!');
|
||||
expect(data.headers['Content-Type']).toEqual('text/plain');
|
||||
});
|
||||
|
||||
tap.test('should send a stream using stream() method', async () => {
|
||||
// Create a simple readable stream
|
||||
const { Readable } = await import('stream');
|
||||
const testData = 'Stream data test';
|
||||
const stream = Readable.from([testData]);
|
||||
|
||||
const smartRequest = SmartRequest.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.stream(stream, 'text/plain')
|
||||
.method('POST');
|
||||
|
||||
const response = await smartRequest.post();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toHaveProperty('data');
|
||||
expect(data.data).toEqual(testData);
|
||||
});
|
||||
|
||||
tap.test('should handle raw streaming with custom function', async () => {
|
||||
const testData = 'Custom raw stream data';
|
||||
|
||||
const smartRequest = SmartRequest.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.raw((request) => {
|
||||
// Custom streaming logic
|
||||
request.write(testData);
|
||||
request.end();
|
||||
})
|
||||
.method('POST');
|
||||
|
||||
const response = await smartRequest.post();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data).toHaveProperty('data');
|
||||
expect(data.data).toEqual(testData);
|
||||
});
|
||||
|
||||
tap.test('should send Uint8Array using buffer() method', async () => {
|
||||
const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII
|
||||
|
||||
const smartRequest = SmartRequest.create()
|
||||
.url('https://httpbin.org/post')
|
||||
.buffer(testData, 'application/octet-stream')
|
||||
.method('POST');
|
||||
|
||||
const response = await smartRequest.post();
|
||||
const data = await response.json();
|
||||
|
||||
// Just verify that data was sent
|
||||
expect(data).toHaveProperty('data');
|
||||
expect(data.headers['Content-Type']).toEqual('application/octet-stream');
|
||||
});
|
||||
|
||||
export default tap.start();
|
27
test/test.streamnode.ts
Normal file
27
test/test.streamnode.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartRequest } from '../ts/index.js';
|
||||
|
||||
tap.test('should have streamNode() method available', async () => {
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://httpbin.org/get')
|
||||
.get();
|
||||
|
||||
// Verify streamNode() method exists
|
||||
expect(response.streamNode).toBeDefined();
|
||||
expect(typeof response.streamNode).toEqual('function');
|
||||
|
||||
// In Node.js, it should return a stream
|
||||
const nodeStream = response.streamNode();
|
||||
expect(nodeStream).toBeDefined();
|
||||
|
||||
// Verify it's a Node.js readable stream
|
||||
expect(typeof nodeStream.pipe).toEqual('function');
|
||||
expect(typeof nodeStream.on).toEqual('function');
|
||||
|
||||
// Consume the stream to avoid hanging
|
||||
nodeStream.resume();
|
||||
});
|
||||
|
||||
|
||||
|
||||
export default tap.start();
|
60
test/test.timeout.ts
Normal file
60
test/test.timeout.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartRequest } from '../ts/index.js';
|
||||
|
||||
tap.test('should clear timeout when request completes before timeout', async () => {
|
||||
// Set a long timeout that would keep the process alive if not cleared
|
||||
const response = await SmartRequest.create()
|
||||
.url('https://httpbin.org/delay/1') // 1 second delay
|
||||
.timeout(10000) // 10 second timeout (much longer than needed)
|
||||
.get();
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toBeDefined();
|
||||
|
||||
// The test should complete quickly, not wait for the 10 second timeout
|
||||
// If the timeout isn't cleared, the process would hang for 10 seconds
|
||||
});
|
||||
|
||||
tap.test('should timeout when request takes longer than timeout', async () => {
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
// Try to fetch with a very short timeout
|
||||
await SmartRequest.create()
|
||||
.url('https://httpbin.org/delay/3') // 3 second delay
|
||||
.timeout(100) // 100ms timeout (will fail)
|
||||
.get();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
expect(error.message).toContain('Request timed out');
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should not leak timers with multiple successful requests', async () => {
|
||||
// Make multiple requests with timeouts to ensure no timer leaks
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
promises.push(
|
||||
SmartRequest.create()
|
||||
.url('https://httpbin.org/get')
|
||||
.timeout(5000) // 5 second timeout
|
||||
.get()
|
||||
.then(response => response.json())
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// All requests should complete successfully
|
||||
expect(results).toHaveLength(5);
|
||||
results.forEach(result => {
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
// Process should exit cleanly after this test without hanging
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartrequest',
|
||||
version: '4.2.2',
|
||||
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.'
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import type {
|
||||
ResponseType,
|
||||
FormField,
|
||||
RateLimitConfig,
|
||||
RawStreamFunction,
|
||||
} from './types/common.js';
|
||||
import {
|
||||
type TPaginationConfig,
|
||||
@@ -121,6 +122,56 @@ export class SmartRequest<T = any> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set raw buffer data for the request
|
||||
*/
|
||||
buffer(data: Buffer | Uint8Array, contentType?: string): this {
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
this._options.headers['Content-Type'] = contentType || 'application/octet-stream';
|
||||
this._options.requestBody = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream data for the request
|
||||
* Accepts Node.js Readable streams or web ReadableStream
|
||||
*/
|
||||
stream(stream: NodeJS.ReadableStream | ReadableStream<Uint8Array>, contentType?: string): this {
|
||||
if (!this._options.headers) {
|
||||
this._options.headers = {};
|
||||
}
|
||||
|
||||
// Set content type if provided
|
||||
if (contentType) {
|
||||
this._options.headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
// Check if it's a Node.js stream (has pipe method)
|
||||
if ('pipe' in stream && typeof (stream as any).pipe === 'function') {
|
||||
// For Node.js streams, we need to use a custom approach
|
||||
// Store the stream to be used later
|
||||
(this._options as any).__nodeStream = stream;
|
||||
} else {
|
||||
// For web ReadableStream, pass directly
|
||||
this._options.requestBody = stream;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, not supported in browsers
|
||||
*/
|
||||
raw(streamFunc: RawStreamFunction): this {
|
||||
// Store the raw streaming function to be used later
|
||||
(this._options as any).__rawStreamFunc = streamFunc;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request timeout in milliseconds
|
||||
*/
|
||||
@@ -389,7 +440,22 @@ export class SmartRequest<T = any> {
|
||||
// Main retry loop
|
||||
for (let attempt = 0; attempt <= this._retries; attempt++) {
|
||||
try {
|
||||
const request = new CoreRequest(this._url, this._options as any);
|
||||
// Check if we have a Node.js stream or raw function that needs special handling
|
||||
let requestDataFunc = null;
|
||||
if ((this._options as any).__nodeStream) {
|
||||
const nodeStream = (this._options as any).__nodeStream;
|
||||
requestDataFunc = (req: any) => {
|
||||
nodeStream.pipe(req);
|
||||
};
|
||||
// Remove the temporary stream reference
|
||||
delete (this._options as any).__nodeStream;
|
||||
} else if ((this._options as any).__rawStreamFunc) {
|
||||
requestDataFunc = (this._options as any).__rawStreamFunc;
|
||||
// Remove the temporary function reference
|
||||
delete (this._options as any).__rawStreamFunc;
|
||||
}
|
||||
|
||||
const request = new CoreRequest(this._url, this._options as any, requestDataFunc);
|
||||
const response = (await request.fire()) as ICoreResponse<R>;
|
||||
|
||||
// Check for 429 status if rate limit handling is enabled
|
||||
|
@@ -66,3 +66,9 @@ export interface RateLimitConfig {
|
||||
backoffFactor?: number; // Exponential backoff factor (default: 2)
|
||||
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw streaming function for advanced request body control
|
||||
* Note: The request parameter type depends on the environment (Node.js ClientRequest or fetch Request)
|
||||
*/
|
||||
export type RawStreamFunction = (request: any) => void;
|
||||
|
@@ -42,4 +42,9 @@ export abstract class CoreResponse<T = any> implements types.ICoreResponse<T> {
|
||||
* Get response as a web-style ReadableStream
|
||||
*/
|
||||
abstract stream(): ReadableStream<Uint8Array> | null;
|
||||
|
||||
/**
|
||||
* Get response as a Node.js stream (throws in browser)
|
||||
*/
|
||||
abstract streamNode(): NodeJS.ReadableStream | never;
|
||||
}
|
||||
|
@@ -86,4 +86,5 @@ export interface ICoreResponse<T = any> {
|
||||
text(): Promise<string>;
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
stream(): ReadableStream<Uint8Array> | null; // Always returns web-style stream
|
||||
streamNode(): NodeJS.ReadableStream | never; // Returns Node.js stream or throws in browser
|
||||
}
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -7,9 +7,6 @@ export * from '../core_base/types.js';
|
||||
* Fetch-specific response extensions
|
||||
*/
|
||||
export interface IFetchResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Node.js stream method that throws in browser
|
||||
streamNode(): never;
|
||||
|
||||
// Access to raw Response object
|
||||
raw(): Response;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -16,9 +16,6 @@ export interface IExtendedIncomingMessage<T = any>
|
||||
* Node.js specific response extensions
|
||||
*/
|
||||
export interface INodeResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Node.js specific methods
|
||||
streamNode(): NodeJS.ReadableStream; // Returns Node.js style stream
|
||||
|
||||
// Legacy compatibility
|
||||
raw(): plugins.http.IncomingMessage;
|
||||
}
|
||||
|
Reference in New Issue
Block a user