Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | import { retry } from 'moderndash' import { getLogger } from '../../logging/log-util' const logger = getLogger('message.service') /** * A custom fetch(..) function that can be used for making HTTP requests. * Primarily used for intercepting responses or configuring an HTTP proxy. */ export type FetchFn = typeof fetch /** * Options for configuring a custom fetch function. */ export interface FetchOptions { /** * The proxy URL to be used for HTTP requests. * If provided, the fetch function will use this proxy. */ proxyUrl?: string /** * Timeout value (in milliseconds) for the fetch requests. * If not provided, a default timeout may be used. */ timeout?: number } /** * Options for configuring a retry mechanism for fetch function. */ export interface RetryOptions { /** * Maximum number of retry attempts (excluding the first try). * Set to 0 or undefined to disable retries. */ retries?: number /** * Base delay in milliseconds before the first retry attempt. * Subsequent retries will use exponential backoff. */ backoffMs?: number /** * A mapping of HTTP status codes to body matchers (string or RegExp) that should trigger a retry. * If the array is empty, any response with the corresponding status code will trigger a retry. */ retryConditions?: Record<number, (string | RegExp)[]> } /** * Options for performing a fetch request with conditional retry logic * based on HTTP status codes and optional response body matchers. */ interface FetchRetryOptions { fetchFn: typeof fetch input: RequestInfo | URL init: RequestInit retryConditions: Record<number, (string | RegExp)[]> } /** * Extended options for instrumented fetch calls. */ export type InstrumentedFetchOptions = RequestInit & FetchOptions & { retryOptions?: RetryOptions } /** * Service interface for managing HTTP requests with optional instrumentation and proxy support. */ export interface HttpClient { /** * Makes an HTTP request with instrumentation support for metrics. * * @param input - The input for the HTTP request, which can be a URL or a `RequestInfo` object. * @param options - (Optional) Additional options for the request, including proxy settings, initialization options and retry configuration. * @returns A promise that resolves with the `Response` object from the HTTP request. * @throws Will throw an error if the HTTP request fails. */ httpFetch( input: RequestInfo | URL, options?: InstrumentedFetchOptions, ): Promise<Response> } export function getHttpClient(): HttpClient { return new DefaultHttpClient() } export class DefaultHttpClient implements HttpClient { constructor() {} async httpFetch( input: RequestInfo | URL, options: InstrumentedFetchOptions = {}, ): Promise<Response> { logger.debug( 'Executing http fetch function; input: [%s], options: [%j]', input, options, ) const { retryOptions, ...init } = options // Configure default retry options if not specified const { retries = 0, backoffMs = 100, retryConditions = {}, } = retryOptions ?? {} const fetchFn = fetch try { const response = await retry( async () => await this.fetchWithRetryConditions({ fetchFn, input, init, retryConditions, }), { maxRetries: retries, backoff: (retries: number) => retries * backoffMs, onRetry: (error, attempt) => { logger.warn( 'HTTP request failed; attempt [%d] of [%d]; [%s]', attempt, retries, error, ) }, }, ) logger.trace( 'HTTP request completed; status: [%d]', response.status, ) return response } catch (error) { throw new Error('instrumented fetch failed') } } /** * Fetches a resource and checks for retry conditions based on the response status and body. * * @param fetchFn - The fetch function to use for making the request. * @param input - The input for the HTTP request, which can be a URL or a `RequestInfo` object. * @param init - The initialization options for the fetch request. * @param retryConditions - A map of status codes to response body contents that should trigger a retry. * @returns A promise that resolves with the `Response` object from the HTTP request. */ private async fetchWithRetryConditions({ fetchFn, input, init, retryConditions, }: FetchRetryOptions): Promise<Response> { const response = await fetchFn(input, init) logger.trace('log fetch ' + response) console.log(retryConditions) //Check if the response status is configured to be retried let conditions Iif (Object.keys(retryConditions).map(k => parseInt(k)).includes(response.status)) { conditions = retryConditions[response.status] } Iif (!conditions) { return response } // Clone the response before reading its body to avoid consuming the original stream const body = await response.clone().text() //Retry on this status regardless of body content Iif (conditions.length === 0) { throw new Error(`Retryable response thrown with http status: [${response.status} ${response.statusText}]; response body: [${body}]`); } //Retry only if the body matches one of the configured retry conditions (string or regex) const matchedCondition = conditions.find((condition) => typeof condition === 'string' ? condition === body : condition.test(body), ) Iif (matchedCondition) { throw new Error(`Retryable response thrown with http status: [${response.status} ${response.statusText}]; matched condition: [${matchedCondition}]; response body: [${body}]`); } // Response matched a retriable status but not a retriable body - treat as successful return response } } |