Skip to content

Commit e8011b5

Browse files
authored
feat: decouple cookies and action state from redirect error (#70769)
<!-- 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 # -->
1 parent fcd146c commit e8011b5

File tree

11 files changed

+385
-473
lines changed

11 files changed

+385
-473
lines changed

packages/next/src/client/components/redirect.ts

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { requestAsyncStorage } from './request-async-storage.external'
2-
import type { ResponseCookies } from '../../server/web/spec-extension/cookies'
31
import { actionAsyncStorage } from './action-async-storage.external'
42
import { RedirectStatusCode } from './redirect-status-code'
53

@@ -10,22 +8,17 @@ export enum RedirectType {
108
replace = 'replace',
119
}
1210

13-
export type RedirectError<U extends string> = Error & {
14-
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U};${RedirectStatusCode};`
15-
mutableCookies: ResponseCookies
11+
export type RedirectError = Error & {
12+
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${string};${RedirectStatusCode};`
1613
}
1714

1815
export function getRedirectError(
1916
url: string,
2017
type: RedirectType,
2118
statusCode: RedirectStatusCode = RedirectStatusCode.TemporaryRedirect
22-
): RedirectError<typeof url> {
23-
const error = new Error(REDIRECT_ERROR_CODE) as RedirectError<typeof url>
19+
): RedirectError {
20+
const error = new Error(REDIRECT_ERROR_CODE) as RedirectError
2421
error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`
25-
const requestStore = requestAsyncStorage.getStore()
26-
if (requestStore) {
27-
error.mutableCookies = requestStore.mutableCookies
28-
}
2922
return error
3023
}
3124

@@ -52,12 +45,7 @@ export function redirect(
5245
throw getRedirectError(
5346
url,
5447
redirectType,
55-
// If we're in an action, we want to use a 303 redirect
56-
// as we don't want the POST request to follow the redirect,
57-
// as it could result in erroneous re-submissions.
58-
actionStore?.isAction
59-
? RedirectStatusCode.SeeOther
60-
: RedirectStatusCode.TemporaryRedirect
48+
RedirectStatusCode.TemporaryRedirect
6149
)
6250
}
6351

@@ -77,17 +65,7 @@ export function permanentRedirect(
7765
url: string,
7866
type: RedirectType = RedirectType.replace
7967
): never {
80-
const actionStore = actionAsyncStorage.getStore()
81-
throw getRedirectError(
82-
url,
83-
type,
84-
// If we're in an action, we want to use a 303 redirect
85-
// as we don't want the POST request to follow the redirect,
86-
// as it could result in erroneous re-submissions.
87-
actionStore?.isAction
88-
? RedirectStatusCode.SeeOther
89-
: RedirectStatusCode.PermanentRedirect
90-
)
68+
throw getRedirectError(url, type, RedirectStatusCode.PermanentRedirect)
9169
}
9270

9371
/**
@@ -97,9 +75,7 @@ export function permanentRedirect(
9775
* @param error the error that may reference a redirect error
9876
* @returns true if the error is a redirect error
9977
*/
100-
export function isRedirectError<U extends string>(
101-
error: unknown
102-
): error is RedirectError<U> {
78+
export function isRedirectError(error: unknown): error is RedirectError {
10379
if (
10480
typeof error !== 'object' ||
10581
error === null ||
@@ -132,9 +108,7 @@ export function isRedirectError<U extends string>(
132108
* @param error the error that may be a redirect error
133109
* @return the url if the error was a redirect error
134110
*/
135-
export function getURLFromRedirectError<U extends string>(
136-
error: RedirectError<U>
137-
): U
111+
export function getURLFromRedirectError(error: RedirectError): string
138112
export function getURLFromRedirectError(error: unknown): string | null {
139113
if (!isRedirectError(error)) return null
140114

@@ -143,19 +117,15 @@ export function getURLFromRedirectError(error: unknown): string | null {
143117
return error.digest.split(';').slice(2, -2).join(';')
144118
}
145119

146-
export function getRedirectTypeFromError<U extends string>(
147-
error: RedirectError<U>
148-
): RedirectType {
120+
export function getRedirectTypeFromError(error: RedirectError): RedirectType {
149121
if (!isRedirectError(error)) {
150122
throw new Error('Not a redirect error')
151123
}
152124

153125
return error.digest.split(';', 2)[1] as RedirectType
154126
}
155127

156-
export function getRedirectStatusCodeFromError<U extends string>(
157-
error: RedirectError<U>
158-
): number {
128+
export function getRedirectStatusCodeFromError(error: RedirectError): number {
159129
if (!isRedirectError(error)) {
160130
throw new Error('Not a redirect error')
161131
}

packages/next/src/server/api-utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function wrapApiHandler<T extends (...args: any[]) => any>(
2525
handler: T
2626
): T {
2727
return ((...args) => {
28-
getTracer().getRootSpanAttributes()?.set('next.route', page)
28+
getTracer().setRootSpanAttribute('next.route', page)
2929
// Call API route method
3030
return getTracer().trace(
3131
NodeSpan.runHandler,

packages/next/src/server/app-render/action-handler.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from '../../client/components/app-router-headers'
1414
import { isNotFoundError } from '../../client/components/not-found'
1515
import {
16-
getRedirectStatusCodeFromError,
1716
getRedirectTypeFromError,
1817
getURLFromRedirectError,
1918
isRedirectError,
@@ -43,6 +42,7 @@ import { HeadersAdapter } from '../web/spec-extension/adapters/headers'
4342
import { fromNodeOutgoingHttpHeaders } from '../web/utils'
4443
import { selectWorkerForForwarding } from './action-utils'
4544
import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers'
45+
import { RedirectStatusCode } from '../../client/components/redirect-status-code'
4646

4747
function formDataFromSearchQueryString(query: string) {
4848
const searchParams = new URLSearchParams(query)
@@ -855,7 +855,6 @@ export async function handleAction({
855855
} catch (err) {
856856
if (isRedirectError(err)) {
857857
const redirectUrl = getURLFromRedirectError(err)
858-
const statusCode = getRedirectStatusCodeFromError(err)
859858
const redirectType = getRedirectTypeFromError(err)
860859

861860
await addRevalidationHeader(res, {
@@ -865,7 +864,7 @@ export async function handleAction({
865864

866865
// if it's a fetch action, we'll set the status code for logging/debugging purposes
867866
// but we won't set a Location header, as the redirect will be handled by the client router
868-
res.statusCode = statusCode
867+
res.statusCode = RedirectStatusCode.SeeOther
869868

870869
if (isFetchAction) {
871870
return {
@@ -882,14 +881,11 @@ export async function handleAction({
882881
}
883882
}
884883

885-
if (err.mutableCookies) {
886-
const headers = new Headers()
887-
888-
// If there were mutable cookies set, we need to set them on the
889-
// response.
890-
if (appendMutableCookies(headers, err.mutableCookies)) {
891-
res.setHeader('set-cookie', Array.from(headers.values()))
892-
}
884+
// If there were mutable cookies set, we need to set them on the
885+
// response.
886+
const headers = new Headers()
887+
if (appendMutableCookies(headers, requestStore.mutableCookies)) {
888+
res.setHeader('set-cookie', Array.from(headers.values()))
893889
}
894890

895891
res.setHeader('Location', redirectUrl)

packages/next/src/server/app-render/app-render.tsx

Lines changed: 35 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,7 @@ async function renderToHTMLOrFlightImpl(
10571057
res,
10581058
}
10591059

1060-
getTracer().getRootSpanAttributes()?.set('next.route', pagePath)
1060+
getTracer().setRootSpanAttribute('next.route', pagePath)
10611061

10621062
if (isStaticGeneration) {
10631063
// We're either building or revalidating. In either case we need to
@@ -1590,40 +1590,32 @@ async function renderToStream(
15901590
throw err
15911591
}
15921592

1593+
let errorType: 'not-found' | 'redirect' | undefined
1594+
15931595
if (isNotFoundError(err)) {
1596+
errorType = 'not-found'
15941597
res.statusCode = 404
1595-
}
1596-
let hasRedirectError = false
1597-
if (isRedirectError(err)) {
1598-
hasRedirectError = true
1598+
} else if (isRedirectError(err)) {
1599+
errorType = 'redirect'
15991600
res.statusCode = getRedirectStatusCodeFromError(err)
1600-
if (err.mutableCookies) {
1601-
const headers = new Headers()
16021601

1603-
// If there were mutable cookies set, we need to set them on the
1604-
// response.
1605-
if (appendMutableCookies(headers, err.mutableCookies)) {
1606-
setHeader('set-cookie', Array.from(headers.values()))
1607-
}
1608-
}
16091602
const redirectUrl = addPathPrefix(
16101603
getURLFromRedirectError(err),
16111604
renderOpts.basePath
16121605
)
1613-
setHeader('Location', redirectUrl)
1614-
}
16151606

1616-
const is404 = res.statusCode === 404
1617-
if (!is404 && !hasRedirectError && !shouldBailoutToCSR) {
1607+
// If there were mutable cookies set, we need to set them on the
1608+
// response.
1609+
const headers = new Headers()
1610+
if (appendMutableCookies(headers, ctx.requestStore.mutableCookies)) {
1611+
setHeader('set-cookie', Array.from(headers.values()))
1612+
}
1613+
1614+
setHeader('location', redirectUrl)
1615+
} else if (!shouldBailoutToCSR) {
16181616
res.statusCode = 500
16191617
}
16201618

1621-
const errorType = is404
1622-
? 'not-found'
1623-
: hasRedirectError
1624-
? 'redirect'
1625-
: undefined
1626-
16271619
const [errorPreinitScripts, errorBootstrapScript] = getRequiredScripts(
16281620
renderOpts.buildManifest,
16291621
ctx.assetPrefix,
@@ -2765,46 +2757,38 @@ async function prerenderToStream(
27652757
throw err
27662758
}
27672759

2760+
// If we errored when we did not have an RSC stream to read from. This is
2761+
// not just a render error, we need to throw early.
2762+
if (reactServerPrerenderResult === null) {
2763+
throw err
2764+
}
2765+
2766+
let errorType: 'not-found' | 'redirect' | undefined
2767+
27682768
if (isNotFoundError(err)) {
2769+
errorType = 'not-found'
27692770
res.statusCode = 404
2770-
}
2771-
let hasRedirectError = false
2772-
if (isRedirectError(err)) {
2773-
hasRedirectError = true
2771+
} else if (isRedirectError(err)) {
2772+
errorType = 'redirect'
27742773
res.statusCode = getRedirectStatusCodeFromError(err)
2775-
if (err.mutableCookies) {
2776-
const headers = new Headers()
27772774

2778-
// If there were mutable cookies set, we need to set them on the
2779-
// response.
2780-
if (appendMutableCookies(headers, err.mutableCookies)) {
2781-
setHeader('set-cookie', Array.from(headers.values()))
2782-
}
2783-
}
27842775
const redirectUrl = addPathPrefix(
27852776
getURLFromRedirectError(err),
27862777
renderOpts.basePath
27872778
)
2788-
setHeader('Location', redirectUrl)
2789-
}
27902779

2791-
const is404 = res.statusCode === 404
2792-
if (!is404 && !hasRedirectError && !shouldBailoutToCSR) {
2793-
res.statusCode = 500
2794-
}
2780+
// If there were mutable cookies set, we need to set them on the
2781+
// response.
2782+
const headers = new Headers()
2783+
if (appendMutableCookies(headers, ctx.requestStore.mutableCookies)) {
2784+
setHeader('set-cookie', Array.from(headers.values()))
2785+
}
27952786

2796-
if (reactServerPrerenderResult === null) {
2797-
// We errored when we did not have an RSC stream to read from. This is not just a render
2798-
// error, we need to throw early
2799-
throw err
2787+
setHeader('location', redirectUrl)
2788+
} else if (!shouldBailoutToCSR) {
2789+
res.statusCode = 500
28002790
}
28012791

2802-
const errorType = is404
2803-
? 'not-found'
2804-
: hasRedirectError
2805-
? 'redirect'
2806-
: undefined
2807-
28082792
const [errorPreinitScripts, errorBootstrapScript] = getRequiredScripts(
28092793
renderOpts.buildManifest,
28102794
ctx.assetPrefix,

packages/next/src/server/base-server.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,6 @@ import { getTracer, isBubbledError, SpanKind } from './lib/trace/tracer'
123123
import { BaseServerSpan } from './lib/trace/constants'
124124
import { I18NProvider } from './lib/i18n-provider'
125125
import { sendResponse } from './send-response'
126-
import {
127-
handleBadRequestResponse,
128-
handleInternalServerErrorResponse,
129-
} from './route-modules/helpers/response-handlers'
130126
import {
131127
fromNodeOutgoingHttpHeaders,
132128
normalizeNextQueryParam,
@@ -2587,7 +2583,7 @@ export default abstract class Server<
25872583
Log.error(err)
25882584

25892585
// Otherwise, send a 500 response.
2590-
await sendResponse(req, res, handleInternalServerErrorResponse())
2586+
await sendResponse(req, res, new Response(null, { status: 500 }))
25912587

25922588
return null
25932589
}
@@ -2597,7 +2593,7 @@ export default abstract class Server<
25972593
) {
25982594
// An OPTIONS request to a page handler is invalid.
25992595
if (req.method === 'OPTIONS' && !is404Page) {
2600-
await sendResponse(req, res, handleBadRequestResponse())
2596+
await sendResponse(req, res, new Response(null, { status: 400 }))
26012597
return null
26022598
}
26032599

@@ -3576,7 +3572,7 @@ export default abstract class Server<
35763572
shouldEnsure: false,
35773573
})
35783574
if (result) {
3579-
getTracer().getRootSpanAttributes()?.set('next.route', pathname)
3575+
getTracer().setRootSpanAttribute('next.route', pathname)
35803576
try {
35813577
return await this.renderToResponseWithComponents(ctx, result)
35823578
} catch (err) {

packages/next/src/server/lib/trace/tracer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,14 @@ class NextTracerImpl implements NextTracer {
450450
const spanId = context.active().getValue(rootSpanIdKey) as number
451451
return rootSpanAttributesStore.get(spanId)
452452
}
453+
454+
public setRootSpanAttribute(key: AttributeNames, value: AttributeValue) {
455+
const spanId = context.active().getValue(rootSpanIdKey) as number
456+
const attributes = rootSpanAttributesStore.get(spanId)
457+
if (attributes) {
458+
attributes.set(key, value)
459+
}
460+
}
453461
}
454462

455463
const getTracer = (() => {

packages/next/src/server/render.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1391,7 +1391,7 @@ export async function renderToHTMLImpl(
13911391
}
13921392
}
13931393

1394-
getTracer().getRootSpanAttributes()?.set('next.route', renderOpts.page)
1394+
getTracer().setRootSpanAttribute('next.route', renderOpts.page)
13951395
const documentResult = await getTracer().trace(
13961396
RenderSpan.renderDocument,
13971397
{

packages/next/src/server/route-modules/app-route/helpers/auto-implement-methods.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { AppRouteHandlerFn, AppRouteHandlers } from '../module'
22

33
import { HTTP_METHODS, type HTTP_METHOD } from '../../../web/http'
4-
import { handleMethodNotAllowedResponse } from '../../helpers/response-handlers'
54

65
const AUTOMATIC_ROUTE_METHODS = ['HEAD', 'OPTIONS'] as const
76

7+
function handleMethodNotAllowedResponse(): Response {
8+
return new Response(null, { status: 405 })
9+
}
10+
811
export function autoImplementMethods(
912
handlers: AppRouteHandlers
1013
): Record<HTTP_METHOD, AppRouteHandlerFn> {

0 commit comments

Comments
 (0)