Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Commit

Permalink
Add RequestContext (vercel#27303)
Browse files Browse the repository at this point in the history
How a page is rendered depends on whether or not we're streaming. For example, if we're just rendering to a `string` or we're generating a response for a crawler or other robot, we don't want React 18 to dynamically flush `<script>` tags to update Suspense boundaries as they resolve. Instead, we just want to wait for the full HTML to resolve and return a result similar to `renderToString`.

This is what `RequestContext` and the new/refactored `pipe` and `getStaticHTML` methods allow.  They add a `requireStaticHTML` option that gets passed down. A follow-up PR will make sure this is `true` when serving a robot, and also ensure React is invoked appropriately.
  • Loading branch information
devknoll authored Jul 20, 2021
1 parent a4045bc commit 79e9d5c
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 83 deletions.
200 changes: 117 additions & 83 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ export type ServerConstructor = {
customServer?: boolean
}

type RequestContext = {
req: IncomingMessage
res: ServerResponse
pathname: string
query: ParsedUrlQuery
renderOpts: RenderOptsPartial
}

export default class Server {
protected dir: string
protected quiet: boolean
Expand Down Expand Up @@ -1313,11 +1321,30 @@ export default class Server {
await this.render404(req, res, parsedUrl)
}

protected async sendResponse(
req: IncomingMessage,
res: ServerResponse,
{ type, body, revalidateOptions }: ResponsePayload
private async pipe(
fn: (ctx: RequestContext) => Promise<ResponsePayload | null>,
partialContext: {
req: IncomingMessage
res: ServerResponse
pathname: string
query: ParsedUrlQuery
}
): Promise<void> {
// TODO: Determine when dynamic HTML is allowed
const requireStaticHTML = true
const ctx = {
...partialContext,
renderOpts: {
...this.renderOpts,
requireStaticHTML,
},
} as const
const payload = await fn(ctx)
if (payload === null) {
return
}
const { req, res } = ctx
const { body, type, revalidateOptions } = payload
if (!isResSent(res)) {
const { generateEtags, poweredByHeader, dev } = this.renderOpts
if (dev) {
Expand All @@ -1338,6 +1365,25 @@ export default class Server {
}
}

private async getStaticHTML(
fn: (ctx: RequestContext) => Promise<ResponsePayload | null>,
partialContext: {
req: IncomingMessage
res: ServerResponse
pathname: string
query: ParsedUrlQuery
}
): Promise<string | null> {
const payload = await fn({
...partialContext,
renderOpts: {
...this.renderOpts,
requireStaticHTML: true,
},
})
return payload ? payload.body : null
}

public async render(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -1385,13 +1431,12 @@ export default class Server {
return this.render404(req, res, parsedUrl)
}

const response = await this.renderToResponse(req, res, pathname, query)
// Request was ended by the user
if (response === null) {
return
}

return this.sendResponse(req, res, response)
return this.pipe((ctx) => this.renderToResponse(ctx), {
req,
res,
pathname,
query,
})
}

protected async findPageComponents(
Expand Down Expand Up @@ -1479,11 +1524,8 @@ export default class Server {
}

private async renderToResponseWithComponents(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
{ components, query }: FindComponentsResult,
opts: RenderOptsPartial
{ req, res, pathname, renderOpts: opts }: RequestContext,
{ components, query }: FindComponentsResult
): Promise<ResponsePayload | null> {
const is404Page = pathname === '/404'
const is500Page = pathname === '/500'
Expand Down Expand Up @@ -1863,25 +1905,17 @@ export default class Server {
}

private async renderToResponse(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: ParsedUrlQuery = {}
ctx: RequestContext
): Promise<ResponsePayload | null> {
const { res, query, pathname } = ctx
const bubbleNoFallback = !!query._nextBubbleNoFallback
delete query._nextBubbleNoFallback

try {
const result = await this.findPageComponents(pathname, query)
if (result) {
try {
return await this.renderToResponseWithComponents(
req,
res,
pathname,
result,
{ ...this.renderOpts }
)
return await this.renderToResponseWithComponents(ctx, result)
} catch (err) {
const isNoFallbackError = err instanceof NoFallbackError

Expand All @@ -1906,11 +1940,15 @@ export default class Server {
if (dynamicRouteResult) {
try {
return await this.renderToResponseWithComponents(
req,
res,
dynamicRoute.page,
dynamicRouteResult,
{ ...this.renderOpts, params }
{
...ctx,
pathname: dynamicRoute.page,
renderOpts: {
...ctx.renderOpts,
params,
},
},
dynamicRouteResult
)
} catch (err) {
const isNoFallbackError = err instanceof NoFallbackError
Expand All @@ -1931,17 +1969,14 @@ export default class Server {
}
if (err instanceof DecodeError) {
res.statusCode = 400
return await this.renderErrorToResponse(err, req, res, pathname, query)
return await this.renderErrorToResponse(ctx, err)
}

res.statusCode = 500
const isWrappedError = err instanceof WrappedBuildError
const response = await this.renderErrorToResponse(
isWrappedError ? err.innerError : err,
req,
res,
pathname,
query
ctx,
isWrappedError ? err.innerError : err
)

if (!isWrappedError) {
Expand All @@ -1953,7 +1988,7 @@ export default class Server {
return response
}
res.statusCode = 404
return this.renderErrorToResponse(null, req, res, pathname, query)
return this.renderErrorToResponse(ctx, null)
}

public async renderToHTML(
Expand All @@ -1962,8 +1997,12 @@ export default class Server {
pathname: string,
query: ParsedUrlQuery = {}
): Promise<string | null> {
const response = await this.renderToResponse(req, res, pathname, query)
return response ? response.body : null
return this.getStaticHTML((ctx) => this.renderToResponse(ctx), {
req,
res,
pathname,
query,
})
}

public async renderError(
Expand All @@ -1980,21 +2019,17 @@ export default class Server {
'no-cache, no-store, max-age=0, must-revalidate'
)
}
const response = await this.renderErrorToResponse(
err,
req,
res,
pathname,
query
)

if (this.minimalMode && res.statusCode === 500) {
throw err
}
if (response === null) {
return
}
return this.sendResponse(req, res, response)
return this.pipe(
async (ctx) => {
const response = await this.renderErrorToResponse(ctx, err)
if (this.minimalMode && res.statusCode === 500) {
throw err
}
return response
},
{ req, res, pathname, query }
)
}

private customErrorNo404Warn = execOnce(() => {
Expand All @@ -2007,12 +2042,10 @@ export default class Server {
})

private async renderErrorToResponse(
_err: Error | null,
req: IncomingMessage,
res: ServerResponse,
_pathname: string,
query: ParsedUrlQuery = {}
ctx: RequestContext,
_err: Error | null
): Promise<ResponsePayload | null> {
const { res, query } = ctx
let err = _err
if (this.renderOpts.dev && !err && res.statusCode === 500) {
err = new Error(
Expand Down Expand Up @@ -2053,14 +2086,15 @@ export default class Server {

try {
return await this.renderToResponseWithComponents(
req,
res,
statusPage,
result!,
{
...this.renderOpts,
err,
}
...ctx,
pathname: statusPage,
renderOpts: {
...ctx.renderOpts,
err,
},
},
result!
)
} catch (maybeFallbackError) {
if (maybeFallbackError instanceof NoFallbackError) {
Expand All @@ -2078,20 +2112,21 @@ export default class Server {

if (fallbackComponents) {
return this.renderToResponseWithComponents(
req,
res,
'/_error',
{
query,
components: fallbackComponents,
...ctx,
pathname: '/_error',
renderOpts: {
...ctx.renderOpts,
// We render `renderToHtmlError` here because `err` is
// already captured in the stacktrace.
err: isWrappedError
? renderToHtmlError.innerError
: renderToHtmlError,
},
},
{
...this.renderOpts,
// We render `renderToHtmlError` here because `err` is
// already captured in the stacktrace.
err: isWrappedError
? renderToHtmlError.innerError
: renderToHtmlError,
query,
components: fallbackComponents,
}
)
}
Expand All @@ -2109,14 +2144,12 @@ export default class Server {
pathname: string,
query: ParsedUrlQuery = {}
): Promise<string | null> {
const response = await this.renderErrorToResponse(
err,
return this.getStaticHTML((ctx) => this.renderErrorToResponse(ctx, err), {
req,
res,
pathname,
query
)
return response ? response.body : null
query,
})
}

protected async getFallbackErrorComponents(): Promise<LoadComponentsReturnType | null> {
Expand All @@ -2139,6 +2172,7 @@ export default class Server {
query.__nextDefaultLocale =
query.__nextDefaultLocale || i18n.defaultLocale
}

res.statusCode = 404
return this.renderError(null, req, res, pathname!, query, setHeaders)
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export type RenderOptsPartial = {
defaultLocale?: string
domainLocales?: DomainLocales
disableOptimizedLoading?: boolean
requireStaticHTML?: boolean
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
Expand Down

0 comments on commit 79e9d5c

Please sign in to comment.