diff --git a/docs/02-app/01-building-your-application/03-data-fetching/04-server-actions.mdx b/docs/02-app/01-building-your-application/03-data-fetching/04-server-actions.mdx index 8e3d09a346720..8367bf20cfa02 100644 --- a/docs/02-app/01-building-your-application/03-data-fetching/04-server-actions.mdx +++ b/docs/02-app/01-building-your-application/03-data-fetching/04-server-actions.mdx @@ -382,6 +382,21 @@ export default function ExampleClientComponent({ myAction }) { In both cases, the form is interactive before hydration occurs. Although Server Actions have the additional benefit of not relying on client JavaScript, you can still compose additional behavior with Client Actions where desired without sacrificing interactivity. +### Size Limitation + +By default, the maximum size of the request body sent to an Server Action is 1MB. This prevents large amounts of data being sent to the server, which consumes a lot of server resource to parse. + +However, you can configure this limit using the **experimental** `serverActionsBodySizeLimit` option. It can take the number of bytes or any string format supported by bytes, for example `1000`, `'500kb'` or `'3mb'`. + +```js filename="next.config.js" +module.exports = { + experimental: { + serverActions: true, + serverActionsBodySizeLimit: '2mb', + }, +} +``` + ## Examples ### Usage with Client Components diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index cbde86a55fed8..9aa223b91debc 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -377,7 +377,8 @@ export function getEdgeServerEntry(opts: { middlewareConfig: Buffer.from( JSON.stringify(opts.middlewareConfig || {}) ).toString('base64'), - serverActionsSizeLimit: opts.config.experimental.serverActionsSizeLimit, + serverActionsBodySizeLimit: + opts.config.experimental.serverActionsBodySizeLimit, } return { diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts index 065fdd58248e7..e9b34ad67b17d 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts @@ -23,7 +23,7 @@ export type EdgeSSRLoaderQuery = { incrementalCacheHandlerPath?: string preferredRegion: string | string[] | undefined middlewareConfig: string - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit } /* @@ -57,7 +57,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = incrementalCacheHandlerPath, preferredRegion, middlewareConfig: middlewareConfigBase64, - serverActionsSizeLimit, + serverActionsBodySizeLimit, } = this.getOptions() const middlewareConfig: MiddlewareConfig = JSON.parse( @@ -177,10 +177,10 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = reactLoadableManifest, clientReferenceManifest: ${isServerComponent} ? rscManifest : null, serverActionsManifest: ${isServerComponent} ? rscServerManifest : null, - serverActionsSizeLimit: ${isServerComponent} ? ${ - typeof serverActionsSizeLimit === 'undefined' + serverActionsBodySizeLimit: ${isServerComponent} ? ${ + typeof serverActionsBodySizeLimit === 'undefined' ? 'undefined' - : JSON.stringify(serverActionsSizeLimit) + : JSON.stringify(serverActionsBodySizeLimit) } : undefined, subresourceIntegrityManifest, config: ${stringifiedConfig}, diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index 0dbd1b09ca14b..bb407509e8b23 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -32,7 +32,7 @@ export function getRender({ clientReferenceManifest, subresourceIntegrityManifest, serverActionsManifest, - serverActionsSizeLimit, + serverActionsBodySizeLimit, config, buildId, nextFontManifest, @@ -53,7 +53,7 @@ export function getRender({ subresourceIntegrityManifest?: Record clientReferenceManifest?: ClientReferenceManifest serverActionsManifest: any - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit appServerMod: any config: NextConfigComplete buildId: string @@ -88,7 +88,7 @@ export function getRender({ disableOptimizedLoading: true, clientReferenceManifest, serverActionsManifest, - serverActionsSizeLimit, + serverActionsBodySizeLimit, }, renderToHTML, incrementalCacheHandler, diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 503cc59576ba3..6676f3e2f7099 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -37,7 +37,7 @@ interface Options { appDir: string isEdgeServer: boolean useServerActions: boolean - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit } const PLUGIN_NAME = 'ClientEntryPlugin' @@ -153,7 +153,7 @@ export class ClientReferenceEntryPlugin { appDir: string isEdgeServer: boolean useServerActions: boolean - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit assetPrefix: string constructor(options: Options) { @@ -161,7 +161,7 @@ export class ClientReferenceEntryPlugin { this.appDir = options.appDir this.isEdgeServer = options.isEdgeServer this.useServerActions = options.useServerActions - this.serverActionsSizeLimit = options.serverActionsSizeLimit + this.serverActionsBodySizeLimit = options.serverActionsBodySizeLimit this.assetPrefix = !this.dev && !this.isEdgeServer ? '../' : '' } diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 3e47934f0f943..321455c2afa51 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -472,7 +472,8 @@ export default async function exportApp( largePageDataBytes: nextConfig.experimental.largePageDataBytes, serverComponents: options.hasAppDir, hasServerComponents: options.hasAppDir, - serverActionsSizeLimit: nextConfig.experimental.serverActionsSizeLimit, + serverActionsBodySizeLimit: + nextConfig.experimental.serverActionsBodySizeLimit, nextFontManifest: require(join( distDir, 'server', diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index ef9b68f4e6002..df631e1cd6f9d 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -6,6 +6,7 @@ import type { } from 'http' import type { WebNextRequest } from '../base-http/web' import type { SizeLimit } from '../../../types' +import type { ApiError } from '../api-utils' import { ACTION, @@ -246,7 +247,7 @@ export async function handleAction({ generateFlight, staticGenerationStore, requestStore, - serverActionsSizeLimit, + serverActionsBodySizeLimit, }: { req: IncomingMessage res: ServerResponse @@ -260,7 +261,7 @@ export async function handleAction({ }) => Promise staticGenerationStore: StaticGenerationStore requestStore: RequestStore - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit }): Promise { let actionId = req.headers[ACTION.toLowerCase()] as string const contentType = req.headers['content-type'] @@ -376,8 +377,20 @@ export async function handleAction({ const { parseBody } = require('../api-utils/node') as typeof import('../api-utils/node') - const actionData = - (await parseBody(req, serverActionsSizeLimit ?? '1mb')) || '' + let actionData + try { + actionData = + (await parseBody(req, serverActionsBodySizeLimit ?? '1mb')) || + '' + } catch (e: any) { + if (e && (e as ApiError).statusCode === 413) { + // Exceeded the size limit + e.message = + e.message + + '\nTo configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#size-limitation' + } + throw e + } if (isURLEncodedAction) { const formData = formDataFromSearchQueryString(actionData) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 6efdf7f4d691e..4caa4eccf0659 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -156,7 +156,7 @@ export async function renderToHTMLOrFlight( nextFontManifest, supportsDynamicHTML, nextConfigOutput, - serverActionsSizeLimit, + serverActionsBodySizeLimit, } = renderOpts const appUsingSizeAdjust = nextFontManifest?.appUsingSizeAdjust @@ -1607,7 +1607,7 @@ export async function renderToHTMLOrFlight( generateFlight, staticGenerationStore, requestStore, - serverActionsSizeLimit, + serverActionsBodySizeLimit, }) if (actionRequestResult === 'not-found') { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index eceb13a0ac1fa..b2118fe52ea57 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -146,7 +146,7 @@ export type RenderOptsPartial = { rawConfig?: boolean, silent?: boolean ) => Promise - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index b7f6dd76a9abf..4ef57fc721fb9 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -255,7 +255,7 @@ export default abstract class Server { supportsDynamicHTML?: boolean isBot?: boolean clientReferenceManifest?: ClientReferenceManifest - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit serverActionsManifest?: any nextFontManifest?: NextFontManifest renderServerComponentData?: boolean @@ -1651,8 +1651,8 @@ export default abstract class Server { incrementalCache, isRevalidate: isSSG, originalPathname: components.ComponentMod.originalPathname, - serverActionsSizeLimit: - this.nextConfig.experimental.serverActionsSizeLimit, + serverActionsBodySizeLimit: + this.nextConfig.experimental.serverActionsBodySizeLimit, } : {}), isDataReq, diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 91b906e2a5036..b52ad11987930 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -304,7 +304,7 @@ const configSchema = { serverActions: { type: 'boolean', }, - serverActionsSizeLimit: { + serverActionsBodySizeLimit: { oneOf: [ { type: 'number', diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 95083e609c294..aeed79b241ef8 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -286,7 +286,7 @@ export interface ExperimentalConfig { /** * Allows adjusting body parser size limit for server actions. */ - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit } export type ExportPathMap = { diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 19ee9ed28a695..6719f86197663 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -416,9 +416,9 @@ function assignDefaults( result.output = 'standalone' } - if (typeof result.experimental?.serverActionsSizeLimit !== 'undefined') { + if (typeof result.experimental?.serverActionsBodySizeLimit !== 'undefined') { const value = parseInt( - result.experimental.serverActionsSizeLimit.toString() + result.experimental.serverActionsBodySizeLimit.toString() ) if (isNaN(value) || value < 1) { throw new Error( diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index d8681ceb085c7..1e1ca4da11397 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -261,7 +261,7 @@ export type RenderOptsPartial = { isBot?: boolean runtime?: ServerRuntime serverComponents?: boolean - serverActionsSizeLimit?: SizeLimit + serverActionsBodySizeLimit?: SizeLimit customServer?: boolean crossOrigin?: 'anonymous' | 'use-credentials' | '' | undefined images: ImageConfigComplete diff --git a/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts b/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts index 83a4e5fd69478..a61e222c3872e 100644 --- a/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts +++ b/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts @@ -21,14 +21,14 @@ createNextDescribe( return } - it('should error if serverActionsSizeLimit config is a negative number', async function () { + it('should error if serverActionsBodySizeLimit config is a negative number', async function () { await next.patchFile( 'next.config.js', ` module.exports = { experimental: { serverActions: true, - serverActionsSizeLimit: -3000, + serverActionsBodySizeLimit: -3000, }, } ` @@ -40,14 +40,14 @@ createNextDescribe( expect(next.cliOutput).toContain(CONFIG_ERROR) }) - it('should error if serverActionsSizeLimit config is invalid', async function () { + it('should error if serverActionsBodySizeLimit config is invalid', async function () { await next.patchFile( 'next.config.js', ` module.exports = { experimental: { serverActions: true, - serverActionsSizeLimit: 'testmb', + serverActionsBodySizeLimit: 'testmb', }, } ` @@ -59,14 +59,14 @@ createNextDescribe( expect(next.cliOutput).toContain(CONFIG_ERROR) }) - it('should error if serverActionsSizeLimit config is a negative size', async function () { + it('should error if serverActionsBodySizeLimit config is a negative size', async function () { await next.patchFile( 'next.config.js', ` module.exports = { experimental: { serverActions: true, - serverActionsSizeLimit: '-3000mb', + serverActionsBodySizeLimit: '-3000mb', }, } ` @@ -79,14 +79,14 @@ createNextDescribe( }) if (!isNextDeploy) { - it('should respect the size set in serverActionsSizeLimit', async function () { + it('should respect the size set in serverActionsBodySizeLimit', async function () { await next.patchFile( 'next.config.js', ` module.exports = { experimental: { serverActions: true, - serverActionsSizeLimit: '1.5mb', + serverActionsBodySizeLimit: '1.5mb', }, } ` @@ -112,9 +112,11 @@ createNextDescribe( await browser.elementByCss('#size-2mb').click() await check(() => { - return logs.some((log) => - log.includes('Error: Body exceeded 1.5mb limit') - ) + const fullLog = logs.join('') + return fullLog.includes('Error: Body exceeded 1.5mb limit') && + fullLog.includes( + 'To configure the body size limit for Server Actions, see' + ) ? 'yes' : '' }, 'yes')