Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add body parser limit for server actions #51104

Merged
merged 23 commits into from Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8941247
feat: add body parser limit for server actions
devjiwonchoi Jun 10, 2023
bd5a4e2
chore: add validation for server actions in use
devjiwonchoi Jun 10, 2023
c69d44d
fix: fix validating serverActionsLimit as ternary
devjiwonchoi Jun 10, 2023
aaaaea3
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 10, 2023
f244f03
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 11, 2023
540a05f
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 12, 2023
e3d8323
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 12, 2023
38e7a37
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 13, 2023
b6bd10d
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 13, 2023
119afe3
refac: remove unnecessary condition
devjiwonchoi Jun 14, 2023
3cc4a47
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 15, 2023
710d810
chore: add serverActionsSizeLimit type to config-schema
devjiwonchoi Jun 15, 2023
d08d078
chore: move loadConfig to renderOpts
devjiwonchoi Jun 15, 2023
7e6c80d
Merge branch 'canary' into feat/server-actions-bodyparser-limit
devjiwonchoi Jun 15, 2023
aa9e0db
chore: add error case for serverActionsSizeLimit
devjiwonchoi Jun 15, 2023
b7daec8
chore: add fail test for invalid size limit config
devjiwonchoi Jun 15, 2023
7d918db
chore: modify test condition text
devjiwonchoi Jun 15, 2023
402e61c
chore: correct grammar of test condition text
devjiwonchoi Jun 15, 2023
8ce8a96
Merge branch 'canary' into feat/server-actions-bodyparser-limit
shuding Jun 23, 2023
915b9b9
avoid loading nextConfig again
shuding Jun 23, 2023
2e39afe
fix lint and early error
shuding Jun 23, 2023
351c20e
fix test
shuding Jun 23, 2023
cf79508
Merge branch 'canary' into feat/server-actions-bodyparser-limit
shuding Jun 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/next/src/build/webpack-config.ts
Expand Up @@ -773,8 +773,9 @@ export default async function getBaseWebpackConfig(
const hasServerComponents = hasAppDir
const disableOptimizedLoading = true
const enableTypedRoutes = !!config.experimental.typedRoutes && hasAppDir
const serverActions = !!config.experimental.serverActions && hasAppDir
const bundledReactChannel = serverActions ? '-experimental' : ''
const useServerActions = !!config.experimental.serverActions && hasAppDir
const serverActionsSizeLimit = config.experimental.serverActionsSizeLimit
const bundledReactChannel = useServerActions ? '-experimental' : ''

if (isClient) {
if (
Expand Down Expand Up @@ -2418,7 +2419,8 @@ export default async function getBaseWebpackConfig(
appDir,
dev,
isEdgeServer,
useServerActions: serverActions,
useServerActions: useServerActions,
shuding marked this conversation as resolved.
Show resolved Hide resolved
serverActionsSizeLimit: serverActionsSizeLimit,
})),
hasAppDir &&
!isClient &&
Expand Down
Expand Up @@ -30,12 +30,14 @@ import {
import { traverseModules, forEachEntryModule } from '../utils'
import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep'
import { getProxiedPluginState } from '../../build-context'
import { SizeLimit } from '../../../../types'

interface Options {
dev: boolean
appDir: string
isEdgeServer: boolean
useServerActions: boolean
serverActionsSizeLimit?: SizeLimit
}

const PLUGIN_NAME = 'ClientEntryPlugin'
Expand Down Expand Up @@ -151,13 +153,15 @@ export class ClientReferenceEntryPlugin {
appDir: string
isEdgeServer: boolean
useServerActions: boolean
serverActionsSizeLimit?: SizeLimit
assetPrefix: string

constructor(options: Options) {
this.dev = options.dev
this.appDir = options.appDir
this.isEdgeServer = options.isEdgeServer
this.useServerActions = options.useServerActions
this.serverActionsSizeLimit = options.serverActionsSizeLimit
this.assetPrefix = !this.dev && !this.isEdgeServer ? '../' : ''
}

Expand Down Expand Up @@ -341,6 +345,15 @@ export class ClientReferenceEntryPlugin {
'Server Actions require `experimental.serverActions` option to be enabled in your Next.js config: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions'
)
)
} else if (
this.serverActionsSizeLimit !== undefined &&
parseInt(this.serverActionsSizeLimit.toString()) < 1
) {
compilation.errors.push(
new Error(
'Server Actions Size Limit must exceed 1 in number or filesize format to be enabled in your Next.js config: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions'
)
)
shuding marked this conversation as resolved.
Show resolved Hide resolved
} else {
if (!actionMapsPerEntry[name]) {
actionMapsPerEntry[name] = new Map()
Expand Down Expand Up @@ -444,6 +457,15 @@ export class ClientReferenceEntryPlugin {
'Server Actions require `experimental.serverActions` option to be enabled in your Next.js config: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions'
)
)
} else if (
this.serverActionsSizeLimit !== undefined &&
parseInt(this.serverActionsSizeLimit.toString()) < 1
) {
compilation.errors.push(
new Error(
'Server Actions Size Limit must exceed 1 in number or filesize format to be enabled in your Next.js config: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions'
)
)
} else {
if (!actionMapsPerClientEntry[name]) {
actionMapsPerClientEntry[name] = new Map()
Expand Down
10 changes: 9 additions & 1 deletion packages/next/src/server/app-render/action-handler.ts
Expand Up @@ -27,6 +27,7 @@ import {
getModifiedCookieValues,
} from '../web/spec-extension/adapters/request-cookies'
import { RequestStore } from '../../client/components/request-async-storage'
import { NextConfigComplete } from '../../server/config-shared'

function nodeToWebReadableStream(nodeReadable: import('stream').Readable) {
if (process.env.NEXT_RUNTIME !== 'edge') {
Expand Down Expand Up @@ -245,6 +246,7 @@ export async function handleAction({
generateFlight,
staticGenerationStore,
requestStore,
nextConfig,
}: {
req: IncomingMessage
res: ServerResponse
Expand All @@ -258,6 +260,7 @@ export async function handleAction({
}) => Promise<RenderResult>
staticGenerationStore: StaticGenerationStore
requestStore: RequestStore
nextConfig?: NextConfigComplete
}): Promise<undefined | RenderResult | 'not-found'> {
let actionId = req.headers[ACTION.toLowerCase()] as string
const contentType = req.headers['content-type']
Expand Down Expand Up @@ -372,7 +375,12 @@ export async function handleAction({
} else {
const { parseBody } =
require('../api-utils/node') as typeof import('../api-utils/node')
const actionData = (await parseBody(req, '1mb')) || ''

const serverActionsSizeLimit =
nextConfig?.experimental?.serverActionsSizeLimit

const actionData =
(await parseBody(req, serverActionsSizeLimit ?? '1mb')) || ''

if (isURLEncodedAction) {
const formData = formDataFromSearchQueryString(actionData)
Expand Down
10 changes: 10 additions & 0 deletions packages/next/src/server/app-render/app-render.tsx
Expand Up @@ -72,6 +72,7 @@ import { handleAction } from './action-handler'
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../../shared/lib/lazy-dynamic/no-ssr-error'
import { warn } from '../../build/output/log'
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies'
import { PHASE_PRODUCTION_SERVER } from '../../shared/lib/constants'

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

Expand Down Expand Up @@ -1596,6 +1597,14 @@ export async function renderToHTMLOrFlight(
}
)

const nextConfig = await renderOpts.loadConfig?.(
PHASE_PRODUCTION_SERVER,
'/',
undefined,
undefined,
true
)

// For action requests, we handle them differently with a special render result.
const actionRequestResult = await handleAction({
req,
Expand All @@ -1606,6 +1615,7 @@ export async function renderToHTMLOrFlight(
generateFlight,
staticGenerationStore,
requestStore,
nextConfig,
})

if (actionRequestResult === 'not-found') {
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/server/app-render/types.ts
@@ -1,5 +1,6 @@
import type { LoadComponentsReturnType } from '../load-components'
import type { ServerRuntime } from '../../../types'
import { NextConfigComplete } from '../../server/config-shared'
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'

Expand Down Expand Up @@ -138,6 +139,13 @@ export type RenderOptsPartial = {
originalPathname?: string
isDraftMode?: boolean
deploymentId?: string
loadConfig?: (
phase: string,
dir: string,
customConfig?: object | null,
rawConfig?: boolean,
silent?: boolean
) => Promise<NextConfigComplete>
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
10 changes: 10 additions & 0 deletions packages/next/src/server/config-schema.ts
Expand Up @@ -304,6 +304,16 @@ const configSchema = {
serverActions: {
type: 'boolean',
},
serverActionsSizeLimit: {
oneOf: [
{
type: 'number',
},
{
type: 'string',
},
] as any,
},
extensionAlias: {
type: 'object',
},
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/server/config-shared.ts
Expand Up @@ -9,6 +9,7 @@ import {
import { SubresourceIntegrityAlgorithm } from '../build/webpack/plugins/subresource-integrity-plugin'
import { WEB_VITALS } from '../shared/lib/utils'
import type { NextParsedUrlQuery } from './request-meta'
import { SizeLimit } from '../../types'

export type NextConfigComplete = Required<NextConfig> & {
images: Required<ImageConfigComplete>
Expand Down Expand Up @@ -281,6 +282,11 @@ export interface ExperimentalConfig {
* Enable `react@experimental` channel for the `app` directory.
*/
serverActions?: boolean

/**
* Allows adjusting body parser size limit for server actions.
*/
serverActionsSizeLimit?: SizeLimit
devjiwonchoi marked this conversation as resolved.
Show resolved Hide resolved
}

export type ExportPathMap = {
Expand Down
80 changes: 80 additions & 0 deletions test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts
@@ -0,0 +1,80 @@
import { createNextDescribe } from 'e2e-utils'

createNextDescribe(
shuding marked this conversation as resolved.
Show resolved Hide resolved
'app-dir action size limit invalid config',
{
files: __dirname,
skipDeployment: true,
dependencies: {
react: 'latest',
'react-dom': 'latest',
'server-only': 'latest',
},
},
({ next, isNextStart }) => {
if (!isNextStart) {
it('skip test for dev mode', () => {})
return
}

beforeAll(async function () {
await next.stop()
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: true,
serverActionsSizeLimit: -3000,
},
}
`
)
try {
await next.build()
} catch {}
})

beforeAll(async function () {
await next.stop()
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: true,
serverActionsSizeLimit: 'testmb',
},
}
`
)
try {
await next.build()
} catch {}
})

beforeAll(async function () {
await next.stop()
await next.patchFile(
'next.config.js',
`
module.exports = {
experimental: {
serverActions: true,
serverActionsSizeLimit: '-3000mb',
},
}
`
)
try {
await next.build()
} catch {}
})

it('should error if serverActionsSizeLimit config is invalid', async function () {
expect(next.cliOutput).toContain(
'Server Actions Size Limit must exceed 1 in number or filesize format'
)
})
}
)