Skip to content

Commit

Permalink
Ensure static generation storage is accessed correctly (vercel#64088)
Browse files Browse the repository at this point in the history
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc
https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See
https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

### What?

Primarily, this is fixing the code highlighted here:


https://github.com/vercel/next.js/blob/073cd74433146d6d3849110e7dada92351ebb7d4/packages/next/src/server/lib/patch-fetch.ts#L228-L230

As `(fetch as any).__nextGetStaticStore?.()` returns
`StaticGenerationAsyncStorage | undefined`, and not
`StaticGenerationStore`. A call to `.getStore()` as the previous line
does corrects this issue.

Secondarily, this improves the `as any` type access being done on the
patched fetch object to make it more type safe and easier to work with.
Since this was added, some features like the `.external` files were
added that allowed files to import the correct async local storage
object in client and server environments correctly to allow for direct
access. Code across Next.js no-longer uses this mechanism to access the
storage, and instead relies on this special treated import.

Types were improved within the `patch-fetch.ts` file to allow for safer
property access by adding consistent types and guards.

### Why?

Without this change, checks like:


https://github.com/vercel/next.js/blob/073cd74433146d6d3849110e7dada92351ebb7d4/packages/next/src/server/lib/patch-fetch.ts#L246

Always fail, because when `(fetch as any).__nextGetStaticStore?.()`
returns `StaticGenerationAsyncStorage`, it isn't the actual store, so
the type is wrong.

Closes NEXT-3008
  • Loading branch information
wyattjoh authored and epiloguess committed Apr 5, 2024
1 parent e2b6dfa commit 11d81bf
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 67 deletions.
16 changes: 6 additions & 10 deletions packages/next/src/client/components/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react'
import { usePathname } from './navigation'
import { isNextRouterError } from './is-next-router-error'
import { staticGenerationAsyncStorage } from './static-generation-async-storage.external'

const styles = {
error: {
Expand Down Expand Up @@ -50,17 +51,12 @@ interface ErrorBoundaryHandlerState {
// function crashes so we can maintain our previous cache
// instead of caching the error page
function HandleISRError({ error }: { error: any }) {
if (typeof (fetch as any).__nextGetStaticStore === 'function') {
const store:
| undefined
| import('./static-generation-async-storage.external').StaticGenerationStore =
(fetch as any).__nextGetStaticStore()?.getStore()

if (store?.isRevalidate || store?.isStaticGeneration) {
console.error(error)
throw error
}
const store = staticGenerationAsyncStorage.getStore()
if (store?.isRevalidate || store?.isStaticGeneration) {
console.error(error)
throw error
}

return null
}

Expand Down
93 changes: 58 additions & 35 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ import type { FetchMetric } from '../base-http'

const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'

type Fetcher = typeof fetch

type PatchedFetcher = Fetcher & {
readonly __nextPatched: true
readonly __nextGetStaticStore: () => StaticGenerationAsyncStorage
readonly _nextOriginalFetch: Fetcher
}

function isPatchedFetch(
fetch: Fetcher | PatchedFetcher
): fetch is PatchedFetcher {
return '__nextPatched' in fetch && fetch.__nextPatched === true
}

export function validateRevalidate(
revalidateVal: unknown,
pathname: string
Expand Down Expand Up @@ -174,22 +188,16 @@ interface PatchableModule {
staticGenerationAsyncStorage: StaticGenerationAsyncStorage
}

// we patch fetch to collect cache information used for
// determining if a page is static or not
export function patchFetch({
serverHooks,
staticGenerationAsyncStorage,
}: PatchableModule) {
if (!(globalThis as any)._nextOriginalFetch) {
;(globalThis as any)._nextOriginalFetch = globalThis.fetch
}

if ((globalThis.fetch as any).__nextPatched) return

const { DynamicServerError } = serverHooks
const originFetch: typeof fetch = (globalThis as any)._nextOriginalFetch

globalThis.fetch = async (
function createPatchedFetcher(
originFetch: Fetcher,
{
serverHooks: { DynamicServerError },
staticGenerationAsyncStorage,
}: PatchableModule
): PatchedFetcher {
// Create the patched fetch function. We don't set the type here, as it's
// verified as the return value of this function.
const patched = async (
input: RequestInfo | URL,
init: RequestInit | undefined
) => {
Expand All @@ -211,7 +219,7 @@ export function patchFetch({
const isInternal = (init?.next as any)?.internal === true
const hideSpan = process.env.NEXT_OTEL_FETCH_DISABLED === '1'

return await getTracer().trace(
return getTracer().trace(
isInternal ? NextNodeServerSpan.internalFetch : AppRenderSpan.fetch,
{
hideSpan,
Expand All @@ -225,9 +233,18 @@ export function patchFetch({
},
},
async () => {
const staticGenerationStore: StaticGenerationStore =
staticGenerationAsyncStorage.getStore() ||
(fetch as any).__nextGetStaticStore?.()
// If this is an internal fetch, we should not do any special treatment.
if (isInternal) return originFetch(input, init)

const staticGenerationStore = staticGenerationAsyncStorage.getStore()

// If the staticGenerationStore is not available, we can't do any
// special treatment of fetch, therefore fallback to the original
// fetch implementation.
if (!staticGenerationStore || staticGenerationStore.isDraftMode) {
return originFetch(input, init)
}

const isRequestInput =
input &&
typeof input === 'object' &&
Expand All @@ -239,17 +256,6 @@ export function patchFetch({
return value || (isRequestInput ? (input as any)[field] : null)
}

// If the staticGenerationStore is not available, we can't do any
// special treatment of fetch, therefore fallback to the original
// fetch implementation.
if (
!staticGenerationStore ||
isInternal ||
staticGenerationStore.isDraftMode
) {
return originFetch(input, init)
}

let revalidate: number | undefined | false = undefined
const getNextField = (field: 'revalidate' | 'tags') => {
return typeof init?.next?.[field] !== 'undefined'
Expand Down Expand Up @@ -718,8 +724,25 @@ export function patchFetch({
}
)
}
;(globalThis.fetch as any).__nextGetStaticStore = () => {
return staticGenerationAsyncStorage
}
;(globalThis.fetch as any).__nextPatched = true

// Attach the necessary properties to the patched fetch function.
patched.__nextPatched = true as const
patched.__nextGetStaticStore = () => staticGenerationAsyncStorage
patched._nextOriginalFetch = originFetch

return patched
}

// we patch fetch to collect cache information used for
// determining if a page is static or not
export function patchFetch(options: PatchableModule) {
// If we've already patched fetch, we should not patch it again.
if (isPatchedFetch(globalThis.fetch)) return

// Grab the original fetch function. We'll attach this so we can use it in
// the patched fetch function.
const original = globalThis.fetch

// Set the global fetch to the patched fetch.
globalThis.fetch = createPatchedFetcher(original, options)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { RequestCookies } from '../cookies'
import type { StaticGenerationStore } from '../../../../client/components/static-generation-async-storage.external'

import { ResponseCookies } from '../cookies'
import { ReflectAdapter } from './reflect'
import { staticGenerationAsyncStorage } from '../../../../client/components/static-generation-async-storage.external'

/**
* @internal
Expand Down Expand Up @@ -106,9 +106,7 @@ export class MutableRequestCookiesAdapter {
const modifiedCookies = new Set<string>()
const updateResponseCookies = () => {
// TODO-APP: change method of getting staticGenerationAsyncStore
const staticGenerationAsyncStore = (fetch as any)
.__nextGetStaticStore?.()
?.getStore() as undefined | StaticGenerationStore
const staticGenerationAsyncStore = staticGenerationAsyncStorage.getStore()
if (staticGenerationAsyncStore) {
staticGenerationAsyncStore.pathWasRevalidated = true
}
Expand Down
13 changes: 2 additions & 11 deletions packages/next/src/server/web/spec-extension/revalidate.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import type {
StaticGenerationAsyncStorage,
StaticGenerationStore,
} from '../../../client/components/static-generation-async-storage.external'
import { trackDynamicDataAccessed } from '../../app-render/dynamic-rendering'
import { isDynamicRoute } from '../../../shared/lib/router/utils'
import {
NEXT_CACHE_IMPLICIT_TAG_ID,
NEXT_CACHE_SOFT_TAG_MAX_LENGTH,
} from '../../../lib/constants'
import { getPathname } from '../../../lib/url'
import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'

/**
* This function allows you to purge [cached data](https://nextjs.org/docs/app/building-your-application/caching) on-demand for a specific cache tag.
Expand Down Expand Up @@ -45,13 +42,7 @@ export function revalidatePath(originalPath: string, type?: 'layout' | 'page') {
}

function revalidate(tag: string, expression: string) {
const staticGenerationAsyncStorage = (
fetch as any
).__nextGetStaticStore?.() as undefined | StaticGenerationAsyncStorage

const store: undefined | StaticGenerationStore =
staticGenerationAsyncStorage?.getStore()

const store = staticGenerationAsyncStorage.getStore()
if (!store || !store.incrementalCache) {
throw new Error(
`Invariant: static generation store missing in ${expression}`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { StaticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
import type { IncrementalCache } from '../../lib/incremental-cache'

import { staticGenerationAsyncStorage as _staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
import { CACHE_ONE_YEAR } from '../../../lib/constants'
import {
addImplicitTags,
validateRevalidate,
validateTags,
} from '../../lib/patch-fetch'
import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'

type Callback = (...args: any[]) => Promise<any>

Expand Down Expand Up @@ -59,11 +58,6 @@ export function unstable_cache<T extends Callback>(
tags?: string[]
} = {}
): T {
const staticGenerationAsyncStorage =
((fetch as any).__nextGetStaticStore?.() as
| StaticGenerationAsyncStorage
| undefined) ?? _staticGenerationAsyncStorage

if (options.revalidate === 0) {
throw new Error(
`Invariant revalidate: 0 can not be passed to unstable_cache(), must be "false" or "> 0" ${cb.toString()}`
Expand Down

0 comments on commit 11d81bf

Please sign in to comment.