Skip to content

Commit

Permalink
Unify Request types (#47884)
Browse files Browse the repository at this point in the history
This serves to start the transition of replacing the following:

- Replace `BaseNextRequest`, `WebNextRequest`, and `NodeNextRequest`
with `NextRequest`
- Replace `BaseNextResponse`, `WebNextResponse`, and `NodeNextResponse`
with `Response`

This will currently only apply to app routes, enabling the following:

```ts
////////////////////////////////////////////////////////////////////////////////
// Use `Request` and `Response`
////////////////////////////////////////////////////////////////////////////////

import { NextRequest, NextResponse } from 'next/server'

export function GET(request: Request): Response {
  return new Response(
    JSON.stringify({
      hello: request.headers.get('user-agent'),
    }),
    { headers: { 'content-type': 'application/json' } }
  )
}

////////////////////////////////////////////////////////////////////////////////
// Use `NextRequest` and `NextResponse`
////////////////////////////////////////////////////////////////////////////////

import { NextRequest, NextResponse } from 'next/server'

export function GET(request: NextRequest): NextResponse {
  return NextResponse.json({ hello: request.headers.get('user-agent') })
}

////////////////////////////////////////////////////////////////////////////////
// Use `NextRequest` and `Response`
////////////////////////////////////////////////////////////////////////////////

import { NextRequest, NextResponse } from 'next/server'

// `NextRequest` extends `Request`.
export function GET(request: NextRequest): Response {
  return new Response(
    JSON.stringify({ hello: request.headers.get('user-agent') }),
    { headers: { 'content-type': 'application/json' } }
  )
}

////////////////////////////////////////////////////////////////////////////////
// Use `NextRequest`, `NextResponse`, and `Response`
////////////////////////////////////////////////////////////////////////////////

import { NextRequest, NextResponse } from 'next/server'

export function GET(request: NextRequest): Response {
  // `NextResponse` extends `Response`.
  return NextResponse.json({ hello: request.headers.get('user-agent') })
}

////////////////////////////////////////////////////////////////////////////////
// Use `Request` and `NextResponse`
////////////////////////////////////////////////////////////////////////////////

import { NextRequest, NextResponse } from 'next/server'

export function GET(request: Request): NextResponse {
  return NextResponse.json({ hello: request.headers.get('user-agent') })
}

```

fix NEXT-713
  • Loading branch information
wyattjoh committed Apr 5, 2023
1 parent 2c9b484 commit 1fbbba6
Show file tree
Hide file tree
Showing 36 changed files with 1,435 additions and 378 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// PAGE and PATHNAME is set from rust code
declare const PAGE: string, PATHNAME: string;

import { EdgeModuleWrapper } from "next/dist/build/webpack/loaders/next-edge-app-route-loader/edge-module-wrapper";
import { EdgeRouteModuleWrapper } from "next/dist/server/web/edge-route-module-wrapper";

import RouteModule from "ROUTE_MODULE";
import * as userland from "ENTRY";
Expand All @@ -17,6 +17,6 @@ const routeModule = new RouteModule({
// @ts-expect-error - exposed for edge support
globalThis._ENTRIES = {
middleware_edge: {
default: EdgeModuleWrapper.wrap(routeModule, { page: `/${PAGE}` }),
default: EdgeRouteModuleWrapper.wrap(routeModule, { page: `/${PAGE}` }),
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
)}?__edge_ssr_entry__`

return `
import { EdgeModuleWrapper } from 'next/dist/esm/build/webpack/loaders/next-edge-app-route-loader/edge-module-wrapper'
import { EdgeRouteModuleWrapper } from 'next/dist/esm/server/web/edge-route-module-wrapper'
import * as module from ${JSON.stringify(modulePath)}
export const ComponentMod = module
export default EdgeModuleWrapper.wrap(module.routeModule)`
export default EdgeRouteModuleWrapper.wrap(module.routeModule)`
}

export default EdgeAppRouteLoader
6 changes: 4 additions & 2 deletions packages/next/src/client/components/headers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { RequestCookiesAdapter } from '../../server/web/spec-extension/adapters/request-cookies'
import { HeadersAdapter } from '../../server/web/spec-extension/adapters/headers'
import { RequestCookies } from '../../server/web/spec-extension/cookies'
import { requestAsyncStorage } from './request-async-storage'
import { staticGenerationBailout } from './static-generation-bailout'

export function headers() {
if (staticGenerationBailout('headers')) {
return new Headers({})
return HeadersAdapter.seal(new Headers({}))
}

const requestStore = requestAsyncStorage.getStore()
Expand All @@ -30,7 +32,7 @@ export function previewData() {

export function cookies() {
if (staticGenerationBailout('cookies')) {
return new RequestCookies(new Headers({}))
return RequestCookiesAdapter.seal(new RequestCookies(new Headers({})))
}

const requestStore = requestAsyncStorage.getStore()
Expand Down
7 changes: 4 additions & 3 deletions packages/next/src/client/components/request-async-storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { AsyncLocalStorage } from 'async_hooks'
import { PreviewData } from '../../../types'
import type { ReadonlyHeaders } from '../../server/app-render/readonly-headers'
import type { ReadonlyRequestCookies } from '../../server/app-render/readonly-request-cookies'
import type { PreviewData } from '../../../types'
import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/headers'
import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies'

import { createAsyncLocalStorage } from './async-local-storage'

export interface RequestStore {
Expand Down
15 changes: 7 additions & 8 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ import { IncrementalCache } from '../server/lib/incremental-cache'
import { isNotFoundError } from '../client/components/not-found'
import { isRedirectError } from '../client/components/redirect'
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/lazy-dynamic/no-ssr-error'
import { mockRequest } from '../server/lib/mock-request'
import { createRequestResponseMocks } from '../server/lib/mock-request'
import { NodeNextRequest } from '../server/base-http/node'
import { isAppRouteRoute } from '../lib/is-app-route-route'
import { toNodeHeaders } from '../server/web/utils'
import { RouteModuleLoader } from '../server/future/helpers/module-loader/route-module-loader'
import { NextRequestAdapter } from '../server/web/spec-extension/adapters/next-request'

loadRequireHook()

Expand Down Expand Up @@ -236,7 +237,7 @@ export default async function exportPage({
}
}

const { req, res } = mockRequest(updatedPath, {}, 'GET')
const { req, res } = createRequestResponseMocks({ url: updatedPath })

for (const statusCode of [404, 500]) {
if (
Expand Down Expand Up @@ -385,12 +386,10 @@ export default async function exportPage({
isRedirectError(err)

if (isRouteHandler) {
const request = new NodeNextRequest(req)

addRequestMeta(
request.originalRequest,
'__NEXT_INIT_URL',
`http://localhost:3000${req.url}`
// Ensure that the url for the page is absolute.
req.url = `http://localhost:3000${req.url}`
const request = NextRequestAdapter.fromNodeNextRequest(
new NodeNextRequest(req)
)

// Create the context for the handler. This contains the params from
Expand Down
31 changes: 18 additions & 13 deletions packages/next/src/server/api-utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { IncomingMessage } from 'http'
import type { BaseNextRequest } from '../base-http'

import type { NextApiRequest, NextApiResponse } from '../../shared/lib/utils'
import type { CookieSerializeOptions } from 'next/dist/compiled/cookie'
import type { NextApiRequest, NextApiResponse } from '../../shared/lib/utils'

import { HeadersAdapter } from '../web/spec-extension/adapters/headers'

export type NextApiRequestCookies = Partial<{ [key: string]: string }>
export type NextApiRequestQuery = Partial<{ [key: string]: string | string[] }>
Expand All @@ -18,17 +19,17 @@ export type __ApiPreviewProps = {
* @param req request object
*/
export function getCookieParser(headers: {
[key: string]: undefined | string | string[]
[key: string]: string | string[] | null | undefined
}): () => NextApiRequestCookies {
return function parseCookie(): NextApiRequestCookies {
const header: undefined | string | string[] = headers.cookie
const { cookie } = headers

if (!header) {
if (!cookie) {
return {}
}

const { parse: parseCookieFn } = require('next/dist/compiled/cookie')
return parseCookieFn(Array.isArray(header) ? header.join(';') : header)
return parseCookieFn(Array.isArray(cookie) ? cookie.join('; ') : cookie)
}
}

Expand Down Expand Up @@ -76,18 +77,22 @@ export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER =
'x-prerender-revalidate-if-generated'

export function checkIsOnDemandRevalidate(
req: IncomingMessage | BaseNextRequest,
req: Request | IncomingMessage | BaseNextRequest,
previewProps: __ApiPreviewProps
): {
isOnDemandRevalidate: boolean
revalidateOnlyGenerated: boolean
} {
return {
isOnDemandRevalidate:
req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId,
revalidateOnlyGenerated:
!!req.headers[PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER],
}
const headers = HeadersAdapter.from(req.headers)

const previewModeId = headers.get(PRERENDER_REVALIDATE_HEADER)
const isOnDemandRevalidate = previewModeId === previewProps.previewModeId

const revalidateOnlyGenerated = headers.has(
PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER
)

return { isOnDemandRevalidate, revalidateOnlyGenerated }
}

export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
Expand Down
50 changes: 22 additions & 28 deletions packages/next/src/server/api-utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ import {
clearPreviewData,
sendError,
ApiError,
NextApiRequestCookies,
PRERENDER_REVALIDATE_HEADER,
COOKIE_NAME_PRERENDER_BYPASS,
COOKIE_NAME_PRERENDER_DATA,
SYMBOL_PREVIEW_DATA,
RESPONSE_LIMIT_DEFAULT,
} from './index'
import { mockRequest } from '../lib/mock-request'
import { createRequestResponseMocks } from '../lib/mock-request'
import { getTracer } from '../lib/trace/tracer'
import { NodeSpan } from '../lib/trace/constants'
import { RequestCookies } from '../web/spec-extension/cookies'
import { HeadersAdapter } from '../web/spec-extension/adapters/headers'

export function tryGetPreviewData(
req: IncomingMessage | BaseNextRequest,
req: IncomingMessage | BaseNextRequest | Request,
res: ServerResponse | BaseNextResponse,
options: __ApiPreviewProps
): PreviewData {
Expand All @@ -49,41 +50,34 @@ export function tryGetPreviewData(
}

// Read cached preview data if present
// TODO: use request metadata instead of a symbol
if (SYMBOL_PREVIEW_DATA in req) {
return (req as any)[SYMBOL_PREVIEW_DATA] as any
}

const getCookies = getCookieParser(req.headers)
let cookies: NextApiRequestCookies
try {
cookies = getCookies()
} catch {
// TODO: warn
return false
}
const headers = HeadersAdapter.from(req.headers)
const cookies = new RequestCookies(headers)

const hasBypass = COOKIE_NAME_PRERENDER_BYPASS in cookies
const hasData = COOKIE_NAME_PRERENDER_DATA in cookies
const previewModeId = cookies.get(COOKIE_NAME_PRERENDER_BYPASS)?.value
const tokenPreviewData = cookies.get(COOKIE_NAME_PRERENDER_DATA)?.value

// Case: neither cookie is set.
if (!(hasBypass || hasData)) {
if (!previewModeId && !tokenPreviewData) {
return false
}

// Case: one cookie is set, but not the other.
if (hasBypass !== hasData) {
if (!previewModeId || !tokenPreviewData) {
clearPreviewData(res as NextApiResponse)
return false
}

// Case: preview session is for an old build.
if (cookies[COOKIE_NAME_PRERENDER_BYPASS] !== options.previewModeId) {
if (previewModeId !== options.previewModeId) {
clearPreviewData(res as NextApiResponse)
return false
}

const tokenPreviewData = cookies[COOKIE_NAME_PRERENDER_DATA] as string

let encryptedPreviewData: {
data: string
}
Expand Down Expand Up @@ -409,19 +403,19 @@ async function revalidate(
throw new Error(`Invalid response ${res.status}`)
}
} else if (context.revalidate) {
const {
req: mockReq,
res: mockRes,
streamPromise,
} = mockRequest(urlPath, revalidateHeaders, 'GET')
await context.revalidate(mockReq, mockRes)
await streamPromise
const mocked = createRequestResponseMocks({
url: urlPath,
headers: revalidateHeaders,
})

await context.revalidate(mocked.req, mocked.res)
await mocked.res.hasStreamed

if (
mockRes.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
!(mockRes.statusCode === 404 && opts.unstable_onlyGenerated)
mocked.res.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
!(mocked.res.statusCode === 404 && opts.unstable_onlyGenerated)
) {
throw new Error(`Invalid response ${mockRes.statusCode}`)
throw new Error(`Invalid response ${mocked.res.statusCode}`)
}
} else {
throw new Error(
Expand Down
44 changes: 0 additions & 44 deletions packages/next/src/server/app-render/readonly-headers.ts

This file was deleted.

44 changes: 0 additions & 44 deletions packages/next/src/server/app-render/readonly-request-cookies.ts

This file was deleted.

0 comments on commit 1fbbba6

Please sign in to comment.