@@ -16,7 +16,7 @@ import cookie from 'cookie'
16
16
import { getProperty } from 'dot-prop'
17
17
import generateETag from 'etag'
18
18
import getAvailablePort from 'get-port'
19
- import httpProxy from 'http-proxy'
19
+ import httpProxy , { type ServerOptions } from 'http-proxy'
20
20
import { createProxyMiddleware } from 'http-proxy-middleware'
21
21
import { jwtDecode , type JwtPayload } from 'jwt-decode'
22
22
import { locatePath } from 'locate-path'
@@ -43,9 +43,25 @@ import { NFFunctionName, NFFunctionRoute, NFRequestID, headersForPath, parseHead
43
43
import { generateRequestID } from './request-id.js'
44
44
import { createRewriter , onChanges } from './rules-proxy.js'
45
45
import { signRedirect } from './sign-redirect.js'
46
- import type { Rewriter , ServerSettings } from './types.js'
46
+ import type { Rewriter , ExtraServerOptions , ServerSettings } from './types.js'
47
47
import { ClientRequest , IncomingMessage } from 'node:http'
48
48
49
+ declare module 'http' {
50
+ // This is only necessary because we're attaching custom junk to the `req` given to us
51
+ // by the `http-proxy` module. Since it in turn imports its request object type from `http`,
52
+ // we have no choice but to augment the `http` module itself globally.
53
+ // NOTE: to be extra clear, this is *augmenting* the existing type:
54
+ // https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces.
55
+ interface IncomingMessage {
56
+ originalBody ?: Buffer | null
57
+ protocol ?: string
58
+ hostname ?: string
59
+ __expectHeader ?: string
60
+ alternativePaths ?: string [ ]
61
+ proxyOptions : ServerOptions
62
+ }
63
+ }
64
+
49
65
const gunzip = util . promisify ( zlib . gunzip )
50
66
const gzip = util . promisify ( zlib . gzip )
51
67
const brotliDecompress = util . promisify ( zlib . brotliDecompress )
@@ -66,6 +82,18 @@ const setShouldGenerateETag = (req: IncomingMessage, shouldGenerateETag: ShouldG
66
82
req [ shouldGenerateETagSymbol ] = shouldGenerateETag
67
83
}
68
84
85
+ type ExtendedServerOptions = ServerOptions | ExtraServerOptions
86
+
87
+ const getExtraServerOption = (
88
+ options : ExtendedServerOptions ,
89
+ name : keyof ExtendedServerOptions ,
90
+ ) : ExtendedServerOptions [ typeof name ] => {
91
+ if ( name in options ) {
92
+ return options [ name ]
93
+ }
94
+ return
95
+ }
96
+
69
97
const decompressResponseBody = async function ( body : Buffer , contentEncoding = '' ) : Promise < Buffer > {
70
98
switch ( contentEncoding ) {
71
99
case 'gzip' :
@@ -158,7 +186,7 @@ const getStatic = async function (pathname: string, publicFolder: string): Promi
158
186
return `/${ path . relative ( publicFolder , file ) } `
159
187
}
160
188
161
- const isEndpointExists = async function ( endpoint : string , origin : string ) {
189
+ const isEndpointExists = async function ( endpoint : string , origin ? : string | undefined ) {
162
190
const url = new URL ( endpoint , origin )
163
191
try {
164
192
const res = await fetch ( url , { method : 'HEAD' } )
@@ -274,19 +302,21 @@ const serveRedirect = async function ({
274
302
res,
275
303
siteInfo,
276
304
} : {
277
- options : IncomingMessage [ 'proxyOptions' ]
305
+ options : ExtendedServerOptions
278
306
req : IncomingMessage
279
307
res : ServerResponse
280
308
match : Match | null
281
309
} & Record < string , $TSFixMe > ) {
282
310
if ( ! match ) return proxy . web ( req , res , options )
283
311
284
- options = options || req . proxyOptions || { }
285
- options . match = null
312
+ options = {
313
+ ...( options ?? req . proxyOptions ) ,
314
+ match : null ,
315
+ }
286
316
287
317
if ( match . force404 ) {
288
318
res . writeHead ( 404 )
289
- res . end ( await render404 ( options . publicFolder ) )
319
+ res . end ( await render404 ( getExtraServerOption ( options , ' publicFolder' ) ) )
290
320
return
291
321
}
292
322
@@ -314,11 +344,11 @@ const serveRedirect = async function ({
314
344
}
315
345
}
316
346
317
- if ( isFunction ( options . functionsPort , req . url ?? '' ) ) {
347
+ if ( isFunction ( getExtraServerOption ( options , ' functionsPort' ) , req . url ?? '' ) ) {
318
348
return proxy . web ( req , res , { target : options . functionsServer } )
319
349
}
320
350
321
- const urlForAddons = getAddonUrl ( options . addonsUrls ?? { } , req )
351
+ const urlForAddons = getAddonUrl ( getExtraServerOption ( options , ' addonsUrls' ) ?? { } , req )
322
352
if ( urlForAddons ) {
323
353
return handleAddonUrl ( { req, res, addonUrl : urlForAddons } )
324
354
}
@@ -354,9 +384,15 @@ const serveRedirect = async function ({
354
384
if ( ( jwtValue . exp || 0 ) < Math . round ( Date . now ( ) / MILLISEC_TO_SEC ) ) {
355
385
console . warn ( NETLIFYDEVWARN , 'Expired JWT provided in request' , req . url )
356
386
} else {
357
- const presentedRoles = getProperty ( jwtValue , options . jwtRolePath ) || [ ]
387
+ // I think through some circuitous callback logic `options.jwtRolePath` is guaranteed to
388
+ // be defined at this point, but I don't think it's possible to convince TS of this.
389
+ const presentedRoles = getProperty ( jwtValue , getExtraServerOption ( options , 'jwtRolePath' ) ) ?? [ ]
358
390
if ( ! Array . isArray ( presentedRoles ) ) {
359
- console . warn ( NETLIFYDEVWARN , `Invalid roles value provided in JWT ${ options . jwtRolePath } ` , presentedRoles )
391
+ console . warn (
392
+ NETLIFYDEVWARN ,
393
+ `Invalid roles value provided in JWT ${ getExtraServerOption ( options , 'jwtRolePath' ) } ` ,
394
+ presentedRoles ,
395
+ )
360
396
res . writeHead ( 400 )
361
397
res . end ( 'Invalid JWT provided. Please see logs for more info.' )
362
398
return
@@ -375,12 +411,18 @@ const serveRedirect = async function ({
375
411
match . proxyHeaders &&
376
412
Object . entries ( match . proxyHeaders ) . some ( ( [ key , val ] ) => key . toLowerCase ( ) === 'x-nf-hidden-proxy' && val === 'true' )
377
413
378
- const staticFile = await getStatic ( decodeURIComponent ( reqUrl . pathname ) , options . publicFolder ?? '' )
414
+ const staticFile = await getStatic (
415
+ decodeURIComponent ( reqUrl . pathname ) ,
416
+ getExtraServerOption ( options , 'publicFolder' ) ?? '' ,
417
+ )
379
418
const endpointExists =
380
419
! staticFile &&
381
420
! isHiddenProxy &&
382
421
process . env . NETLIFY_DEV_SERVER_CHECK_SSG_ENDPOINTS &&
383
- ( await isEndpointExists ( decodeURIComponent ( reqUrl . pathname ) , options . target ) )
422
+ // @ts -expect-error(serhalp) -- TODO verify if the intent is that `options.target` is
423
+ // always a string (if so, use `typeof` to only pass strings), or if this is implicitly
424
+ // relying on built-in coercion to a string of the various support target URL-ish types.
425
+ ( await isEndpointExists ( decodeURIComponent ( reqUrl . pathname ) , getExtraServerOption ( options , 'target' ) ) )
384
426
if ( staticFile || endpointExists ) {
385
427
const pathname = staticFile || reqUrl . pathname
386
428
req . url = encodeURI ( pathname ) + reqUrl . search
@@ -390,7 +432,7 @@ const serveRedirect = async function ({
390
432
}
391
433
}
392
434
393
- if ( match . force || ! staticFile || ! options . framework || req . method === 'POST' ) {
435
+ if ( match . force || ! staticFile || ! getExtraServerOption ( options , ' framework' ) || req . method === 'POST' ) {
394
436
// construct destination URL from redirect rule match
395
437
const dest = new URL ( match . to , `${ reqUrl . protocol } //${ reqUrl . host } ` )
396
438
@@ -439,17 +481,18 @@ const serveRedirect = async function ({
439
481
! isInternal ( destURL ) &&
440
482
( ct . endsWith ( '/x-www-form-urlencoded' ) || ct === 'multipart/form-data' )
441
483
) {
442
- return proxy . web ( req , res , { target : options . functionsServer } )
484
+ return proxy . web ( req , res , { target : getExtraServerOption ( options , ' functionsServer' ) } )
443
485
}
444
486
445
- const destStaticFile = await getStatic ( dest . pathname , options . publicFolder ?? '' )
487
+ const destStaticFile = await getStatic ( dest . pathname , getExtraServerOption ( options , 'functionsServer' ) ?? '' )
446
488
const matchingFunction =
447
489
functionsRegistry &&
448
490
( await functionsRegistry . getFunctionForURLPath ( destURL , req . method , ( ) => Boolean ( destStaticFile ) ) )
449
491
let statusValue : number | undefined
450
492
if (
451
493
match . force ||
452
- ( ! staticFile && ( ( ! options . framework && destStaticFile ) || isInternal ( destURL ) || matchingFunction ) )
494
+ ( ! staticFile &&
495
+ ( ( ! getExtraServerOption ( options , 'framework' ) && destStaticFile ) || isInternal ( destURL ) || matchingFunction ) )
453
496
) {
454
497
req . url = destStaticFile ? destStaticFile + dest . search : destURL
455
498
const { status } = match
@@ -468,12 +511,12 @@ const serveRedirect = async function ({
468
511
req . headers [ 'x-netlify-original-pathname' ] = url . pathname
469
512
req . headers [ 'x-netlify-original-search' ] = url . search
470
513
471
- return proxy . web ( req , res , { headers : functionHeaders , target : options . functionsServer } )
514
+ return proxy . web ( req , res , { headers : functionHeaders , target : getExtraServerOption ( options , ' functionsServer' ) } )
472
515
}
473
516
if ( isImageRequest ( req ) ) {
474
517
return imageProxy ( req , res )
475
518
}
476
- const addonUrl = getAddonUrl ( options . addonsUrls ?? { } , req )
519
+ const addonUrl = getAddonUrl ( getExtraServerOption ( options , ' addonsUrls' ) ?? { } , req )
477
520
if ( addonUrl ) {
478
521
return handleAddonUrl ( { req, res, addonUrl } )
479
522
}
@@ -585,7 +628,7 @@ const initializeProxy = async function ({
585
628
// The request has failed but we might still have a matching redirect
586
629
// rule (without `force`) that should kick in. This is how we mimic the
587
630
// file shadowing behavior from the CDN.
588
- if ( req . proxyOptions && req . proxyOptions . match ) {
631
+ if ( req . proxyOptions ? .match ) {
589
632
return serveRedirect ( {
590
633
// We don't want to match functions at this point because any redirects
591
634
// to functions will have already been processed, so we don't supply a
0 commit comments