Skip to content

Commit

Permalink
update status codes for redirect and permanentRedirect in action …
Browse files Browse the repository at this point in the history
…handlers
  • Loading branch information
ztanner committed Nov 24, 2023
1 parent 85ffb7a commit b492e6e
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: API Reference for the permanentRedirect function.

The `permanentRedirect` function allows you to redirect the user to another URL. `permanentRedirect` can be used in Server Components, Client Components, [Route Handlers](/docs/app/building-your-application/routing/route-handlers), and [Server Actions](/docs/app/building-your-application/data-fetching/forms-and-mutations).

When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. Otherwise, it will serve a 308 (Permanent) HTTP redirect response to the caller.
When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. When used in a server action, it will serve a 301 (Permanent) HTTP redirect response to the caller. Otherwise, it will serve a 308 (Permanent) HTTP redirect response to the caller.

If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead.

Expand Down
2 changes: 1 addition & 1 deletion docs/02-app/02-api-reference/04-functions/redirect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: API Reference for the redirect function.

The `redirect` function allows you to redirect the user to another URL. `redirect` can be used in Server Components, Client Components, [Route Handlers](/docs/app/building-your-application/routing/route-handlers), and [Server Actions](/docs/app/building-your-application/data-fetching/forms-and-mutations).

When used in a [streaming context](/docs/app/building-your-application/routing/loading-ui-and-streaming#what-is-streaming), this will insert a meta tag to emit the redirect on the client side. Otherwise, it will serve a 307 HTTP redirect response to the caller.
When used in a [streaming context](/docs/app/building-your-application/routing/loading-ui-and-streaming#what-is-streaming), this will insert a meta tag to emit the redirect on the client side. When used in a server action, it will serve a 302 (Found) HTTP redirect response to the caller. Otherwise, it will serve a 307 HTTP redirect response to the caller.

If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead.

Expand Down
50 changes: 38 additions & 12 deletions packages/next/src/client/components/redirect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { requestAsyncStorage } from './request-async-storage.external'
import type { ResponseCookies } from '../../server/web/spec-extension/cookies'
import { actionAsyncStorage } from './action-async-storage.external'
import { RedirectStatusCode } from '../../shared/lib/constants'

const REDIRECT_ERROR_CODE = 'NEXT_REDIRECT'

Expand All @@ -9,17 +11,17 @@ export enum RedirectType {
}

export type RedirectError<U extends string> = Error & {
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U};${boolean}`
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${U};${RedirectStatusCode};`
mutableCookies: ResponseCookies
}

export function getRedirectError(
url: string,
type: RedirectType,
permanent: boolean = false
statusCode: RedirectStatusCode = RedirectStatusCode.TemporaryRedirect
): RedirectError<typeof url> {
const error = new Error(REDIRECT_ERROR_CODE) as RedirectError<typeof url>
error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${permanent}`
error.digest = `${REDIRECT_ERROR_CODE};${type};${url};${statusCode};`
const requestStore = requestAsyncStorage.getStore()
if (requestStore) {
error.mutableCookies = requestStore.mutableCookies
Expand All @@ -30,29 +32,49 @@ export function getRedirectError(
/**
* When used in a streaming context, this will insert a meta tag to
* redirect the user to the target page. When used in a custom app route, it
* will serve a 307 to the caller.
* will serve a 307/302 to the caller.
*
* @param url the url to redirect to
*/
export function redirect(
url: string,
type: RedirectType = RedirectType.replace
): never {
throw getRedirectError(url, type, false)
const actionStore = actionAsyncStorage.getStore()
throw getRedirectError(
url,
type,
// If we're in an action, we want to use a 302 redirect
// as we don't want the POST request to follow the redirect,
// as it could result in erroneous re-submissions.
actionStore?.isAction
? RedirectStatusCode.Found
: RedirectStatusCode.TemporaryRedirect
)
}

/**
* When used in a streaming context, this will insert a meta tag to
* redirect the user to the target page. When used in a custom app route, it
* will serve a 308 to the caller.
* will serve a 308/301 to the caller.
*
* @param url the url to redirect to
*/
export function permanentRedirect(
url: string,
type: RedirectType = RedirectType.replace
): never {
throw getRedirectError(url, type, true)
const actionStore = actionAsyncStorage.getStore()
throw getRedirectError(
url,
type,
// If we're in an action, we want to use a 301 redirect
// as we don't want the POST request to follow the redirect,
// as it could result in erroneous re-submissions.
actionStore?.isAction
? RedirectStatusCode.MovedPermanently
: RedirectStatusCode.PermanentRedirect
)
}

/**
Expand All @@ -67,15 +89,19 @@ export function isRedirectError<U extends string>(
): error is RedirectError<U> {
if (typeof error?.digest !== 'string') return false

const [errorCode, type, destination, permanent] = (
error.digest as string
).split(';', 4)
const [errorCode, type, destination, status] = (error.digest as string).split(
';',
4
)

const statusCode = Number(status)

return (
errorCode === REDIRECT_ERROR_CODE &&
(type === 'replace' || type === 'push') &&
typeof destination === 'string' &&
(permanent === 'true' || permanent === 'false')
!isNaN(statusCode) &&
statusCode in RedirectStatusCode
)
}

Expand Down Expand Up @@ -114,5 +140,5 @@ export function getRedirectStatusCodeFromError<U extends string>(
throw new Error('Not a redirect error')
}

return error.digest.split(';', 4)[3] === 'true' ? 308 : 307
return Number(error.digest.split(';', 4)[3])
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { requestAsyncStorage } from '../../../../client/components/request-async
import { staticGenerationAsyncStorage } from '../../../../client/components/static-generation-async-storage.external'
import { actionAsyncStorage } from '../../../../client/components/action-async-storage.external'
import * as sharedModules from './shared-modules'
import { getIsServerAction } from '../../../lib/server-action-request-meta'

/**
* The AppRouteModule is the type of the module exported by the bundled App
Expand Down Expand Up @@ -274,6 +275,7 @@ export class AppRouteRouteModule extends RouteModule<
const response: unknown = await this.actionAsyncStorage.run(
{
isAppRoute: true,
isAction: getIsServerAction(request),
},
() =>
RequestAsyncStorageWrapper.wrap(
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/shared/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,10 @@ export const SYSTEM_ENTRYPOINTS = new Set<string>([
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
])

export enum RedirectStatusCode {
MovedPermanently = 301,
Found = 302,
TemporaryRedirect = 307,
PermanentRedirect = 308,
}
49 changes: 49 additions & 0 deletions test/e2e/app-dir/actions/app-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,5 +858,54 @@ createNextDescribe(
expect(html).not.toContain('qwerty123')
})
})

describe('redirects', () => {
it('redirects properly when server action handler uses `redirect`', async () => {
const postRequests = []

const browser = await next.browser('/redirects', {
beforePageLoad(page) {
page.on('request', (request) => {
const url = new URL(request.url())
if (
url.pathname.includes('/api-') &&
request.method() === 'POST'
) {
postRequests.push(url.pathname)
}
})
},
})
await browser.elementById('submit-api-redirect').click()
await check(() => browser.url(), /success=true/)

// verify that the POST request was only made to the action handler
expect(postRequests).toEqual(['/redirects/api-redirect'])
})

it('redirects properly when server action handler uses `permanentRedirect`', async () => {
const postRequests = []

const browser = await next.browser('/redirects', {
beforePageLoad(page) {
page.on('request', (request) => {
const url = new URL(request.url())
if (
url.pathname.includes('/api-') &&
request.method() === 'POST'
) {
postRequests.push(url.pathname)
}
})
},
})

await browser.elementById('submit-api-redirect-permanent').click()
await check(() => browser.url(), /success=true/)

// verify that the POST request was only made to the action handler
expect(postRequests).toEqual(['/redirects/api-redirect-permanent'])
})
})
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { permanentRedirect } from 'next/navigation'

export function POST(request) {
permanentRedirect('/redirects/?success=true')
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/actions/app/redirects/api-redirect/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'

export function POST(request) {
redirect('/redirects/?success=true')
}
18 changes: 18 additions & 0 deletions test/e2e/app-dir/actions/app/redirects/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function Home() {
return (
<main id="redirect-page">
<h1>POST /api-redirect (`redirect()`)</h1>
<form action="/redirects/api-redirect" method="POST">
<input type="submit" value="Submit" id="submit-api-redirect" />
</form>
<h1>POST /api-redirect-permanent (`permanentRedirect()`)</h1>
<form action="/redirects/api-redirect-permanent" method="POST">
<input
type="submit"
value="Submit"
id="submit-api-redirect-permanent"
/>
</form>
</main>
)
}

0 comments on commit b492e6e

Please sign in to comment.