b75b2f02c9
This PR improves error handling in the SSR middleware. Previously the response was sent out synchronously, and and errors were silently swallowed. There was no `.catch` for `renderToHTML`. This changes the middleware to be asynchronous, which waits until the initial Document to be rendered correctly and then starts the streaming. With this change we can also send correct status code when there're immediate errors before Fizz. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint`
202 lines
6.1 KiB
TypeScript
202 lines
6.1 KiB
TypeScript
import { stringifyRequest } from '../../stringify-request'
|
|
|
|
export default async function middlewareRSCLoader(this: any) {
|
|
const {
|
|
absolutePagePath,
|
|
absoluteAppPath,
|
|
absoluteDocumentPath,
|
|
basePath,
|
|
isServerComponent: isServerComponentQuery,
|
|
assetPrefix,
|
|
buildId,
|
|
} = this.getOptions()
|
|
|
|
const isServerComponent = isServerComponentQuery === 'true'
|
|
const stringifiedAbsolutePagePath = stringifyRequest(this, absolutePagePath)
|
|
const stringifiedAbsoluteAppPath = stringifyRequest(this, absoluteAppPath)
|
|
const stringifiedAbsoluteDocumentPath = stringifyRequest(
|
|
this,
|
|
absoluteDocumentPath
|
|
)
|
|
|
|
let appDefinition = `const App = require(${stringifiedAbsoluteAppPath}).default`
|
|
let documentDefinition = `const Document = require(${stringifiedAbsoluteDocumentPath}).default`
|
|
|
|
const transformed = `
|
|
import { adapter } from 'next/dist/server/web/adapter'
|
|
|
|
import { RouterContext } from 'next/dist/shared/lib/router-context'
|
|
import { renderToHTML } from 'next/dist/server/web/render'
|
|
|
|
import React, { createElement } from 'react'
|
|
|
|
${
|
|
isServerComponent
|
|
? `
|
|
import { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/writer.browser.server'
|
|
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'`
|
|
: ''
|
|
}
|
|
|
|
${appDefinition}
|
|
${documentDefinition}
|
|
|
|
const {
|
|
default: Page,
|
|
config,
|
|
getStaticProps,
|
|
getServerSideProps,
|
|
getStaticPaths
|
|
} = require(${stringifiedAbsolutePagePath})
|
|
|
|
const buildManifest = self.__BUILD_MANIFEST
|
|
const reactLoadableManifest = self.__REACT_LOADABLE_MANIFEST
|
|
const rscManifest = self.__RSC_MANIFEST
|
|
|
|
if (typeof Page !== 'function') {
|
|
throw new Error('Your page must export a \`default\` component')
|
|
}
|
|
|
|
function wrapReadable(readable) {
|
|
const encoder = new TextEncoder()
|
|
const transformStream = new TransformStream()
|
|
const writer = transformStream.writable.getWriter()
|
|
const reader = readable.getReader()
|
|
const process = () => {
|
|
reader.read().then(({ done, value }) => {
|
|
if (!done) {
|
|
writer.write(typeof value === 'string' ? encoder.encode(value) : value)
|
|
process()
|
|
} else {
|
|
writer.close()
|
|
}
|
|
})
|
|
}
|
|
process()
|
|
return transformStream.readable
|
|
}
|
|
|
|
${
|
|
isServerComponent
|
|
? `
|
|
const renderFlight = props => renderToReadableStream(createElement(Page, props), rscManifest)
|
|
|
|
let responseCache
|
|
const FlightWrapper = props => {
|
|
let response = responseCache
|
|
if (!response) {
|
|
responseCache = response = createFromReadableStream(renderFlight(props))
|
|
}
|
|
return response.readRoot()
|
|
}
|
|
const Component = props => {
|
|
return createElement(
|
|
React.Suspense,
|
|
{ fallback: null },
|
|
createElement(FlightWrapper, props)
|
|
)
|
|
}`
|
|
: `
|
|
const Component = Page`
|
|
}
|
|
|
|
async function render(request) {
|
|
const url = request.nextUrl
|
|
const query = Object.fromEntries(url.searchParams)
|
|
|
|
// Preflight request
|
|
if (request.method === 'HEAD') {
|
|
return new Response('OK.', {
|
|
headers: { 'x-middleware-ssr': '1' }
|
|
})
|
|
}
|
|
|
|
${
|
|
isServerComponent
|
|
? `
|
|
// Flight data request
|
|
const isFlightDataRequest = query.__flight__ !== undefined
|
|
if (isFlightDataRequest) {
|
|
delete query.__flight__
|
|
return new Response(
|
|
wrapReadable(
|
|
renderFlight({
|
|
router: {
|
|
route: url.pathname,
|
|
asPath: url.pathname,
|
|
pathname: url.pathname,
|
|
query,
|
|
}
|
|
})
|
|
)
|
|
)
|
|
}`
|
|
: ''
|
|
}
|
|
|
|
const renderOpts = {
|
|
Component,
|
|
pageConfig: config || {},
|
|
// Locales are not supported yet.
|
|
// locales: i18n?.locales,
|
|
// locale: detectedLocale,
|
|
// defaultLocale,
|
|
// domainLocales: i18n?.domains,
|
|
dev: process.env.NODE_ENV !== 'production',
|
|
App,
|
|
Document,
|
|
buildManifest,
|
|
getStaticProps,
|
|
getServerSideProps,
|
|
getStaticPaths,
|
|
reactLoadableManifest,
|
|
buildId: ${JSON.stringify(buildId)},
|
|
assetPrefix: ${JSON.stringify(assetPrefix || '')},
|
|
env: process.env,
|
|
basePath: ${JSON.stringify(basePath || '')},
|
|
supportsDynamicHTML: true,
|
|
concurrentFeatures: true,
|
|
renderServerComponent: ${isServerComponent ? 'true' : 'false'},
|
|
}
|
|
|
|
const transformStream = new TransformStream()
|
|
const writer = transformStream.writable.getWriter()
|
|
const encoder = new TextEncoder()
|
|
|
|
try {
|
|
const result = await renderToHTML(
|
|
{ url: url.pathname },
|
|
{},
|
|
url.pathname,
|
|
query,
|
|
renderOpts
|
|
)
|
|
result.pipe({
|
|
write: str => writer.write(encoder.encode(str)),
|
|
end: () => writer.close()
|
|
})
|
|
} catch (err) {
|
|
return new Response(
|
|
(err || 'An error occurred while rendering ' + url.pathname + '.').toString(),
|
|
{
|
|
status: 500,
|
|
headers: { 'x-middleware-ssr': '1' }
|
|
}
|
|
)
|
|
}
|
|
|
|
return new Response(transformStream.readable, {
|
|
headers: { 'x-middleware-ssr': '1' }
|
|
})
|
|
}
|
|
|
|
export default function rscMiddleware(opts) {
|
|
return adapter({
|
|
...opts,
|
|
handler: render
|
|
})
|
|
}
|
|
`
|
|
|
|
return transformed
|
|
}
|