import { URL } from 'url'; import { Stream, PassThrough, pipeline as pump } from 'stream'; import ElectronAdapter from './ElectronAdapter'; import { REQUEST_EVENT } from '@/enum'; import { isRedirect, inElectron } from '@/utils'; import ResponseImpl from '@/Response'; import Headers from '@/Headers'; import { HEADER_MAP, METHOD_MAP, RESPONSE_EVENT } from '@/enum'; import type { Writable } from 'stream'; import type { RequestOptions, RequestClient, Response } from '@/typings.d'; class ElectronRequestClient implements RequestClient { private readonly electronAdapter = inElectron ? new ElectronAdapter() : null; private options: RequestOptions; private redirectCount: number = 0; private timeoutId: NodeJS.Timeout | null = null; constructor(options: RequestOptions) { this.options = options; } private clearRequestTimeout = () => { if (this.timeoutId === null) return; clearTimeout(this.timeoutId); this.timeoutId = null; }; private createRequest = async () => { if (this.electronAdapter === null) { throw new Error('Error in environmental judgment'); } await this.electronAdapter.whenReady(); const { requestURL, parsedURL: { protocol, host, hostname, port, pathname, origin, search }, method, session, useSessionCookies, headers, } = this.options; const options = { method, url: `${requestURL}${pathname}${search || ''}`, // path: `${pathname}${search || ''}`, session: session || this.electronAdapter.getDefaultSession(), useSessionCookies, protocol, host, hostname, origin, port: Number(port), }; // console.log('options: ', options); const clientRequest = this.electronAdapter.request(options); for (const [key, value] of Object.entries(headers.raw())) { if (Array.isArray(value)) { for (const v of value) { clientRequest.setHeader(key, v); } } else { clientRequest.setHeader(key, value); } } return clientRequest; }; public send = async () => { const { method, followRedirect, maxRedirectCount, requestURL, parsedURL, size, username, password, timeout, body: requestBody, } = this.options; /** Create electron request */ const clientRequest = await this.createRequest(); /** Cancel electron request */ const cancelRequest = () => { // In electron, `request.destroy()` does not send abort to server clientRequest.abort(); }; /** Write body to electron request */ const writeToRequest = () => { if (requestBody === null) { clientRequest.end(); } else if (requestBody instanceof Stream) { // TODO remove as requestBody.pipe(new PassThrough()).pipe(clientRequest as unknown as Writable); } else { clientRequest.write(requestBody); clientRequest.end(); } }; /** Bind electron request event */ const bindRequestEvent = ( onFulfilled: (value: Response | PromiseLike<Response>) => void, onRejected: (reason: Error) => void, ) => { /** Set electron request timeout */ if (timeout) { this.timeoutId = setTimeout(() => { onRejected(new Error(`Electron request timeout in ${timeout} ms`)); }, timeout); } /** Bind electron request error event */ clientRequest.on(REQUEST_EVENT.ERROR, onRejected); /** Bind electron request abort event */ clientRequest.on(REQUEST_EVENT.ABORT, () => { onRejected(new Error('Electron request was aborted by the server')); }); /** Bind electron request login event */ clientRequest.on(REQUEST_EVENT.LOGIN, (authInfo, callback) => { if (username && password) { callback(username, password); } else { onRejected( new Error(`Login event received from ${authInfo.host} but no credentials provided`), ); } }); /** Bind electron request response event */ clientRequest.on(REQUEST_EVENT.RESPONSE, (res) => { this.clearRequestTimeout(); const { statusCode = 200, headers: responseHeaders } = res; const headers = new Headers(responseHeaders); if (isRedirect(statusCode) && followRedirect) { if (maxRedirectCount && this.redirectCount >= maxRedirectCount) { onRejected(new Error(`Maximum redirect reached at: ${requestURL}`)); } if (!headers.get(HEADER_MAP.LOCATION)) { onRejected(new Error(`Redirect location header missing at: ${requestURL}`)); } if ( statusCode === 303 || ((statusCode === 301 || statusCode === 302) && method === METHOD_MAP.POST) ) { this.options.method = METHOD_MAP.GET; this.options.body = null; this.options.headers.delete(HEADER_MAP.CONTENT_LENGTH); } this.redirectCount += 1; this.options.parsedURL = new URL( String(headers.get(HEADER_MAP.LOCATION)), parsedURL.toString(), ); onFulfilled(this.send()); } const responseBody = pump(res, new PassThrough(), (error) => { if (error !== null) { onRejected(error); } }); responseBody.on(RESPONSE_EVENT.CANCEL_REQUEST, cancelRequest); onFulfilled( new ResponseImpl(responseBody, { requestURL, statusCode, headers, size, }), ); }); }; return new Promise<Response>((resolve, reject) => { const onRejected = (reason: Error) => { this.clearRequestTimeout(); cancelRequest(); reject(reason); }; bindRequestEvent(resolve, onRejected); writeToRequest(); }); }; } export default ElectronRequestClient;