Skip to content

Commit

Permalink
unify server action detection logic (#58879)
Browse files Browse the repository at this point in the history
In anticipation of #58885 (and to avoid adding another spot where we're checking the same headers), this unifies the logic that parses the request headers and determines if the request corresponds with a server action.

There was already some drift between the check in `base-server` and `action-handler` (unsure if this was intentional - let me know if so, in which case maybe separated handling is the better approach. I couldn't think of a good reason why it would be different, though). 

Existing tests should be sufficient for testing this changeset.
  • Loading branch information
ztanner committed Nov 27, 2023
1 parent 40a6e61 commit 8f2c482
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 21 deletions.
22 changes: 11 additions & 11 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { AppRenderContext, GenerateFlight } from './app-render'
import type { AppPageModule } from '../../server/future/route-modules/app-page/module'

import {
ACTION,
RSC_HEADER,
RSC_CONTENT_TYPE_HEADER,
} from '../../client/components/app-router-headers'
Expand All @@ -36,6 +35,10 @@ import {
NEXT_CACHE_REVALIDATED_TAGS_HEADER,
NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER,
} from '../../lib/constants'
import {
getIsServerAction,
getServerActionRequestMetadata,
} from '../lib/server-action-request-meta'

function formDataFromSearchQueryString(query: string) {
const searchParams = new URLSearchParams(query)
Expand Down Expand Up @@ -271,20 +274,13 @@ export async function handleAction({
formState?: any
}
> {
let actionId = req.headers[ACTION.toLowerCase()] as string
const contentType = req.headers['content-type']
const isURLEncodedAction =
req.method === 'POST' && contentType === 'application/x-www-form-urlencoded'
const isMultipartAction =
req.method === 'POST' && contentType?.startsWith('multipart/form-data')

const isFetchAction =
actionId !== undefined &&
typeof actionId === 'string' &&
req.method === 'POST'
const { actionId, isURLEncodedAction, isMultipartAction, isFetchAction } =
getServerActionRequestMetadata(req)

// If it's not a Server Action, skip handling.
if (!(isFetchAction || isURLEncodedAction || isMultipartAction)) {
if (!getIsServerAction(req)) {
return
}

Expand Down Expand Up @@ -528,6 +524,10 @@ To configure the body size limit for Server Actions, see: https://nextjs.org/doc

let actionModId: string
try {
if (!actionId) {
throw new Error('Invariant: actionId should be set')
}

actionModId = serverModuleMap[actionId].id
} catch (err) {
// When this happens, it could be a deployment skew where the action came
Expand Down
12 changes: 2 additions & 10 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ import {
RSC_HEADER,
RSC_VARY_HEADER,
NEXT_RSC_UNION_QUERY,
ACTION,
NEXT_ROUTER_PREFETCH_HEADER,
} from '../client/components/app-router-headers'
import type {
Expand Down Expand Up @@ -129,6 +128,7 @@ import {
} from './future/route-modules/checks'
import { PrefetchRSCPathnameNormalizer } from './future/normalizers/request/prefetch-rsc'
import { NextDataPathnameNormalizer } from './future/normalizers/request/next-data'
import { getIsServerAction } from './lib/server-action-request-meta'

export type FindComponentsResult = {
components: LoadComponentsReturnType
Expand Down Expand Up @@ -1749,15 +1749,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const isAppPath = components.isAppPath === true
const hasServerProps = !!components.getServerSideProps
let hasStaticPaths = !!components.getStaticPaths
const actionId = req.headers[ACTION.toLowerCase()] as string
const contentType = req.headers['content-type']
const isMultipartAction =
req.method === 'POST' && contentType?.startsWith('multipart/form-data')
const isFetchAction =
actionId !== undefined &&
typeof actionId === 'string' &&
req.method === 'POST'
const isServerAction = isFetchAction || isMultipartAction
const isServerAction = getIsServerAction(req)
const hasGetInitialProps = !!components.Component?.getInitialProps
let isSSG = !!components.getStaticProps

Expand Down
41 changes: 41 additions & 0 deletions packages/next/src/server/lib/server-action-request-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { IncomingMessage } from 'http'
import type { BaseNextRequest } from '../base-http'
import { ACTION } from '../../client/components/app-router-headers'

export function getServerActionRequestMetadata(
req: IncomingMessage | BaseNextRequest
): {
actionId: string | null
isURLEncodedAction: boolean
isMultipartAction: boolean
isFetchAction: boolean
} {
let actionId: string | null
let contentType: string | null

actionId = (req.headers[ACTION.toLowerCase()] as string) ?? null
contentType = req.headers['content-type'] ?? null

const isURLEncodedAction = Boolean(
req.method === 'POST' && contentType === 'application/x-www-form-urlencoded'
)
const isMultipartAction = Boolean(
req.method === 'POST' && contentType?.startsWith('multipart/form-data')
)
const isFetchAction = Boolean(
actionId !== undefined &&
typeof actionId === 'string' &&
req.method === 'POST'
)

return { actionId, isURLEncodedAction, isMultipartAction, isFetchAction }
}

export function getIsServerAction(
req: IncomingMessage | BaseNextRequest
): boolean {
const { isFetchAction, isURLEncodedAction, isMultipartAction } =
getServerActionRequestMetadata(req)

return Boolean(isFetchAction || isURLEncodedAction || isMultipartAction)
}

0 comments on commit 8f2c482

Please sign in to comment.