(err: unknown)
| 194 | } |
| 195 | |
| 196 | function extractRetryAfterMs(err: unknown): number | undefined { |
| 197 | if (typeof err !== 'object' || err === null) return undefined; |
| 198 | const headers = |
| 199 | (err as { headers?: Record<string, string | string[] | undefined> }).headers ?? |
| 200 | (err as { response?: { headers?: Record<string, string | string[] | undefined> } }).response |
| 201 | ?.headers; |
| 202 | const direct = (err as { retryAfter?: unknown }).retryAfter; |
| 203 | const raw = |
| 204 | pickHeader(headers, 'retry-after') ?? |
| 205 | (typeof direct === 'string' || typeof direct === 'number' ? String(direct) : undefined); |
| 206 | if (raw === undefined) return undefined; |
| 207 | const trimmed = raw.trim(); |
| 208 | // Empty / whitespace-only headers must not coerce to 0 via Number(''), |
| 209 | // which would otherwise emit a zero-delay retry hint and defeat backoff. |
| 210 | if (trimmed.length === 0) return undefined; |
| 211 | // Numeric path first — explicit shape so '7' / '1.5' parse but a |
| 212 | // Date-formatted header ('Wed, 21 Oct 2015 …') falls through to Date.parse. |
| 213 | if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) { |
| 214 | const seconds = Number(trimmed); |
| 215 | if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000); |
| 216 | } |
| 217 | const dateMs = Date.parse(trimmed); |
| 218 | if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now()); |
| 219 | return undefined; |
| 220 | } |
| 221 | |
| 222 | function pickHeader( |
| 223 | headers: Record<string, string | string[] | undefined> | undefined, |
no test coverage detected