Skip to content

Commit

Permalink
Merge branch 'canary' into shu/1054
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed Mar 17, 2022
2 parents 46f6907 + d3a53a6 commit c610570
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 101 deletions.
94 changes: 50 additions & 44 deletions packages/next/server/api-utils/node.ts
Expand Up @@ -31,6 +31,7 @@ import {
SYMBOL_PREVIEW_DATA,
RESPONSE_LIMIT_DEFAULT,
} from './index'
import { mockRequest } from '../lib/mock-request'

export function tryGetPreviewData(
req: IncomingMessage | BaseNextRequest,
Expand Down Expand Up @@ -149,16 +150,17 @@ export async function parseBody(
}
}

type ApiContext = __ApiPreviewProps & {
trustHostHeader?: boolean
revalidate?: (_req: IncomingMessage, _res: ServerResponse) => Promise<any>
}

export async function apiResolver(
req: IncomingMessage,
res: ServerResponse,
query: any,
resolverModule: any,
apiContext: __ApiPreviewProps & {
trustHostHeader?: boolean
hostname?: string
port?: number
},
apiContext: ApiContext,
propagateError: boolean,
dev?: boolean,
page?: string
Expand Down Expand Up @@ -277,55 +279,59 @@ export async function apiResolver(

async function unstable_revalidate(
urlPath: string,
req: IncomingMessage | BaseNextRequest,
context: {
hostname?: string
port?: number
previewModeId: string
trustHostHeader?: boolean
}
req: IncomingMessage,
context: ApiContext
) {
if (!context.trustHostHeader && (!context.hostname || !context.port)) {
throw new Error(
`"hostname" and "port" must be provided when starting next to use "unstable_revalidate". See more here https://nextjs.org/docs/advanced-features/custom-server`
)
}

if (typeof urlPath !== 'string' || !urlPath.startsWith('/')) {
throw new Error(
`Invalid urlPath provided to revalidate(), must be a path e.g. /blog/post-1, received ${urlPath}`
)
}

const baseUrl = context.trustHostHeader
? `https://${req.headers.host}`
: `http://${context.hostname}:${context.port}`

const extraHeaders: Record<string, string | undefined> = {}

if (context.trustHostHeader) {
extraHeaders.cookie = req.headers.cookie
}

try {
const res = await fetch(`${baseUrl}${urlPath}`, {
headers: {
[PRERENDER_REVALIDATE_HEADER]: context.previewModeId,
...extraHeaders,
},
})

// we use the cache header to determine successful revalidate as
// a non-200 status code can be returned from a successful revalidate
// e.g. notFound: true returns 404 status code but is successful
const cacheHeader =
res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache')
if (context.trustHostHeader) {
const res = await fetch(`https://${req.headers.host}${urlPath}`, {
headers: {
[PRERENDER_REVALIDATE_HEADER]: context.previewModeId,
cookie: req.headers.cookie || '',
},
})
// we use the cache header to determine successful revalidate as
// a non-200 status code can be returned from a successful revalidate
// e.g. notFound: true returns 404 status code but is successful
const cacheHeader =
res.headers.get('x-vercel-cache') || res.headers.get('x-nextjs-cache')

if (cacheHeader?.toUpperCase() !== 'REVALIDATED') {
throw new Error(`Invalid response ${res.status}`)
}
} else if (context.revalidate) {
const {
req: mockReq,
res: mockRes,
streamPromise,
} = mockRequest(
urlPath,
{
[PRERENDER_REVALIDATE_HEADER]: context.previewModeId,
},
'GET'
)
await context.revalidate(mockReq, mockRes)
await streamPromise

if (cacheHeader?.toUpperCase() !== 'REVALIDATED') {
throw new Error(`Invalid response ${res.status}`)
if (mockRes.getHeader('x-nextjs-cache') !== 'REVALIDATED') {
throw new Error(`Invalid response ${mockRes.status}`)
}
} else {
throw new Error(
`Invariant: required internal revalidate method not passed to api-utils`
)
}
} catch (err) {
throw new Error(`Failed to revalidate ${urlPath}`)
} catch (err: unknown) {
throw new Error(
`Failed to revalidate ${urlPath}: ${isError(err) ? err.message : err}`
)
}
}

Expand Down
62 changes: 7 additions & 55 deletions packages/next/server/image-optimizer.ts
Expand Up @@ -8,7 +8,6 @@ import { IncomingMessage, ServerResponse } from 'http'
import isAnimated from 'next/dist/compiled/is-animated'
import contentDisposition from 'next/dist/compiled/content-disposition'
import { join } from 'path'
import Stream from 'stream'
import nodeUrl, { UrlWithParsedQuery } from 'url'
import { NextConfigComplete } from './config-shared'
import { processBuffer, decodeBuffer, Operation } from './lib/squoosh/main'
Expand All @@ -17,6 +16,7 @@ import { getContentType, getExtension } from './serve-static'
import chalk from 'next/dist/compiled/chalk'
import { NextUrlWithParsedQuery } from './request-meta'
import { IncrementalCacheEntry, IncrementalCacheValue } from './response-cache'
import { mockRequest } from './lib/mock-request'

type XCacheHeader = 'MISS' | 'HIT' | 'STALE'

Expand Down Expand Up @@ -307,60 +307,12 @@ export async function imageOptimizer(
maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control'))
} else {
try {
const resBuffers: Buffer[] = []
const mockRes: any = new Stream.Writable()

const isStreamFinished = new Promise(function (resolve, reject) {
mockRes.on('finish', () => resolve(true))
mockRes.on('end', () => resolve(true))
mockRes.on('error', (err: any) => reject(err))
})

mockRes.write = (chunk: Buffer | string) => {
resBuffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
mockRes._write = (
chunk: Buffer | string,
_encoding: string,
callback: () => void
) => {
mockRes.write(chunk)
// According to Node.js documentation, the callback MUST be invoked to signal that
// the write completed successfully. If this callback is not invoked, the 'finish' event
// will not be emitted.
// https://nodejs.org/docs/latest-v16.x/api/stream.html#writable_writechunk-encoding-callback
callback()
}

const mockHeaders: Record<string, string | string[]> = {}

mockRes.writeHead = (_status: any, _headers: any) =>
Object.assign(mockHeaders, _headers)
mockRes.getHeader = (name: string) => mockHeaders[name.toLowerCase()]
mockRes.getHeaders = () => mockHeaders
mockRes.getHeaderNames = () => Object.keys(mockHeaders)
mockRes.setHeader = (name: string, value: string | string[]) =>
(mockHeaders[name.toLowerCase()] = value)
mockRes.removeHeader = (name: string) => {
delete mockHeaders[name.toLowerCase()]
}
mockRes._implicitHeader = () => {}
mockRes.connection = _res.connection
mockRes.finished = false
mockRes.statusCode = 200

const mockReq: any = new Stream.Readable()

mockReq._read = () => {
mockReq.emit('end')
mockReq.emit('close')
return Buffer.from('')
}

mockReq.headers = _req.headers
mockReq.method = _req.method
mockReq.url = href
mockReq.connection = _req.connection
const {
resBuffers,
req: mockReq,
res: mockRes,
streamPromise: isStreamFinished,
} = mockRequest(href, _req.headers, _req.method || 'GET', _req.connection)

await handleRequest(mockReq, mockRes, nodeUrl.parse(href, true))
await isStreamFinished
Expand Down
70 changes: 70 additions & 0 deletions packages/next/server/lib/mock-request.ts
@@ -0,0 +1,70 @@
import Stream from 'stream'

export function mockRequest(
requestUrl: string,
requestHeaders: Record<string, string | string[] | undefined>,
requestMethod: string,
requestConnection?: any
) {
const resBuffers: Buffer[] = []
const mockRes: any = new Stream.Writable()

const isStreamFinished = new Promise(function (resolve, reject) {
mockRes.on('finish', () => resolve(true))
mockRes.on('end', () => resolve(true))
mockRes.on('error', (err: any) => reject(err))
})

mockRes.write = (chunk: Buffer | string) => {
resBuffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
mockRes._write = (
chunk: Buffer | string,
_encoding: string,
callback: () => void
) => {
mockRes.write(chunk)
// According to Node.js documentation, the callback MUST be invoked to signal that
// the write completed successfully. If this callback is not invoked, the 'finish' event
// will not be emitted.
// https://nodejs.org/docs/latest-v16.x/api/stream.html#writable_writechunk-encoding-callback
callback()
}

const mockHeaders: Record<string, string | string[]> = {}

mockRes.writeHead = (_status: any, _headers: any) =>
Object.assign(mockHeaders, _headers)
mockRes.getHeader = (name: string) => mockHeaders[name.toLowerCase()]
mockRes.getHeaders = () => mockHeaders
mockRes.getHeaderNames = () => Object.keys(mockHeaders)
mockRes.setHeader = (name: string, value: string | string[]) =>
(mockHeaders[name.toLowerCase()] = value)
mockRes.removeHeader = (name: string) => {
delete mockHeaders[name.toLowerCase()]
}
mockRes._implicitHeader = () => {}
mockRes.connection = requestConnection
mockRes.finished = false
mockRes.statusCode = 200

const mockReq: any = new Stream.Readable()

mockReq._read = () => {
mockReq.emit('end')
mockReq.emit('close')
return Buffer.from('')
}

mockReq.headers = requestHeaders
mockReq.method = requestMethod
mockReq.url = requestUrl
mockReq.connection = requestConnection

return {
resBuffers,
req: mockReq,
res: mockRes,
streamPromise: isStreamFinished,
}
}
7 changes: 5 additions & 2 deletions packages/next/server/next-server.ts
Expand Up @@ -554,8 +554,11 @@ export default class NextNodeServer extends BaseServer {
pageModule,
{
...this.renderOpts.previewProps,
port: this.port,
hostname: this.hostname,
revalidate: (newReq: IncomingMessage, newRes: ServerResponse) =>
this.getRequestHandler()(
new NodeNextRequest(newReq),
new NodeNextResponse(newRes)
),
// internal config so is not typed
trustHostHeader: (this.nextConfig.experimental as any).trustHostHeader,
},
Expand Down

0 comments on commit c610570

Please sign in to comment.