[PR #338] [MERGED] feat: support experimental.serverActions.bodySizeLimit from next.config #490

Closed
opened 2026-05-06 13:08:21 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/338
Author: @youanden
Created: 3/8/2026
Status: Merged
Merged: 3/8/2026
Merged by: @james-elicx

Base: mainHead: feat/body-size-limit


📝 Commits (3)

  • 88de1c3 feat: support experimental.serverActions.bodySizeLimit from next.config
  • 226f033 fix: address PR review feedback for parseBodySizeLimit
  • 48e5ab9 fix: warn on invalid bodySizeLimit string instead of silently falling back

📊 Changes

4 files changed (+158 additions, -5 deletions)

View changed files

📝 packages/vinext/src/config/next-config.ts (+43 -1)
📝 packages/vinext/src/index.ts (+1 -0)
📝 packages/vinext/src/server/app-dev-server.ts (+7 -3)
📝 tests/next-config.test.ts (+107 -1)

📄 Description

Adds support for reading experimental.serverActions.bodySizeLimit from next.config.ts and using it to configure the server action request body size limit. Currently, vinext hardcodes __MAX_ACTION_BODY_SIZE to 1MB (1 * 1024 * 1024), ignoring any user configuration and causing 413 Payload Too Large errors when uploading files >1MB via server actions.

// next.config.ts
export default {
  experimental: {
    serverActions: {
      bodySizeLimit: "10mb", // ❌ ignored by vinext — always 1MB
    },
  },
};

Next.js respects this setting (docs), but vinext's generated dev server entry hardcodes the limit.

Problem

I faced an issue uploading files above 1MB in dev mode that drove this PR's creation. I had created a patch in my repo beforehand to correct the problem locally:

// patches/vinext@0.0.24.patch
diff --git a/dist/config/next-config.js b/dist/config/next-config.js
index 3f049032fc986ad4561cb2674cac11bc244cc7b6..a781da763fa7ac2c10655df2a8a0b127bbc882b4 100644
--- a/dist/config/next-config.js
+++ b/dist/config/next-config.js
@@ -10,6 +10,27 @@ import { createRequire } from "node:module";
 import fs from "node:fs";
 import { PHASE_DEVELOPMENT_SERVER } from "../shims/constants.js";
 import { normalizePageExtensions } from "../routing/file-matcher.js";
+/**
+ * Parse a body size limit value (string or number) into bytes.
+ * Accepts Next.js-style strings like "1mb", "500kb", "10mb".
+ * Returns the default 1MB if the value is not provided or invalid.
+ */
+function parseBodySizeLimit(value) {
+    if (value === undefined || value === null) return 1 * 1024 * 1024;
+    if (typeof value === "number") return value;
+    if (typeof value !== "string") return 1 * 1024 * 1024;
+    const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i);
+    if (!match) return 1 * 1024 * 1024;
+    const num = parseFloat(match[1]);
+    const unit = match[2].toLowerCase();
+    switch (unit) {
+        case "b": return Math.floor(num);
+        case "kb": return Math.floor(num * 1024);
+        case "mb": return Math.floor(num * 1024 * 1024);
+        case "gb": return Math.floor(num * 1024 * 1024 * 1024);
+        default: return 1 * 1024 * 1024;
+    }
+}
 const CONFIG_FILES = [
     "next.config.ts",
     "next.config.mjs",
@@ -105,6 +126,7 @@ export async function resolveNextConfig(config) {
             i18n: null,
             mdx: null,
             serverActionsAllowedOrigins: [],
+            serverActionsBodySizeLimit: 1 * 1024 * 1024,
         };
     }
     // Resolve redirects
@@ -135,12 +157,13 @@ export async function resolveNextConfig(config) {
     }
     // Extract MDX remark/rehype plugins from @next/mdx's webpack wrapper
     const mdx = extractMdxOptions(config);
-    // Resolve serverActions.allowedOrigins from experimental config
+    // Resolve serverActions.allowedOrigins and bodySizeLimit from experimental config
     const experimental = config.experimental;
     const serverActionsConfig = experimental?.serverActions;
     const serverActionsAllowedOrigins = Array.isArray(serverActionsConfig?.allowedOrigins)
         ? serverActionsConfig.allowedOrigins
         : [];
+    const serverActionsBodySizeLimit = parseBodySizeLimit(serverActionsConfig?.bodySizeLimit);
     // Warn about unsupported options (skip webpack if we extracted MDX from it)
     const unsupported = mdx ? [] : ["webpack"];
     for (const key of unsupported) {
@@ -177,6 +200,7 @@ export async function resolveNextConfig(config) {
         i18n,
         mdx,
         serverActionsAllowedOrigins,
+        serverActionsBodySizeLimit,
     };
 }
 /**
diff --git a/dist/index.js b/dist/index.js
index 6659b4a0edab80f91a21547ae08c6340ee101c7a..a30439bc9a17fbb3905eeb0f95ddabd12d4981c2 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -2139,6 +2139,7 @@ hydrate();
                         headers: nextConfig?.headers,
                         allowedOrigins: nextConfig?.serverActionsAllowedOrigins,
                         allowedDevOrigins: nextConfig?.serverActionsAllowedOrigins,
+                        bodySizeLimit: nextConfig?.serverActionsBodySizeLimit,
                     });
                 }
                 if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) {
diff --git a/dist/server/app-dev-server.js b/dist/server/app-dev-server.js
index e76418758ec636d05f0ea14ee7fffc367bbf937c..8ec09db8e5a8dc4e74cbe69904fb534923a06fe8 100644
--- a/dist/server/app-dev-server.js
+++ b/dist/server/app-dev-server.js
@@ -25,6 +25,7 @@ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes,
     const rewrites = config?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] };
     const headers = config?.headers ?? [];
     const allowedOrigins = config?.allowedOrigins ?? [];
+    const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024;
     // Build import map for all page and layout files
     const imports = [];
     const importMap = new Map();
@@ -1112,12 +1113,13 @@ function __isExternalUrl(url) {
 }
 
 /**
- * Maximum server-action request body size (1 MB).
- * Matches the Next.js default for serverActions.bodySizeLimit.
+ * Maximum server-action request body size.
+ * Configurable via experimental.serverActions.bodySizeLimit in next.config.
+ * Defaults to 1MB, matching the Next.js default.
  * @see https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions#bodysizelimit
  * Prevents unbounded request body buffering.
  */
-var __MAX_ACTION_BODY_SIZE = 1 * 1024 * 1024;
+var __MAX_ACTION_BODY_SIZE = ${JSON.stringify(bodySizeLimit)};
 
 /**
  * Read a request body as text with a size limit.

Test plan

Added 14 unit tests covering string parsing (mb/kb/gb/b), numeric passthrough, case insensitivity, fractional values, and default/invalid fallbacks
Added integration tests verifying resolveNextConfig() end-to-end with experimental.serverActions.bodySizeLimit

All existing tests continue to pass, oxlint was clean, typecheck had 4 existing errors prior to my changes.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/338 **Author:** [@youanden](https://github.com/youanden) **Created:** 3/8/2026 **Status:** ✅ Merged **Merged:** 3/8/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `feat/body-size-limit` --- ### 📝 Commits (3) - [`88de1c3`](https://github.com/cloudflare/vinext/commit/88de1c3264b2bbf6a5dcb49e544067f6b3467864) feat: support experimental.serverActions.bodySizeLimit from next.config - [`226f033`](https://github.com/cloudflare/vinext/commit/226f033f4feca07a5fa388c651a605a5e9c0cc5c) fix: address PR review feedback for parseBodySizeLimit - [`48e5ab9`](https://github.com/cloudflare/vinext/commit/48e5ab982fe6016401a1f39afc86f310313b8abf) fix: warn on invalid bodySizeLimit string instead of silently falling back ### 📊 Changes **4 files changed** (+158 additions, -5 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/config/next-config.ts` (+43 -1) 📝 `packages/vinext/src/index.ts` (+1 -0) 📝 `packages/vinext/src/server/app-dev-server.ts` (+7 -3) 📝 `tests/next-config.test.ts` (+107 -1) </details> ### 📄 Description Adds support for reading experimental.serverActions.bodySizeLimit from next.config.ts and using it to configure the server action request body size limit. Currently, vinext hardcodes `__MAX_ACTION_BODY_SIZE` to 1MB (1 * 1024 * 1024), ignoring any user configuration and causing 413 Payload Too Large errors when uploading files >1MB via server actions. ```ts // next.config.ts export default { experimental: { serverActions: { bodySizeLimit: "10mb", // ❌ ignored by vinext — always 1MB }, }, }; ``` Next.js respects this setting ([docs](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions#bodysizelimit)), but vinext's generated dev server entry hardcodes the limit. ## Problem I faced an issue uploading files above 1MB in dev mode that drove this PR's creation. I had created a patch in my repo beforehand to correct the problem locally: ``` // patches/vinext@0.0.24.patch diff --git a/dist/config/next-config.js b/dist/config/next-config.js index 3f049032fc986ad4561cb2674cac11bc244cc7b6..a781da763fa7ac2c10655df2a8a0b127bbc882b4 100644 --- a/dist/config/next-config.js +++ b/dist/config/next-config.js @@ -10,6 +10,27 @@ import { createRequire } from "node:module"; import fs from "node:fs"; import { PHASE_DEVELOPMENT_SERVER } from "../shims/constants.js"; import { normalizePageExtensions } from "../routing/file-matcher.js"; +/** + * Parse a body size limit value (string or number) into bytes. + * Accepts Next.js-style strings like "1mb", "500kb", "10mb". + * Returns the default 1MB if the value is not provided or invalid. + */ +function parseBodySizeLimit(value) { + if (value === undefined || value === null) return 1 * 1024 * 1024; + if (typeof value === "number") return value; + if (typeof value !== "string") return 1 * 1024 * 1024; + const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i); + if (!match) return 1 * 1024 * 1024; + const num = parseFloat(match[1]); + const unit = match[2].toLowerCase(); + switch (unit) { + case "b": return Math.floor(num); + case "kb": return Math.floor(num * 1024); + case "mb": return Math.floor(num * 1024 * 1024); + case "gb": return Math.floor(num * 1024 * 1024 * 1024); + default: return 1 * 1024 * 1024; + } +} const CONFIG_FILES = [ "next.config.ts", "next.config.mjs", @@ -105,6 +126,7 @@ export async function resolveNextConfig(config) { i18n: null, mdx: null, serverActionsAllowedOrigins: [], + serverActionsBodySizeLimit: 1 * 1024 * 1024, }; } // Resolve redirects @@ -135,12 +157,13 @@ export async function resolveNextConfig(config) { } // Extract MDX remark/rehype plugins from @next/mdx's webpack wrapper const mdx = extractMdxOptions(config); - // Resolve serverActions.allowedOrigins from experimental config + // Resolve serverActions.allowedOrigins and bodySizeLimit from experimental config const experimental = config.experimental; const serverActionsConfig = experimental?.serverActions; const serverActionsAllowedOrigins = Array.isArray(serverActionsConfig?.allowedOrigins) ? serverActionsConfig.allowedOrigins : []; + const serverActionsBodySizeLimit = parseBodySizeLimit(serverActionsConfig?.bodySizeLimit); // Warn about unsupported options (skip webpack if we extracted MDX from it) const unsupported = mdx ? [] : ["webpack"]; for (const key of unsupported) { @@ -177,6 +200,7 @@ export async function resolveNextConfig(config) { i18n, mdx, serverActionsAllowedOrigins, + serverActionsBodySizeLimit, }; } /** diff --git a/dist/index.js b/dist/index.js index 6659b4a0edab80f91a21547ae08c6340ee101c7a..a30439bc9a17fbb3905eeb0f95ddabd12d4981c2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2139,6 +2139,7 @@ hydrate(); headers: nextConfig?.headers, allowedOrigins: nextConfig?.serverActionsAllowedOrigins, allowedDevOrigins: nextConfig?.serverActionsAllowedOrigins, + bodySizeLimit: nextConfig?.serverActionsBodySizeLimit, }); } if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) { diff --git a/dist/server/app-dev-server.js b/dist/server/app-dev-server.js index e76418758ec636d05f0ea14ee7fffc367bbf937c..8ec09db8e5a8dc4e74cbe69904fb534923a06fe8 100644 --- a/dist/server/app-dev-server.js +++ b/dist/server/app-dev-server.js @@ -25,6 +25,7 @@ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, const rewrites = config?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] }; const headers = config?.headers ?? []; const allowedOrigins = config?.allowedOrigins ?? []; + const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024; // Build import map for all page and layout files const imports = []; const importMap = new Map(); @@ -1112,12 +1113,13 @@ function __isExternalUrl(url) { } /** - * Maximum server-action request body size (1 MB). - * Matches the Next.js default for serverActions.bodySizeLimit. + * Maximum server-action request body size. + * Configurable via experimental.serverActions.bodySizeLimit in next.config. + * Defaults to 1MB, matching the Next.js default. * @see https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions#bodysizelimit * Prevents unbounded request body buffering. */ -var __MAX_ACTION_BODY_SIZE = 1 * 1024 * 1024; +var __MAX_ACTION_BODY_SIZE = ${JSON.stringify(bodySizeLimit)}; /** * Read a request body as text with a size limit. ``` ## Test plan Added 14 unit tests covering string parsing (mb/kb/gb/b), numeric passthrough, case insensitivity, fractional values, and default/invalid fallbacks Added integration tests verifying `resolveNextConfig()` end-to-end with `experimental.serverActions.bodySizeLimit` All existing tests continue to pass, oxlint was clean, typecheck had 4 existing errors prior to my changes. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:21 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#490
No description provided.