2020-07-01 16:59:18 +02:00
|
|
|
import { IncomingMessage, ServerResponse } from 'http'
|
2021-06-30 13:44:40 +02:00
|
|
|
import { isResSent } from '../shared/lib/utils'
|
2020-11-06 03:33:14 +01:00
|
|
|
import generateETag from 'etag'
|
2020-07-01 16:59:18 +02:00
|
|
|
import fresh from 'next/dist/compiled/fresh'
|
Add `RenderResult` (#27319)
Adds `RenderResult`, replacing the `string` that `renderToHTML` used to return, with an `Observable`-like API that callers can use to subscribe and get a callback when chunks are available to flush, etc.
This is the last architectural change needed for streaming. There are, however, other things currently standing in the way of streaming. For example, it is common to mutate `res` in `getServerSideProps` to do routing work, or write headers, before fetching page data. This pattern effectively nullifies any advantages of streaming. I may do a follow-up PR that adds an experimental alternative for applications not using React 18, but the main purpose for this support is for Suspense and Server Components.
For that reason, there's no actual streaming here yet: instead we just flush a single chunk. A follow-up PR will add support for streaming suspense boundaries in React 18.
2021-07-27 21:18:21 +02:00
|
|
|
import { RenderResult } from './utils'
|
2020-03-13 10:40:10 +01:00
|
|
|
|
Replace `withCoalescedInvoke` with `ResponseCache` (#26997)
By itself, `withCoalescedInvoke` with a separate `this.incrementalCache.set(...)` isn't really suitable for streaming responses. Since streaming is asynchronous, updating the cache separately introduces a gap where another origin request for the same resource could be made.
This could potentially be addressed by moving the cache update, but then `IncrementalCache` itself would need to be made to support streaming, in addition to the many other responsibilities it has. In this case, it seemed best to just use composition to add another caching layer in front of it, which is a familiar and understandable concept. Eventually, we might want to move this cache to the HTTP layer, which will also be simpler with this change.
As an added bonus, `renderToResponseWithComponents` becomes significantly simpler, and we delete some duplication.
2021-07-12 21:47:39 +02:00
|
|
|
export type PayloadOptions =
|
2020-11-14 08:12:47 +01:00
|
|
|
| { private: true }
|
|
|
|
| { private: boolean; stateful: true }
|
|
|
|
| { private: boolean; stateful: false; revalidate: number | false }
|
|
|
|
|
|
|
|
export function setRevalidateHeaders(
|
|
|
|
res: ServerResponse,
|
|
|
|
options: PayloadOptions
|
|
|
|
) {
|
|
|
|
if (options.private || options.stateful) {
|
|
|
|
if (options.private || !res.hasHeader('Cache-Control')) {
|
|
|
|
res.setHeader(
|
|
|
|
'Cache-Control',
|
|
|
|
`private, no-cache, no-store, max-age=0, must-revalidate`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else if (typeof options.revalidate === 'number') {
|
|
|
|
if (options.revalidate < 1) {
|
|
|
|
throw new Error(
|
|
|
|
`invariant: invalid Cache-Control duration provided: ${options.revalidate} < 1`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
res.setHeader(
|
|
|
|
'Cache-Control',
|
|
|
|
`s-maxage=${options.revalidate}, stale-while-revalidate`
|
|
|
|
)
|
|
|
|
} else if (options.revalidate === false) {
|
|
|
|
res.setHeader('Cache-Control', `s-maxage=31536000, stale-while-revalidate`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-13 10:40:10 +01:00
|
|
|
export function sendPayload(
|
2020-07-01 16:59:18 +02:00
|
|
|
req: IncomingMessage,
|
2020-03-13 10:40:10 +01:00
|
|
|
res: ServerResponse,
|
|
|
|
payload: any,
|
|
|
|
type: 'html' | 'json',
|
2020-07-27 22:19:30 +02:00
|
|
|
{
|
|
|
|
generateEtags,
|
|
|
|
poweredByHeader,
|
|
|
|
}: { generateEtags: boolean; poweredByHeader: boolean },
|
2020-11-14 08:12:47 +01:00
|
|
|
options?: PayloadOptions
|
2020-03-13 10:40:10 +01:00
|
|
|
): void {
|
Add `RenderResult` (#27319)
Adds `RenderResult`, replacing the `string` that `renderToHTML` used to return, with an `Observable`-like API that callers can use to subscribe and get a callback when chunks are available to flush, etc.
This is the last architectural change needed for streaming. There are, however, other things currently standing in the way of streaming. For example, it is common to mutate `res` in `getServerSideProps` to do routing work, or write headers, before fetching page data. This pattern effectively nullifies any advantages of streaming. I may do a follow-up PR that adds an experimental alternative for applications not using React 18, but the main purpose for this support is for Suspense and Server Components.
For that reason, there's no actual streaming here yet: instead we just flush a single chunk. A follow-up PR will add support for streaming suspense boundaries in React 18.
2021-07-27 21:18:21 +02:00
|
|
|
sendRenderResult({
|
|
|
|
req,
|
|
|
|
res,
|
|
|
|
resultOrPayload: payload,
|
|
|
|
type,
|
|
|
|
generateEtags,
|
|
|
|
poweredByHeader,
|
|
|
|
options,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
export function sendRenderResult({
|
|
|
|
req,
|
|
|
|
res,
|
|
|
|
resultOrPayload,
|
|
|
|
type,
|
|
|
|
generateEtags,
|
|
|
|
poweredByHeader,
|
|
|
|
options,
|
|
|
|
}: {
|
|
|
|
req: IncomingMessage
|
|
|
|
res: ServerResponse
|
|
|
|
resultOrPayload: RenderResult | string
|
|
|
|
type: 'html' | 'json'
|
|
|
|
generateEtags: boolean
|
|
|
|
poweredByHeader: boolean
|
|
|
|
options?: PayloadOptions
|
|
|
|
}): void {
|
2020-03-13 10:40:10 +01:00
|
|
|
if (isResSent(res)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-27 22:19:30 +02:00
|
|
|
if (poweredByHeader && type === 'html') {
|
|
|
|
res.setHeader('X-Powered-By', 'Next.js')
|
|
|
|
}
|
|
|
|
|
Add `RenderResult` (#27319)
Adds `RenderResult`, replacing the `string` that `renderToHTML` used to return, with an `Observable`-like API that callers can use to subscribe and get a callback when chunks are available to flush, etc.
This is the last architectural change needed for streaming. There are, however, other things currently standing in the way of streaming. For example, it is common to mutate `res` in `getServerSideProps` to do routing work, or write headers, before fetching page data. This pattern effectively nullifies any advantages of streaming. I may do a follow-up PR that adds an experimental alternative for applications not using React 18, but the main purpose for this support is for Suspense and Server Components.
For that reason, there's no actual streaming here yet: instead we just flush a single chunk. A follow-up PR will add support for streaming suspense boundaries in React 18.
2021-07-27 21:18:21 +02:00
|
|
|
const isPayload = typeof resultOrPayload === 'string'
|
|
|
|
|
|
|
|
if (isPayload) {
|
|
|
|
const etag = generateEtags
|
|
|
|
? generateETag(resultOrPayload as string)
|
|
|
|
: undefined
|
|
|
|
if (sendEtagResponse(req, res, etag)) {
|
|
|
|
return
|
|
|
|
}
|
2020-07-01 16:59:18 +02:00
|
|
|
}
|
|
|
|
|
2020-07-27 22:19:30 +02:00
|
|
|
if (!res.getHeader('Content-Type')) {
|
|
|
|
res.setHeader(
|
|
|
|
'Content-Type',
|
|
|
|
type === 'json' ? 'application/json' : 'text/html; charset=utf-8'
|
|
|
|
)
|
|
|
|
}
|
Add `RenderResult` (#27319)
Adds `RenderResult`, replacing the `string` that `renderToHTML` used to return, with an `Observable`-like API that callers can use to subscribe and get a callback when chunks are available to flush, etc.
This is the last architectural change needed for streaming. There are, however, other things currently standing in the way of streaming. For example, it is common to mutate `res` in `getServerSideProps` to do routing work, or write headers, before fetching page data. This pattern effectively nullifies any advantages of streaming. I may do a follow-up PR that adds an experimental alternative for applications not using React 18, but the main purpose for this support is for Suspense and Server Components.
For that reason, there's no actual streaming here yet: instead we just flush a single chunk. A follow-up PR will add support for streaming suspense boundaries in React 18.
2021-07-27 21:18:21 +02:00
|
|
|
|
|
|
|
if (isPayload) {
|
|
|
|
res.setHeader(
|
|
|
|
'Content-Length',
|
|
|
|
Buffer.byteLength(resultOrPayload as string)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-03-13 10:40:10 +01:00
|
|
|
if (options != null) {
|
2020-11-14 08:12:47 +01:00
|
|
|
setRevalidateHeaders(res, options)
|
2020-03-13 10:40:10 +01:00
|
|
|
}
|
Add `RenderResult` (#27319)
Adds `RenderResult`, replacing the `string` that `renderToHTML` used to return, with an `Observable`-like API that callers can use to subscribe and get a callback when chunks are available to flush, etc.
This is the last architectural change needed for streaming. There are, however, other things currently standing in the way of streaming. For example, it is common to mutate `res` in `getServerSideProps` to do routing work, or write headers, before fetching page data. This pattern effectively nullifies any advantages of streaming. I may do a follow-up PR that adds an experimental alternative for applications not using React 18, but the main purpose for this support is for Suspense and Server Components.
For that reason, there's no actual streaming here yet: instead we just flush a single chunk. A follow-up PR will add support for streaming suspense boundaries in React 18.
2021-07-27 21:18:21 +02:00
|
|
|
|
|
|
|
if (req.method === 'HEAD') {
|
|
|
|
res.end(null)
|
|
|
|
} else if (isPayload) {
|
|
|
|
res.end(resultOrPayload as string)
|
|
|
|
} else {
|
|
|
|
;(resultOrPayload as RenderResult)({
|
|
|
|
next: (chunk) => res.write(chunk),
|
|
|
|
error: (_) => res.end(),
|
|
|
|
complete: () => res.end(),
|
|
|
|
})
|
|
|
|
}
|
2020-03-13 10:40:10 +01:00
|
|
|
}
|
2020-11-10 05:40:26 +01:00
|
|
|
|
|
|
|
export function sendEtagResponse(
|
|
|
|
req: IncomingMessage,
|
|
|
|
res: ServerResponse,
|
|
|
|
etag: string | undefined
|
|
|
|
): boolean {
|
|
|
|
if (etag) {
|
|
|
|
/**
|
|
|
|
* The server generating a 304 response MUST generate any of the
|
|
|
|
* following header fields that would have been sent in a 200 (OK)
|
|
|
|
* response to the same request: Cache-Control, Content-Location, Date,
|
|
|
|
* ETag, Expires, and Vary. https://tools.ietf.org/html/rfc7232#section-4.1
|
|
|
|
*/
|
|
|
|
res.setHeader('ETag', etag)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fresh(req.headers, { etag })) {
|
|
|
|
res.statusCode = 304
|
|
|
|
res.end()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|