From 8a612627429718781616a0265833473857e3ae6c Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Mon, 25 Oct 2021 19:11:47 +0200 Subject: [PATCH 1/7] Add logic to skip middleware on subrequests --- packages/next/server/next-server.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 0cc26446adb206b..c47f25a9a428fd2 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -102,6 +102,7 @@ import isError from '../lib/is-error' import { getMiddlewareInfo } from './require' import { parseUrl as simpleParseUrl } from '../shared/lib/router/utils/parse-url' import { MIDDLEWARE_ROUTE } from '../lib/constants' +import { NextResponse } from './web/spec-extension/response' import { run } from './web/sandbox' import type { FetchEventResult } from './web/types' import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' @@ -621,6 +622,9 @@ export default class Server { } } + const subreq = params.request.headers[`x-middleware-subrequest`] + const subrequests = typeof subreq === 'string' ? subreq.split(':') : [] + let result: FetchEventResult | null = null for (const middleware of this.middleware || []) { @@ -639,6 +643,15 @@ export default class Server { serverless: this._isLikeServerless, }) + if (subrequests.includes(middlewareInfo.name)) { + result = { + promise: Promise.resolve(), + response: NextResponse.next(), + waitUntil: Promise.resolve(), + } + continue + } + result = await run({ name: middlewareInfo.name, paths: middlewareInfo.paths, From a6b8acbce0aead06020faaa202287cc73cac19c3 Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Mon, 25 Oct 2021 19:42:05 +0200 Subject: [PATCH 2/7] Allow subrequests --- packages/next/server/web/sandbox/sandbox.ts | 40 +++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 51b95cb80c409ea..1905bee01e5d0fc 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -1,4 +1,4 @@ -import type { RequestData, FetchEventResult } from '../types' +import type { RequestData, FetchEventResult, NodeHeaders } from '../types' import { Blob, File, FormData } from 'next/dist/compiled/formdata-node' import { dirname } from 'path' import { ReadableStream } from 'next/dist/compiled/web-streams-polyfill' @@ -57,7 +57,20 @@ export async function run(params: { }, Crypto: polyfills.Crypto, crypto: new polyfills.Crypto(), - fetch, + fetch: (input: RequestInfo, init: RequestInit = {}) => { + const url = getFetchURL(input, params.request.headers) + init.headers = getFetchHeaders(params.name, init) + if (isRequestLike(input)) { + return fetch(url, { + ...init, + headers: { + ...Object.fromEntries(input.headers), + ...Object.fromEntries(init.headers), + }, + }) + } + return fetch(url, init) + }, File, FormData, process: { env: { ...process.env } }, @@ -168,3 +181,26 @@ function sandboxRequire(referrer: string, specifier: string) { module.loaded = true return module.exports } + +function getFetchHeaders(middleware: string, init: RequestInit) { + const headers = new Headers(init.headers ?? {}) + const prevsub = headers.get(`x-middleware-subrequest`) || '' + const value = prevsub.split(':').concat(middleware).join(':') + headers.set(`x-middleware-subrequest`, value) + headers.set(`user-agent`, `Next.JS Middleware`) + return headers +} + +function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string { + const initurl = isRequestLike(input) ? input.url : input + if (initurl.startsWith('/')) { + const host = headers.host?.toString() + const localhost = host === 'localhost' || host?.startsWith('localhost:') + return `${localhost ? 'http' : 'https'}://${host}${initurl}` + } + return initurl +} + +function isRequestLike(obj: unknown): obj is Request { + return Boolean(obj && typeof obj === 'object' && 'url' in obj) +} From a509ae063d807eaf1868980b5c7c08cf280a46c7 Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Mon, 25 Oct 2021 22:55:55 +0200 Subject: [PATCH 3/7] Update to new API --- packages/next/server/dev/next-dev-server.ts | 3 -- packages/next/server/next-server.ts | 5 -- packages/next/server/web/adapter.ts | 49 ++++++++++++------- packages/next/server/web/error.ts | 10 ++++ .../server/web/spec-extension/fetch-event.ts | 5 ++ .../next/server/web/spec-extension/request.ts | 2 +- packages/next/server/web/types.ts | 1 - 7 files changed, 48 insertions(+), 27 deletions(-) create mode 100644 packages/next/server/web/error.ts diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 7e971f6eb89afde..5912772b10a58e2 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -475,9 +475,6 @@ export default class DevServer extends Server { }): Promise { try { const result = await super.runMiddleware(params) - result?.promise.catch((error) => - this.logErrorWithOriginalStack(error, 'unhandledRejection', 'client') - ) result?.waitUntil.catch((error) => this.logErrorWithOriginalStack(error, 'unhandledRejection', 'client') ) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index c47f25a9a428fd2..0d9149777e1f863 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -645,7 +645,6 @@ export default class Server { if (subrequests.includes(middlewareInfo.name)) { result = { - promise: Promise.resolve(), response: NextResponse.next(), waitUntil: Promise.resolve(), } @@ -669,10 +668,6 @@ export default class Server { }) if (!this.renderOpts.dev) { - result.promise.catch((error) => { - console.error(`Uncaught: middleware error after responding`, error) - }) - result.waitUntil.catch((error) => { console.error(`Uncaught: middleware waitUntil errored`, error) }) diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts index e723b7eca50feeb..32e210c4c5921a9 100644 --- a/packages/next/server/web/adapter.ts +++ b/packages/next/server/web/adapter.ts @@ -1,36 +1,51 @@ import type { RequestData, FetchEventResult } from './types' - +import { DeprecationError } from './error' import { fromNodeHeaders } from './utils' import { NextFetchEvent } from './spec-extension/fetch-event' -import { NextRequest } from './spec-extension/request' +import { NextRequest, RequestInit } from './spec-extension/request' import { NextResponse } from './spec-extension/response' -import { waitUntilSymbol, responseSymbol } from './spec-compliant/fetch-event' +import { waitUntilSymbol } from './spec-compliant/fetch-event' export async function adapter(params: { - handler: (event: NextFetchEvent) => void | Promise + handler: (request: NextRequest, event: NextFetchEvent) => Promise request: RequestData }): Promise { const url = params.request.url.startsWith('/') ? `https://${params.request.headers.host}${params.request.url}` : params.request.url - const event = new NextFetchEvent( - new NextRequest(url, { - geo: params.request.geo, - headers: fromNodeHeaders(params.request.headers), - ip: params.request.ip, - method: params.request.method, - nextConfig: params.request.nextConfig, - page: params.request.page, - }) - ) + const request = new NextRequestHint(url, { + geo: params.request.geo, + headers: fromNodeHeaders(params.request.headers), + ip: params.request.ip, + method: params.request.method, + nextConfig: params.request.nextConfig, + page: params.request.page, + }) - const handled = params.handler(event) - const original = await event[responseSymbol] + const event = new NextFetchEvent(request) + const original = await params.handler(request, event) return { - promise: Promise.resolve(handled), response: original || NextResponse.next(), waitUntil: Promise.all(event[waitUntilSymbol]), } } + +class NextRequestHint extends NextRequest { + constructor(input: Request | string, init: RequestInit = {}) { + super(input, init) + } + + get request() { + throw new DeprecationError() + } + + respondWith() { + throw new DeprecationError() + } + + waitUntil() { + throw new DeprecationError() + } +} diff --git a/packages/next/server/web/error.ts b/packages/next/server/web/error.ts new file mode 100644 index 000000000000000..8efcdfb0836444b --- /dev/null +++ b/packages/next/server/web/error.ts @@ -0,0 +1,10 @@ +export class DeprecationError extends Error { + constructor() { + super(`Middleware now accepts an async API directly with the form: + + export function middleware(request, event) { + return new Response("Hello " + request.url) + } + `) + } +} diff --git a/packages/next/server/web/spec-extension/fetch-event.ts b/packages/next/server/web/spec-extension/fetch-event.ts index 09613d33da53959..1d6d704ed3bdb85 100644 --- a/packages/next/server/web/spec-extension/fetch-event.ts +++ b/packages/next/server/web/spec-extension/fetch-event.ts @@ -1,3 +1,4 @@ +import { DeprecationError } from '../error' import { FetchEvent } from '../spec-compliant/fetch-event' import { NextRequest } from './request' @@ -7,4 +8,8 @@ export class NextFetchEvent extends FetchEvent { super(request) this.request = request } + + respondWith() { + throw new DeprecationError() + } } diff --git a/packages/next/server/web/spec-extension/request.ts b/packages/next/server/web/spec-extension/request.ts index 2a8e80f1b06f7c9..3bae487549cf53d 100644 --- a/packages/next/server/web/spec-extension/request.ts +++ b/packages/next/server/web/spec-extension/request.ts @@ -91,7 +91,7 @@ export class NextRequest extends Request { } } -interface RequestInit extends globalThis.RequestInit { +export interface RequestInit extends globalThis.RequestInit { geo?: { city?: string country?: string diff --git a/packages/next/server/web/types.ts b/packages/next/server/web/types.ts index b6f04fe5d4f7e74..3dc2518e25e87d9 100644 --- a/packages/next/server/web/types.ts +++ b/packages/next/server/web/types.ts @@ -26,7 +26,6 @@ export interface RequestData { } export interface FetchEventResult { - promise: Promise response: Response waitUntil: Promise } From 06f7010fa0b6448f4f3ebee756df147e3447383c Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Mon, 25 Oct 2021 22:56:04 +0200 Subject: [PATCH 4/7] Update tests --- .../middleware-base-path/pages/_middleware.js | 8 +- .../pages/interface/_middleware.js | 24 ++--- .../pages/redirects/_middleware.js | 8 +- .../pages/responses/_middleware.js | 100 ++++++++++-------- .../pages/rewrites/_middleware.js | 16 +-- .../middleware-core/test/index.test.js | 9 -- 6 files changed, 80 insertions(+), 85 deletions(-) diff --git a/test/integration/middleware-base-path/pages/_middleware.js b/test/integration/middleware-base-path/pages/_middleware.js index 3b229fa441eb02f..a4614ceeb547a7e 100644 --- a/test/integration/middleware-base-path/pages/_middleware.js +++ b/test/integration/middleware-base-path/pages/_middleware.js @@ -1,14 +1,14 @@ import { NextResponse } from 'next/server' -export function middleware(event) { - const url = event.request.nextUrl +export async function middleware(request) { + const url = request.nextUrl if (url.pathname === '/redirect-with-basepath' && !url.basePath) { url.basePath = '/root' - event.respondWith(NextResponse.redirect(url)) + return NextResponse.redirect(url) } if (url.pathname === '/redirect-with-basepath') { url.pathname = '/about' - event.respondWith(NextResponse.rewrite(url)) + return NextResponse.rewrite(url) } } diff --git a/test/integration/middleware-core/pages/interface/_middleware.js b/test/integration/middleware-core/pages/interface/_middleware.js index 428f94281052aeb..fc3d19c63ac609e 100644 --- a/test/integration/middleware-core/pages/interface/_middleware.js +++ b/test/integration/middleware-core/pages/interface/_middleware.js @@ -1,14 +1,12 @@ -export function middleware(event) { - event.respondWith( - new Response(null, { - headers: { - 'req-url-basepath': event.request.nextUrl.basePath, - 'req-url-pathname': event.request.nextUrl.pathname, - 'req-url-params': JSON.stringify(event.request.page.params), - 'req-url-page': event.request.page.name, - 'req-url-query': event.request.nextUrl.searchParams.get('foo'), - 'req-url-locale': event.request.nextUrl.locale, - }, - }) - ) +export function middleware(request) { + return new Response(null, { + headers: { + 'req-url-basepath': request.nextUrl.basePath, + 'req-url-pathname': request.nextUrl.pathname, + 'req-url-params': JSON.stringify(request.page.params), + 'req-url-page': request.page.name, + 'req-url-query': request.nextUrl.searchParams.get('foo'), + 'req-url-locale': request.nextUrl.locale, + }, + }) } diff --git a/test/integration/middleware-core/pages/redirects/_middleware.js b/test/integration/middleware-core/pages/redirects/_middleware.js index 92dd9cf3970931b..e97cb99011b04a2 100644 --- a/test/integration/middleware-core/pages/redirects/_middleware.js +++ b/test/integration/middleware-core/pages/redirects/_middleware.js @@ -1,9 +1,5 @@ -export function middleware(event) { - event.respondWith(handleRequest(event)) -} - -async function handleRequest(event) { - const url = event.request.nextUrl +export async function middleware(request) { + const url = request.nextUrl if (url.searchParams.get('foo') === 'bar') { url.pathname = '/redirects/new-home' diff --git a/test/integration/middleware-core/pages/responses/_middleware.js b/test/integration/middleware-core/pages/responses/_middleware.js index 1c7535bf6cb852c..a375375351e5246 100644 --- a/test/integration/middleware-core/pages/responses/_middleware.js +++ b/test/integration/middleware-core/pages/responses/_middleware.js @@ -2,10 +2,10 @@ import { createElement } from 'react' import { renderToString } from 'react-dom/server.browser' import { NextResponse } from 'next/server' -export async function middleware(event) { +export async function middleware(request, ev) { // eslint-disable-next-line no-undef const { readable, writable } = new TransformStream() - const url = event.request.nextUrl + const url = request.nextUrl const writer = writable.getWriter() const encoder = new TextEncoder() const next = NextResponse.next() @@ -13,60 +13,64 @@ export async function middleware(event) { // Sends a header if (url.pathname === '/responses/header') { next.headers.set('x-first-header', 'valid') - event.respondWith(next) + return next } // Header based on query param if (url.searchParams.get('nested-header') === 'true') { next.headers.set('x-nested-header', 'valid') - event.respondWith(next) + return next } // Streams a basic response if (url.pathname === '/responses/stream-a-response') { - event.respondWith(new Response(readable)) - writer.write(encoder.encode('this is a streamed ')) - writer.write(encoder.encode('response')) - writer.close() - return + ev.waitUntil( + (async () => { + writer.write(encoder.encode('this is a streamed ')) + writer.write(encoder.encode('response')) + writer.close() + })() + ) + + return new Response(readable) } if (url.pathname === '/responses/bad-status') { - event.respondWith( - new Response('Auth required', { - headers: { 'WWW-Authenticate': 'Basic realm="Secure Area"' }, - status: 401, - }) - ) + return new Response('Auth required', { + headers: { 'WWW-Authenticate': 'Basic realm="Secure Area"' }, + status: 401, + }) } if (url.pathname === '/responses/stream-long') { - event.respondWith(new Response(readable)) - writer.write(encoder.encode('this is a streamed '.repeat(10))) - await sleep(2000) - writer.write(encoder.encode('after 2 seconds '.repeat(10))) - await sleep(2000) - writer.write(encoder.encode('after 4 seconds '.repeat(10))) - await sleep(2000) - writer.close() - return + ev.waitUntil( + (async () => { + writer.write(encoder.encode('this is a streamed '.repeat(10))) + await sleep(2000) + writer.write(encoder.encode('after 2 seconds '.repeat(10))) + await sleep(2000) + writer.write(encoder.encode('after 4 seconds '.repeat(10))) + await sleep(2000) + writer.close() + })() + ) + + return new Response(readable) } // Sends response if (url.pathname === '/responses/send-response') { - return event.respondWith(new Response(JSON.stringify({ message: 'hi!' }))) + return new Response(JSON.stringify({ message: 'hi!' })) } // Render React component if (url.pathname === '/responses/react') { - return event.respondWith( - new Response( - renderToString( - createElement( - 'h1', - {}, - 'SSR with React! Hello, ' + url.searchParams.get('name') - ) + return new Response( + renderToString( + createElement( + 'h1', + {}, + 'SSR with React! Hello, ' + url.searchParams.get('name') ) ) ) @@ -74,21 +78,27 @@ export async function middleware(event) { // Stream React component if (url.pathname === '/responses/react-stream') { - event.respondWith(new Response(readable)) - writer.write( - encoder.encode(renderToString(createElement('h1', {}, 'I am a stream'))) - ) - await sleep(500) - writer.write( - encoder.encode( - renderToString(createElement('p', {}, 'I am another stream')) - ) + ev.waitUntil( + (async () => { + writer.write( + encoder.encode( + renderToString(createElement('h1', {}, 'I am a stream')) + ) + ) + await sleep(500) + writer.write( + encoder.encode( + renderToString(createElement('p', {}, 'I am another stream')) + ) + ) + writer.close() + })() ) - writer.close() - return + + return new Response(readable) } - event.respondWith(next) + return next } function sleep(time) { diff --git a/test/integration/middleware-core/pages/rewrites/_middleware.js b/test/integration/middleware-core/pages/rewrites/_middleware.js index 4c1b04285f82f31..291b2be08af2790 100644 --- a/test/integration/middleware-core/pages/rewrites/_middleware.js +++ b/test/integration/middleware-core/pages/rewrites/_middleware.js @@ -1,31 +1,31 @@ import { NextResponse } from 'next/server' -export function middleware(event) { - const url = event.request.nextUrl +export async function middleware(request) { + const url = request.nextUrl if (url.pathname === '/rewrites/rewrite-to-ab-test') { - let bucket = event.request.cookies.bucket + let bucket = request.cookies.bucket if (!bucket) { bucket = Math.random() >= 0.5 ? 'a' : 'b' const response = NextResponse.rewrite(`/rewrites/${bucket}`) response.cookie('bucket', bucket) - return event.respondWith(response) + return response } - return event.respondWith(NextResponse.rewrite(`/rewrites/${bucket}`)) + return NextResponse.rewrite(`/rewrites/${bucket}`) } if (url.pathname === '/rewrites/rewrite-me-to-about') { - return event.respondWith(NextResponse.rewrite('/rewrites/about')) + return NextResponse.rewrite('/rewrites/about') } if (url.pathname === '/rewrites/rewrite-me-to-vercel') { - return event.respondWith(NextResponse.rewrite('https://vercel.com')) + return NextResponse.rewrite('https://vercel.com') } if (url.pathname === '/rewrites/rewrite-me-without-hard-navigation') { url.pathname = '/rewrites/about' url.searchParams.set('middleware', 'foo') - event.respondWith(NextResponse.rewrite(url)) + return NextResponse.rewrite(url) } } diff --git a/test/integration/middleware-core/test/index.test.js b/test/integration/middleware-core/test/index.test.js index 5c4c8fbccd13514..1d651d5502ec095 100644 --- a/test/integration/middleware-core/test/index.test.js +++ b/test/integration/middleware-core/test/index.test.js @@ -316,15 +316,6 @@ function responseTests(locale = '') { ) expect(res.headers.get('x-first-header')).toBe('valid') }) - - it(`${locale} should respond with 2 nested headers`, async () => { - const res = await fetchViaHTTP( - context.appPort, - `${locale}/responses/header?nested-header=true` - ) - expect(res.headers.get('x-first-header')).toBe('valid') - expect(res.headers.get('x-nested-header')).toBe('valid') - }) } function interfaceTests(locale = '') { From fd15cdff62c8627c59982c124bfbb661ce5fee9d Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Oct 2021 17:40:53 -0400 Subject: [PATCH 5/7] Apply suggestions --- packages/next/server/web/sandbox/sandbox.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index 1905bee01e5d0fc..b92d0a644ad2064 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -187,7 +187,7 @@ function getFetchHeaders(middleware: string, init: RequestInit) { const prevsub = headers.get(`x-middleware-subrequest`) || '' const value = prevsub.split(':').concat(middleware).join(':') headers.set(`x-middleware-subrequest`, value) - headers.set(`user-agent`, `Next.JS Middleware`) + headers.set(`user-agent`, `Next.js Middleware`) return headers } @@ -195,7 +195,7 @@ function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string { const initurl = isRequestLike(input) ? input.url : input if (initurl.startsWith('/')) { const host = headers.host?.toString() - const localhost = host === 'localhost' || host?.startsWith('localhost:') + const localhost = host === '127.0.0.1' || host === 'localhost' || host?.startsWith('localhost:') return `${localhost ? 'http' : 'https'}://${host}${initurl}` } return initurl From e92409e0a2169eb9b5b22715ff641d087fd29c79 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Oct 2021 17:55:05 -0400 Subject: [PATCH 6/7] Fix test --- .../middleware-core/pages/responses/_middleware.js | 11 +++++------ test/integration/middleware-core/test/index.test.js | 9 +++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/integration/middleware-core/pages/responses/_middleware.js b/test/integration/middleware-core/pages/responses/_middleware.js index a375375351e5246..246ab0a18d479af 100644 --- a/test/integration/middleware-core/pages/responses/_middleware.js +++ b/test/integration/middleware-core/pages/responses/_middleware.js @@ -10,15 +10,14 @@ export async function middleware(request, ev) { const encoder = new TextEncoder() const next = NextResponse.next() - // Sends a header - if (url.pathname === '/responses/header') { - next.headers.set('x-first-header', 'valid') - return next - } - // Header based on query param if (url.searchParams.get('nested-header') === 'true') { next.headers.set('x-nested-header', 'valid') + } + + // Sends a header + if (url.pathname === '/responses/header') { + next.headers.set('x-first-header', 'valid') return next } diff --git a/test/integration/middleware-core/test/index.test.js b/test/integration/middleware-core/test/index.test.js index 1d651d5502ec095..121375f254f725e 100644 --- a/test/integration/middleware-core/test/index.test.js +++ b/test/integration/middleware-core/test/index.test.js @@ -309,6 +309,15 @@ function responseTests(locale = '') { expect($('.title').text()).toBe('Hello World') }) + it(`${locale} should respond with 2 nested headers`, async () => { + const res = await fetchViaHTTP( + context.appPort, + `${locale}/responses/header?nested-header=true` + ) + expect(res.headers.get('x-first-header')).toBe('valid') + expect(res.headers.get('x-nested-header')).toBe('valid') + }) + it(`${locale} should respond with a header`, async () => { const res = await fetchViaHTTP( context.appPort, From f165e396eed535d744ea04bd6121554d254d1e1f Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Oct 2021 18:20:35 -0400 Subject: [PATCH 7/7] lint --- packages/next/server/web/sandbox/sandbox.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts index b92d0a644ad2064..98eb9700bf8e0bf 100644 --- a/packages/next/server/web/sandbox/sandbox.ts +++ b/packages/next/server/web/sandbox/sandbox.ts @@ -195,7 +195,10 @@ function getFetchURL(input: RequestInfo, headers: NodeHeaders = {}): string { const initurl = isRequestLike(input) ? input.url : input if (initurl.startsWith('/')) { const host = headers.host?.toString() - const localhost = host === '127.0.0.1' || host === 'localhost' || host?.startsWith('localhost:') + const localhost = + host === '127.0.0.1' || + host === 'localhost' || + host?.startsWith('localhost:') return `${localhost ? 'http' : 'https'}://${host}${initurl}` } return initurl