import { Fetcher, FetcherRequest } from './Fetcher'; import { ConvertUtils } from '../convert/ConvertUtils'; export type RequestInitType = RequestInit & { timeout?: number }; export type RequestInfo = { requestInfo: string | URL; init: RequestInitType; }; export type HttpFetcherTarget = | URL | string | { url: URL | string; searchParams?: { [key: string]: any } | URLSearchParams | [string, unknown][] }; export type HttpFetcherConfig = { fetch?: RequestInitType; fetcher?: typeof fetch; config?: CONFIG; allowedResponseNotOk?: boolean; beforeProxyFetch?: ( config: BeforeProxyFetchParams ) => Promise>; afterProxyFetch?: (config: AfterProxyFetchParams) => Promise; skipGlobalBeforeProxyFetch?: boolean; skipGlobalAfterProxyFetch?: boolean; fetchResponseBeforeCallBack?: (config: HttpFetcherConfig) => void; fetchResponseAfterCallBack?: (data: Response, config: HttpFetcherConfig) => void; hasResponseErrorChecker?: (data: Response, config: HttpFetcherConfig) => any; }; export type BeforeProxyFetchParams = { requestInfo: T; init?: RequestInit; }; export type AfterProxyFetchParams = { fetch: { target: URL; requestInit: RequestInit; } config: BeforeProxyFetchParams; response: Response; }; export type FetchSet = { target: URL, requestInit?: RequestInit }; export class HttpResponseError extends Error { public error?: any; public body?: T; public response?: Response; } export const isHttpResponseError = (data: any): data is HttpResponseError => { return data instanceof HttpResponseError; } export type HttpFetcherRequest = FetcherRequest< HttpFetcherTarget, RESPONSE, HttpFetcherConfig, T > export class HttpFetcher< CONFIG = any, RESPONSE = Response, PIPE extends { responseData?: RESPONSE | undefined } = any > extends Fetcher, PIPE> { get(config: HttpFetcherRequest): Promise { config.config ??= {}; config.config.fetch = config.config?.fetch ?? {}; config.config.fetch.method = 'GET'; return this.fetch(config); } post( config: HttpFetcherRequest ): Promise { config.config ??= {}; config.config.fetch = config.config?.fetch ?? {}; config.config.fetch.method = 'POST'; return this.fetch(config); } patch( config: HttpFetcherRequest ): Promise { config.config ??= {}; config.config.fetch = config.config?.fetch ?? {}; config.config.fetch.method = 'PATCH'; return this.fetch(config); } put( config: HttpFetcherRequest ): Promise { config.config ??= {}; config.config.fetch = config.config?.fetch ?? {}; config.config.fetch.method = 'PUT'; return this.fetch(config); } head( config: HttpFetcherRequest ): Promise { config.config ??= {}; config.config.fetch = config.config?.fetch ?? {}; config.config.fetch.method = 'HEAD'; return this.fetch(config); } delete( config: HttpFetcherRequest ): Promise { config.config ??= {}; config.config.fetch = config.config?.fetch ?? {}; config.config.fetch.method = 'DELETE'; return this.fetch(config); } protected async beforeProxyFetch( config: BeforeProxyFetchParams ): Promise> { return config; } protected async afterProxyFetch(config: AfterProxyFetchParams): Promise { return config.response; } protected afterSuccess( config: HttpFetcherRequest, pipe: PIPE ): void {} protected afterSuccessTransform( config: HttpFetcherRequest, pipe: PIPE ): void {} protected before( config: HttpFetcherRequest, pipe: PIPE ): void {} protected error( config: HttpFetcherRequest, pipe: PIPE, e: any ): void {} protected finally( config: HttpFetcherRequest, pipe: PIPE ): void {} protected beforeFetch(fetch: FetchSet): void {} protected afterFetch(fetch: FetchSet, response: Response): void {} // protected async errorTransform(e: any): Promise> { protected async errorTransform(e: any): Promise { const httpResponseError = new HttpResponseError(); httpResponseError.error = e; if (e instanceof Response) { httpResponseError.response = e; try { httpResponseError.body = await e.clone().text(); httpResponseError.message = e.statusText; } catch (e: any) { httpResponseError.body = e; httpResponseError.message = e.message; } } else { httpResponseError.body = e; httpResponseError.message = e.message; } return httpResponseError; } protected async execute( fetcherRequest: HttpFetcherRequest ): Promise { let target = fetcherRequest.target; let config = fetcherRequest.config; // target data setting if (!(target instanceof URL) && typeof target !== 'string') { // const url: URL | string = target.url; // if (typeof target.url === 'string') { // } const searchParams = ConvertUtils.toURLSearchParams(target.searchParams ?? {}); try { const url = typeof target.url === 'string' ? new URL(target.url) : target.url; searchParams.forEach((value, key) => { url.searchParams.append(key, value); }); target = url; } catch (e) { if (typeof (target as any).url === 'string') { const searchParamString = searchParams.toString(); target = (target as any).url + (searchParamString ? '?' + searchParamString : ''); } else { target = ''; } } } // before proxy fetch const beforeProxyData = { requestInfo: target, init: config?.fetch } as BeforeProxyFetchParams; let beforeData = config?.beforeProxyFetch ? await config?.beforeProxyFetch(config as any) : beforeProxyData.requestInfo; beforeData = config?.skipGlobalBeforeProxyFetch ? beforeProxyData : await this.beforeProxyFetch(beforeProxyData); target = beforeData.requestInfo as URL; if (beforeData.init) { config ??= {}; config.fetch = beforeData.init; } let abortedTimeout: NodeJS.Timeout | undefined; if (config?.fetch && 'timeout' in config.fetch) { const abortController = new AbortController(); abortedTimeout = setTimeout(() => { if (config?.fetch?.signal) { const inputSignal = config.fetch.signal; const listener = () => { if (!abortController.signal.aborted) { abortController.abort(); } inputSignal.removeEventListener('abort', listener); }; inputSignal.addEventListener('abort', listener); } if (!abortController.signal.aborted) { abortController.abort(); } }, config.fetch.timeout); config.fetch.signal = abortController.signal; } this.beforeFetch({ target, requestInit: config?.fetch }); return (config.fetcher??fetch)(target, config?.fetch) .then(async it => { // console.log('httpFetch!!!', Array.from(it.headers)) this.afterFetch({ target: target as URL, requestInit: config?.fetch }, it); // after proxy fetch // @ts-ignore const afterProxyData: AfterProxyFetchParams = { fetch: { target: target as URL, requestInit: config.fetch }, config: beforeData, response: it }; it = config?.afterProxyFetch ? await config.afterProxyFetch(afterProxyData) : it; // @ts-ignore it = config?.skipGlobalAfterProxyFetch ? it : await this.afterProxyFetch(afterProxyData); config?.fetchResponseAfterCallBack?.(it, config); if (!config?.allowedResponseNotOk && !it.ok) { throw it; } const data = config?.hasResponseErrorChecker?.(it, config); if (data) { throw data; } return it; }) .finally(() => { if (abortedTimeout) { clearTimeout(abortedTimeout); } }); } }