next
Version:
The React Framework
191 lines (189 loc) • 9.57 kB
JavaScript
"use client";
// @ts-ignore
// eslint-disable-next-line import/no-extraneous-dependencies
// import { createFromReadableStream } from 'react-server-dom-webpack/client'
const { createFromReadableStream } = !!process.env.NEXT_RUNTIME ? require('react-server-dom-webpack/client.edge') : require('react-server-dom-webpack/client');
import { NEXT_ROUTER_PREFETCH_HEADER, NEXT_ROUTER_STATE_TREE_HEADER, NEXT_RSC_UNION_QUERY, NEXT_URL, RSC_HEADER, RSC_CONTENT_TYPE_HEADER, NEXT_HMR_REFRESH_HEADER, NEXT_DID_POSTPONE_HEADER, NEXT_ROUTER_STALE_TIME_HEADER } from '../app-router-headers';
import { callServer } from '../../app-call-server';
import { findSourceMapURL } from '../../app-find-source-map-url';
import { PrefetchKind } from './router-reducer-types';
import { hexHash } from '../../../shared/lib/hash';
import { normalizeFlightData } from '../../flight-data-helpers';
function urlToUrlWithoutFlightMarker(url) {
const urlWithoutFlightParameters = new URL(url, location.origin);
urlWithoutFlightParameters.searchParams.delete(NEXT_RSC_UNION_QUERY);
if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export' && urlWithoutFlightParameters.pathname.endsWith('.txt')) {
const { pathname } = urlWithoutFlightParameters;
const length = pathname.endsWith('/index.txt') ? 10 : 4;
// Slice off `/index.txt` or `.txt` from the end of the pathname
urlWithoutFlightParameters.pathname = pathname.slice(0, -length);
}
}
return urlWithoutFlightParameters;
}
function doMpaNavigation(url) {
return {
flightData: urlToUrlWithoutFlightMarker(url).toString(),
canonicalUrl: undefined,
couldBeIntercepted: false,
prerendered: false,
postponed: false,
staleTime: -1
};
}
/**
* Fetch the flight data for the provided url. Takes in the current router state
* to decide what to render server-side.
*/ export async function fetchServerResponse(url, options) {
const { flightRouterState, nextUrl, buildId, prefetchKind } = options;
const headers = {
// Enable flight response
[RSC_HEADER]: '1',
// Provide the current router state
[NEXT_ROUTER_STATE_TREE_HEADER]: encodeURIComponent(JSON.stringify(flightRouterState))
};
/**
* Three cases:
* - `prefetchKind` is `undefined`, it means it's a normal navigation, so we want to prefetch the page data fully
* - `prefetchKind` is `full` - we want to prefetch the whole page so same as above
* - `prefetchKind` is `auto` - if the page is dynamic, prefetch the page data partially, if static prefetch the page data fully
*/ if (prefetchKind === PrefetchKind.AUTO) {
headers[NEXT_ROUTER_PREFETCH_HEADER] = '1';
}
if (process.env.NODE_ENV === 'development' && options.isHmrRefresh) {
headers[NEXT_HMR_REFRESH_HEADER] = '1';
}
if (nextUrl) {
headers[NEXT_URL] = nextUrl;
}
if (process.env.NEXT_DEPLOYMENT_ID) {
headers['x-deployment-id'] = process.env.NEXT_DEPLOYMENT_ID;
}
const uniqueCacheQuery = hexHash([
headers[NEXT_ROUTER_PREFETCH_HEADER] || '0',
headers[NEXT_ROUTER_STATE_TREE_HEADER],
headers[NEXT_URL]
].join(','));
try {
var _res_headers_get;
let fetchUrl = new URL(url);
if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
if (fetchUrl.pathname.endsWith('/')) {
fetchUrl.pathname += 'index.txt';
} else {
fetchUrl.pathname += '.txt';
}
}
}
// Add unique cache query to avoid caching conflicts on CDN which don't respect the Vary header
fetchUrl.searchParams.set(NEXT_RSC_UNION_QUERY, uniqueCacheQuery);
// When creating a "temporary" prefetch (the "on-demand" prefetch that gets created on navigation, if one doesn't exist)
// we send the request with a "high" priority as it's in response to a user interaction that could be blocking a transition.
// Otherwise, all other prefetches are sent with a "low" priority.
// We use "auto" for in all other cases to match the existing default, as this function is shared outside of prefetching.
const fetchPriority = prefetchKind ? prefetchKind === PrefetchKind.TEMPORARY ? 'high' : 'low' : 'auto';
if (process.env.__NEXT_TEST_MODE) {
headers['Next-Test-Fetch-Priority'] = fetchPriority;
}
const res = await fetch(fetchUrl, {
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
credentials: 'same-origin',
headers,
priority: fetchPriority
});
const responseUrl = urlToUrlWithoutFlightMarker(res.url);
const canonicalUrl = res.redirected ? responseUrl : undefined;
const contentType = res.headers.get('content-type') || '';
const interception = !!((_res_headers_get = res.headers.get('vary')) == null ? void 0 : _res_headers_get.includes(NEXT_URL));
const postponed = !!res.headers.get(NEXT_DID_POSTPONE_HEADER);
const staleTimeHeader = res.headers.get(NEXT_ROUTER_STALE_TIME_HEADER);
const staleTime = staleTimeHeader !== null ? parseInt(staleTimeHeader, 10) : -1;
let isFlightResponse = contentType.startsWith(RSC_CONTENT_TYPE_HEADER);
if (process.env.NODE_ENV === 'production') {
if (process.env.__NEXT_CONFIG_OUTPUT === 'export') {
if (!isFlightResponse) {
isFlightResponse = contentType.startsWith('text/plain');
}
}
}
// If fetch returns something different than flight response handle it like a mpa navigation
// If the fetch was not 200, we also handle it like a mpa navigation
if (!isFlightResponse || !res.ok || !res.body) {
// in case the original URL came with a hash, preserve it before redirecting to the new URL
if (url.hash) {
responseUrl.hash = url.hash;
}
return doMpaNavigation(responseUrl.toString());
}
// We may navigate to a page that requires a different Webpack runtime.
// In prod, every page will have the same Webpack runtime.
// In dev, the Webpack runtime is minimal for each page.
// We need to ensure the Webpack runtime is updated before executing client-side JS of the new page.
if (process.env.NODE_ENV !== 'production' && !process.env.TURBOPACK) {
await require('../react-dev-overlay/app/hot-reloader-client').waitForWebpackRuntimeHotUpdate();
}
// Handle the `fetch` readable stream that can be unwrapped by `React.use`.
const flightStream = postponed ? createUnclosingPrefetchStream(res.body) : res.body;
const response = await createFromReadableStream(flightStream, {
callServer,
findSourceMapURL
});
if (buildId !== response.b) {
return doMpaNavigation(res.url);
}
return {
flightData: normalizeFlightData(response.f),
canonicalUrl: canonicalUrl,
couldBeIntercepted: interception,
prerendered: response.S,
postponed,
staleTime
};
} catch (err) {
console.error("Failed to fetch RSC payload for " + url + ". Falling back to browser navigation.", err);
// If fetch fails handle it like a mpa navigation
// TODO-APP: Add a test for the case where a CORS request fails, e.g. external url redirect coming from the response.
// See https://github.com/vercel/next.js/issues/43605#issuecomment-1451617521 for a reproduction.
return {
flightData: url.toString(),
canonicalUrl: undefined,
couldBeIntercepted: false,
prerendered: false,
postponed: false,
staleTime: -1
};
}
}
function createUnclosingPrefetchStream(originalFlightStream) {
// When PPR is enabled, prefetch streams may contain references that never
// resolve, because that's how we encode dynamic data access. In the decoded
// object returned by the Flight client, these are reified into hanging
// promises that suspend during render, which is effectively what we want.
// The UI resolves when it switches to the dynamic data stream
// (via useDeferredValue(dynamic, static)).
//
// However, the Flight implementation currently errors if the server closes
// the response before all the references are resolved. As a cheat to work
// around this, we wrap the original stream in a new stream that never closes,
// and therefore doesn't error.
const reader = originalFlightStream.getReader();
return new ReadableStream({
async pull (controller) {
while(true){
const { done, value } = await reader.read();
if (!done) {
// Pass to the target stream and keep consuming the Flight response
// from the server.
controller.enqueue(value);
continue;
}
// The server stream has closed. Exit, but intentionally do not close
// the target stream.
return;
}
}
});
}
//# sourceMappingURL=fetch-server-response.js.map