( state: ClientState, request: Request, resolved: ResolvedOptions, effectiveTimeoutMs: number | undefined, userSignal: AbortSignal | undefined, )
| 144 | // low-level `request` entrypoint (oRPC's pre-built Request) share one retry/timeout |
| 145 | // /error policy. |
| 146 | async function execute( |
| 147 | state: ClientState, |
| 148 | request: Request, |
| 149 | resolved: ResolvedOptions, |
| 150 | effectiveTimeoutMs: number | undefined, |
| 151 | userSignal: AbortSignal | undefined, |
| 152 | ): Promise<Response> { |
| 153 | const { method, retryAttempts: effectiveRetryAttempts, throwOnError } = resolved |
| 154 | |
| 155 | for (let attempt = 0; ; attempt++) { |
| 156 | // Fresh timeout budget per attempt, merged with the persistent user/oRPC signal. |
| 157 | // Mirrors the old recursion that re-ran buildSignal() on every retry. Keep this |
| 158 | // INSIDE the loop — hoisting it would leak attempt 0's timeout into later attempts. |
| 159 | const signal = mergeSignal(userSignal, effectiveTimeoutMs) |
| 160 | |
| 161 | // clone-on-retry: the first fetch consumes the Request body, so any attempt that |
| 162 | // may still be retried sends a clone and keeps `request` as the pristine replay copy. |
| 163 | const sendable = attempt < effectiveRetryAttempts ? request.clone() : request |
| 164 | |
| 165 | const ctx: FetchContext = { |
| 166 | request: sendable, |
| 167 | options: resolved, |
| 168 | attempt, |
| 169 | meta: new Map(), |
| 170 | } |
| 171 | |
| 172 | await runHooks(state.hooks.onRequest, ctx) |
| 173 | |
| 174 | const init: RequestInit & { dispatcher?: unknown, verbose?: boolean } = { signal } |
| 175 | if (state.dispatcher !== undefined) |
| 176 | init.dispatcher = state.dispatcher |
| 177 | if (isVerbose()) |
| 178 | init.verbose = true |
| 179 | |
| 180 | try { |
| 181 | ctx.response = await fetch(ctx.request, init) |
| 182 | } |
| 183 | catch (err) { |
| 184 | ctx.error = err |
| 185 | // Snapshot the abort cause before onRequestError hooks rewrite ctx.error into BaseError. |
| 186 | const userAborted = userSignal?.aborted === true |
| 187 | await runHooks(state.hooks.onRequestError, ctx) |
| 188 | |
| 189 | // User aborts (ctrl+C) must never retry. Timeouts and other transport errors fall |
| 190 | // through to shouldRetry, which enforces the method allowlist. |
| 191 | if (!userAborted && attempt < effectiveRetryAttempts && shouldRetry(ctx.error, ctx)) { |
| 192 | state.logger?.({ phase: 'retry', method, url: redactBearer(ctx.request.url), attempt: attempt + 1 }) |
| 193 | const delay = backoffDelay(attempt + 1) |
| 194 | if (delay > 0) |
| 195 | await new Promise(resolve => setTimeout(resolve, delay)) |
| 196 | continue |
| 197 | } |
| 198 | |
| 199 | const finalErr = ctx.error |
| 200 | if (finalErr instanceof Error && typeof Error.captureStackTrace === 'function') |
| 201 | Error.captureStackTrace(finalErr, execute) |
| 202 | throw finalErr |
| 203 | } |
no test coverage detected