(
url: string,
init?: RequestInit,
maxRedirects: number = 5,
agentOptions?: SecureRequestAgentOptions
)
| 233 | * @throws Error if any URL in the redirect chain is denied |
| 234 | */ |
| 235 | export async function secureFetch( |
| 236 | url: string, |
| 237 | init?: RequestInit, |
| 238 | maxRedirects: number = 5, |
| 239 | agentOptions?: SecureRequestAgentOptions |
| 240 | ): Promise<Response> { |
| 241 | let currentUrl = url |
| 242 | let redirectCount = 0 |
| 243 | let currentInit = { ...init, redirect: 'manual' as const } // Disable automatic redirects |
| 244 | |
| 245 | while (redirectCount <= maxRedirects) { |
| 246 | const resolved = await resolveAndValidate(currentUrl) |
| 247 | const agent = createPinnedAgent(resolved, agentOptions) |
| 248 | |
| 249 | const response = await fetch(currentUrl, { ...currentInit, agent: () => agent }) |
| 250 | |
| 251 | // If it's a successful response (not a redirect), return it |
| 252 | if (response.status < 300 || response.status >= 400) { |
| 253 | return response |
| 254 | } |
| 255 | |
| 256 | // Handle redirect |
| 257 | const location = response.headers.get('location') |
| 258 | if (!location) { |
| 259 | // No location header, but it's a redirect status - return the response |
| 260 | return response |
| 261 | } |
| 262 | |
| 263 | redirectCount++ |
| 264 | |
| 265 | if (redirectCount > maxRedirects) { |
| 266 | throw new Error('Too many redirects') |
| 267 | } |
| 268 | |
| 269 | // Resolve the redirect URL (handle relative URLs) |
| 270 | currentUrl = new URL(location, currentUrl).toString() |
| 271 | |
| 272 | // Handle method changes for redirects according to HTTP specs |
| 273 | if (response.status === 301 || response.status === 302 || response.status === 303) { |
| 274 | // For 303, or when redirecting POST/PUT/PATCH requests, change to GET |
| 275 | if (response.status === 303 || (currentInit.method && ['POST', 'PUT', 'PATCH'].includes(currentInit.method.toUpperCase()))) { |
| 276 | currentInit = { |
| 277 | ...currentInit, |
| 278 | method: 'GET', |
| 279 | body: undefined |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | throw new Error('Too many redirects') |
| 286 | } |
| 287 | |
| 288 | type ResolvedTarget = { |
| 289 | hostname: string |
no test coverage detected