Skip to content

Commit 1bd8eac

Browse files
committed
wip: how about maybe this, good luck Nathan!
1 parent b9cee27 commit 1bd8eac

File tree

4 files changed

+76
-77
lines changed

4 files changed

+76
-77
lines changed

src/utils/proxy.ts

+63-20
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import cookie from 'cookie'
1616
import { getProperty } from 'dot-prop'
1717
import generateETag from 'etag'
1818
import getAvailablePort from 'get-port'
19-
import httpProxy from 'http-proxy'
19+
import httpProxy, { type ServerOptions } from 'http-proxy'
2020
import { createProxyMiddleware } from 'http-proxy-middleware'
2121
import { jwtDecode, type JwtPayload } from 'jwt-decode'
2222
import { locatePath } from 'locate-path'
@@ -43,9 +43,25 @@ import { NFFunctionName, NFFunctionRoute, NFRequestID, headersForPath, parseHead
4343
import { generateRequestID } from './request-id.js'
4444
import { createRewriter, onChanges } from './rules-proxy.js'
4545
import { signRedirect } from './sign-redirect.js'
46-
import type { Rewriter, ServerSettings } from './types.js'
46+
import type { Rewriter, ExtraServerOptions, ServerSettings } from './types.js'
4747
import { ClientRequest, IncomingMessage } from 'node:http'
4848

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+
4965
const gunzip = util.promisify(zlib.gunzip)
5066
const gzip = util.promisify(zlib.gzip)
5167
const brotliDecompress = util.promisify(zlib.brotliDecompress)
@@ -66,6 +82,18 @@ const setShouldGenerateETag = (req: IncomingMessage, shouldGenerateETag: ShouldG
6682
req[shouldGenerateETagSymbol] = shouldGenerateETag
6783
}
6884

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+
6997
const decompressResponseBody = async function (body: Buffer, contentEncoding = ''): Promise<Buffer> {
7098
switch (contentEncoding) {
7199
case 'gzip':
@@ -158,7 +186,7 @@ const getStatic = async function (pathname: string, publicFolder: string): Promi
158186
return `/${path.relative(publicFolder, file)}`
159187
}
160188

161-
const isEndpointExists = async function (endpoint: string, origin: string) {
189+
const isEndpointExists = async function (endpoint: string, origin?: string | undefined) {
162190
const url = new URL(endpoint, origin)
163191
try {
164192
const res = await fetch(url, { method: 'HEAD' })
@@ -274,19 +302,21 @@ const serveRedirect = async function ({
274302
res,
275303
siteInfo,
276304
}: {
277-
options: IncomingMessage['proxyOptions']
305+
options: ExtendedServerOptions
278306
req: IncomingMessage
279307
res: ServerResponse
280308
match: Match | null
281309
} & Record<string, $TSFixMe>) {
282310
if (!match) return proxy.web(req, res, options)
283311

284-
options = options || req.proxyOptions || {}
285-
options.match = null
312+
options = {
313+
...(options ?? req.proxyOptions),
314+
match: null,
315+
}
286316

287317
if (match.force404) {
288318
res.writeHead(404)
289-
res.end(await render404(options.publicFolder))
319+
res.end(await render404(getExtraServerOption(options, 'publicFolder')))
290320
return
291321
}
292322

@@ -314,11 +344,11 @@ const serveRedirect = async function ({
314344
}
315345
}
316346

317-
if (isFunction(options.functionsPort, req.url ?? '')) {
347+
if (isFunction(getExtraServerOption(options, 'functionsPort'), req.url ?? '')) {
318348
return proxy.web(req, res, { target: options.functionsServer })
319349
}
320350

321-
const urlForAddons = getAddonUrl(options.addonsUrls ?? {}, req)
351+
const urlForAddons = getAddonUrl(getExtraServerOption(options, 'addonsUrls') ?? {}, req)
322352
if (urlForAddons) {
323353
return handleAddonUrl({ req, res, addonUrl: urlForAddons })
324354
}
@@ -354,9 +384,15 @@ const serveRedirect = async function ({
354384
if ((jwtValue.exp || 0) < Math.round(Date.now() / MILLISEC_TO_SEC)) {
355385
console.warn(NETLIFYDEVWARN, 'Expired JWT provided in request', req.url)
356386
} 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')) ?? []
358390
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+
)
360396
res.writeHead(400)
361397
res.end('Invalid JWT provided. Please see logs for more info.')
362398
return
@@ -375,12 +411,18 @@ const serveRedirect = async function ({
375411
match.proxyHeaders &&
376412
Object.entries(match.proxyHeaders).some(([key, val]) => key.toLowerCase() === 'x-nf-hidden-proxy' && val === 'true')
377413

378-
const staticFile = await getStatic(decodeURIComponent(reqUrl.pathname), options.publicFolder ?? '')
414+
const staticFile = await getStatic(
415+
decodeURIComponent(reqUrl.pathname),
416+
getExtraServerOption(options, 'publicFolder') ?? '',
417+
)
379418
const endpointExists =
380419
!staticFile &&
381420
!isHiddenProxy &&
382421
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')))
384426
if (staticFile || endpointExists) {
385427
const pathname = staticFile || reqUrl.pathname
386428
req.url = encodeURI(pathname) + reqUrl.search
@@ -390,7 +432,7 @@ const serveRedirect = async function ({
390432
}
391433
}
392434

393-
if (match.force || !staticFile || !options.framework || req.method === 'POST') {
435+
if (match.force || !staticFile || !getExtraServerOption(options, 'framework') || req.method === 'POST') {
394436
// construct destination URL from redirect rule match
395437
const dest = new URL(match.to, `${reqUrl.protocol}//${reqUrl.host}`)
396438

@@ -439,17 +481,18 @@ const serveRedirect = async function ({
439481
!isInternal(destURL) &&
440482
(ct.endsWith('/x-www-form-urlencoded') || ct === 'multipart/form-data')
441483
) {
442-
return proxy.web(req, res, { target: options.functionsServer })
484+
return proxy.web(req, res, { target: getExtraServerOption(options, 'functionsServer') })
443485
}
444486

445-
const destStaticFile = await getStatic(dest.pathname, options.publicFolder ?? '')
487+
const destStaticFile = await getStatic(dest.pathname, getExtraServerOption(options, 'functionsServer') ?? '')
446488
const matchingFunction =
447489
functionsRegistry &&
448490
(await functionsRegistry.getFunctionForURLPath(destURL, req.method, () => Boolean(destStaticFile)))
449491
let statusValue: number | undefined
450492
if (
451493
match.force ||
452-
(!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || matchingFunction))
494+
(!staticFile &&
495+
((!getExtraServerOption(options, 'framework') && destStaticFile) || isInternal(destURL) || matchingFunction))
453496
) {
454497
req.url = destStaticFile ? destStaticFile + dest.search : destURL
455498
const { status } = match
@@ -468,12 +511,12 @@ const serveRedirect = async function ({
468511
req.headers['x-netlify-original-pathname'] = url.pathname
469512
req.headers['x-netlify-original-search'] = url.search
470513

471-
return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
514+
return proxy.web(req, res, { headers: functionHeaders, target: getExtraServerOption(options, 'functionsServer') })
472515
}
473516
if (isImageRequest(req)) {
474517
return imageProxy(req, res)
475518
}
476-
const addonUrl = getAddonUrl(options.addonsUrls ?? {}, req)
519+
const addonUrl = getAddonUrl(getExtraServerOption(options, 'addonsUrls') ?? {}, req)
477520
if (addonUrl) {
478521
return handleAddonUrl({ req, res, addonUrl })
479522
}
@@ -585,7 +628,7 @@ const initializeProxy = async function ({
585628
// The request has failed but we might still have a matching redirect
586629
// rule (without `force`) that should kick in. This is how we mimic the
587630
// file shadowing behavior from the CDN.
588-
if (req.proxyOptions && req.proxyOptions.match) {
631+
if (req.proxyOptions?.match) {
589632
return serveRedirect({
590633
// We don't want to match functions at this point because any redirects
591634
// to functions will have already been processed, so we don't supply a

src/utils/types.ts

+13
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ export type ServerSettings = BaseServerSettings & {
5353
skipWaitPort?: boolean
5454
}
5555

56+
export interface ExtraServerOptions {
57+
status?: number
58+
match: Match | null
59+
staticFile?: string | false
60+
target: string
61+
publicFolder?: string | undefined
62+
functionsPort: number
63+
jwtRolePath: string
64+
framework?: string | undefined
65+
addonsUrls?: Record<string, string>
66+
functionsServer?: string | null
67+
}
68+
5669
export type Rewriter = (req: IncomingMessage) => Promise<Match | null>
5770

5871
export interface SiteInfo {

types/http-proxy/index.d.ts

-39
This file was deleted.

types/http/index.d.ts

-18
This file was deleted.

0 commit comments

Comments
 (0)