UNPKG

next

Version:

The React Framework

191 lines (189 loc) • 9.57 kB
"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