207 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from './plugins.js';
 | |
| import * as types from './types.js';
 | |
| import { CoreResponse } from './response.js';
 | |
| import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
 | |
| 
 | |
| // Keep-alive agents for connection pooling
 | |
| const httpAgent = new plugins.agentkeepalive.HttpAgent({
 | |
|   keepAlive: true,
 | |
|   maxFreeSockets: 10,
 | |
|   maxSockets: 100,
 | |
|   maxTotalSockets: 1000,
 | |
| });
 | |
| 
 | |
| const httpAgentKeepAliveFalse = new plugins.agentkeepalive.HttpAgent({
 | |
|   keepAlive: false,
 | |
| });
 | |
| 
 | |
| const httpsAgent = new plugins.agentkeepalive.HttpsAgent({
 | |
|   keepAlive: true,
 | |
|   maxFreeSockets: 10,
 | |
|   maxSockets: 100,
 | |
|   maxTotalSockets: 1000,
 | |
| });
 | |
| 
 | |
| const httpsAgentKeepAliveFalse = new plugins.agentkeepalive.HttpsAgent({
 | |
|   keepAlive: false,
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Node.js implementation of Core Request class that handles all HTTP/HTTPS requests
 | |
|  */
 | |
| export class CoreRequest extends AbstractCoreRequest<
 | |
|   types.ICoreRequestOptions,
 | |
|   CoreResponse
 | |
| > {
 | |
|   private requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null;
 | |
| 
 | |
|   constructor(
 | |
|     url: string,
 | |
|     options: types.ICoreRequestOptions = {},
 | |
|     requestDataFunc: ((req: plugins.http.ClientRequest) => void) | null = null,
 | |
|   ) {
 | |
|     super(url, options);
 | |
|     this.requestDataFunc = requestDataFunc;
 | |
| 
 | |
|     // Check for unsupported fetch-specific options
 | |
|     if (
 | |
|       options.credentials ||
 | |
|       options.mode ||
 | |
|       options.cache ||
 | |
|       options.redirect ||
 | |
|       options.referrer ||
 | |
|       options.referrerPolicy ||
 | |
|       options.integrity
 | |
|     ) {
 | |
|       throw new Error(
 | |
|         'Fetch API specific options (credentials, mode, cache, redirect, referrer, referrerPolicy, integrity) are not supported in Node.js implementation',
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Fire the request and return a CoreResponse
 | |
|    */
 | |
|   async fire(): Promise<CoreResponse> {
 | |
|     const incomingMessage = await this.fireCore();
 | |
|     return new CoreResponse(incomingMessage, this.url, this.options);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Fire the request and return the raw IncomingMessage
 | |
|    */
 | |
|   async fireCore(): Promise<plugins.http.IncomingMessage> {
 | |
|     const done = plugins.smartpromise.defer<plugins.http.IncomingMessage>();
 | |
| 
 | |
|     // Parse URL
 | |
|     const parsedUrl = plugins.smarturl.Smarturl.createFromUrl(this.url, {
 | |
|       searchParams: this.options.queryParams || {},
 | |
|     });
 | |
| 
 | |
|     this.options.hostname = parsedUrl.hostname;
 | |
|     if (parsedUrl.port) {
 | |
|       this.options.port = parseInt(parsedUrl.port, 10);
 | |
|     }
 | |
|     this.options.path = parsedUrl.path;
 | |
| 
 | |
|     // Handle unix socket URLs
 | |
|     if (CoreRequest.isUnixSocket(this.url)) {
 | |
|       const { socketPath, path } = CoreRequest.parseUnixSocketUrl(
 | |
|         this.options.path,
 | |
|       );
 | |
|       this.options.socketPath = socketPath;
 | |
|       this.options.path = path;
 | |
|     }
 | |
| 
 | |
|     // Determine agent based on protocol and keep-alive setting
 | |
|     if (!this.options.agent) {
 | |
|       // Only use keep-alive agents if explicitly requested
 | |
|       if (this.options.keepAlive === true) {
 | |
|         this.options.agent =
 | |
|           parsedUrl.protocol === 'https:' ? httpsAgent : httpAgent;
 | |
|       } else if (this.options.keepAlive === false) {
 | |
|         this.options.agent =
 | |
|           parsedUrl.protocol === 'https:'
 | |
|             ? httpsAgentKeepAliveFalse
 | |
|             : httpAgentKeepAliveFalse;
 | |
|       }
 | |
|       // If keepAlive is undefined, don't set any agent (more fetch-like behavior)
 | |
|     }
 | |
| 
 | |
|     // Determine request module
 | |
|     const requestModule =
 | |
|       parsedUrl.protocol === 'https:' ? plugins.https : plugins.http;
 | |
| 
 | |
|     if (!requestModule) {
 | |
|       throw new Error(
 | |
|         `The request to ${this.url} is missing a viable protocol. Must be http or https`,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // Perform the request
 | |
|     let timeoutId: NodeJS.Timeout | null = null;
 | |
|     const request = requestModule.request(this.options, async (response) => {
 | |
|       // Handle hard timeout
 | |
|       if (this.options.hardDataCuttingTimeout) {
 | |
|         timeoutId = setTimeout(() => {
 | |
|           response.destroy();
 | |
|           done.reject(new Error('Request timed out'));
 | |
|         }, this.options.hardDataCuttingTimeout);
 | |
|       }
 | |
| 
 | |
|       // Always return the raw stream
 | |
|       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) {
 | |
|         this.options.requestBody.pipe(request).on('finish', () => {
 | |
|           request.end();
 | |
|         });
 | |
|       } else {
 | |
|         // Write body as-is - caller is responsible for serialization
 | |
|         const bodyData =
 | |
|           typeof this.options.requestBody === 'string'
 | |
|             ? this.options.requestBody
 | |
|             : this.options.requestBody instanceof Buffer
 | |
|               ? this.options.requestBody
 | |
|               : JSON.stringify(this.options.requestBody); // Still stringify for backward compatibility
 | |
|         request.write(bodyData);
 | |
|         request.end();
 | |
|       }
 | |
|     } else if (this.requestDataFunc) {
 | |
|       this.requestDataFunc(request);
 | |
|     } else {
 | |
|       request.end();
 | |
|     }
 | |
| 
 | |
|     // Handle request errors
 | |
|     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();
 | |
|     });
 | |
| 
 | |
|     return response;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Static factory method to create and fire a request
 | |
|    */
 | |
|   static async create(
 | |
|     url: string,
 | |
|     options: types.ICoreRequestOptions = {},
 | |
|   ): Promise<CoreResponse> {
 | |
|     const request = new CoreRequest(url, options);
 | |
|     return request.fire();
 | |
|   }
 | |
| }
 |