Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: Handle invalid `Location` headers for redirects (#26)

authored by

Phil Pluckthun and committed by
GitHub
e4d1b4db 7e469cc2

+40 -16
+5
.changeset/fresh-states-punch.md
··· 1 + --- 2 + 'fetch-nodeshim': patch 3 + --- 4 + 5 + Protect against invalid `Location` URI
+17 -10
src/__tests__/fetch.test.ts
··· 290 290 }); 291 291 }); 292 292 293 - it.each([['follow'], ['manual']] as const)( 294 - 'should treat broken redirect as ordinary response (%s)', 295 - async redirect => { 296 - const response = await fetch(new URL('redirect/no-location', baseURL), { 297 - redirect, 298 - }); 299 - expect(response.status).toBe(301); 300 - expect(response.headers.has('location')).toBe(false); 301 - } 302 - ); 293 + it('should treat broken redirect as ordinary response for redirect: "manual"', async () => { 294 + const response = await fetch(new URL('redirect/no-location', baseURL), { 295 + redirect: 'manual', 296 + }); 297 + expect(response.status).toBe(301); 298 + expect(response.headers.has('location')).toBe(false); 299 + }); 300 + 301 + it('should throw on broken redirects for redirect: "follow"', async () => { 302 + await expect(() => 303 + fetch(new URL('redirect/no-location', baseURL), { 304 + redirect: 'follow', 305 + }) 306 + ).rejects.toThrowErrorMatchingInlineSnapshot( 307 + `[Error: URI requested responds with an invalid redirect URL]` 308 + ); 309 + }); 303 310 304 311 it('should throw a TypeError on an invalid redirect option', async () => { 305 312 await expect(() =>
+18 -6
src/fetch.ts
··· 11 11 /** Maximum allowed redirects (matching Chromium's limit) */ 12 12 const MAX_REDIRECTS = 20; 13 13 14 + const parseURL = (input: string, base?: string | URL): URL | null => { 15 + try { 16 + return new URL(input, base); 17 + } catch { 18 + return null; 19 + } 20 + }; 21 + 14 22 /** Convert Node.js raw headers array to Headers */ 15 23 const headersOfRawHeaders = (rawHeaders: readonly string[]): Headers => { 16 24 const headers = new Headers(); ··· 186 194 if (isRedirectCode(init.status)) { 187 195 const location = init.headers.get('Location'); 188 196 const locationURL = 189 - location != null ? new URL(location, requestUrl) : null; 197 + location != null ? parseURL(location, requestUrl) : null; 190 198 if (redirect === 'error') { 191 - // TODO: do we need a special Error instance here? 192 199 reject( 193 200 new Error( 194 201 'URI requested responds with a redirect, redirect mode is set to error' 195 202 ) 196 203 ); 197 204 return; 198 - } else if (redirect === 'manual' && locationURL !== null) { 199 - init.headers.set('Location', locationURL.toString()); 200 - } else if (redirect === 'follow' && locationURL !== null) { 201 - if (++redirects > MAX_REDIRECTS) { 205 + } else if (redirect === 'manual' && location) { 206 + init.headers.set('Location', locationURL?.href ?? location); 207 + } else if (redirect === 'follow') { 208 + if (locationURL === null) { 209 + reject( 210 + new Error('URI requested responds with an invalid redirect URL') 211 + ); 212 + return; 213 + } else if (++redirects > MAX_REDIRECTS) { 202 214 reject(new Error(`maximum redirect reached at: ${requestUrl}`)); 203 215 return; 204 216 } else if (