Skip to content

Commit

Permalink
feat: implement body limit using node transform stream in node runtimes
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Apr 18, 2024
1 parent 10d5c27 commit abce5cd
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 30 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"@swc/helpers": "0.5.5",
"@testing-library/jest-dom": "6.1.2",
"@testing-library/react": "13.0.0",
"@types/busboy": "1.5.3",
"@types/cheerio": "0.22.16",
"@types/cookie": "0.3.3",
"@types/cross-spawn": "6.0.0",
Expand Down
87 changes: 57 additions & 30 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ type ServerModuleMap = Record<
| undefined
>

type ServerActionsConfig = {
bodySizeLimit?: SizeLimit
allowedOrigins?: string[]
}

export async function handleAction({
req,
res,
Expand All @@ -371,10 +376,7 @@ export async function handleAction({
generateFlight: GenerateFlight
staticGenerationStore: StaticGenerationStore
requestStore: RequestStore
serverActions?: {
bodySizeLimit?: SizeLimit
allowedOrigins?: string[]
}
serverActions?: ServerActionsConfig
ctx: AppRenderContext
}): Promise<
| undefined
Expand Down Expand Up @@ -552,11 +554,12 @@ export async function handleAction({
) {
// Use react-server-dom-webpack/server.edge
const { decodeReply, decodeAction, decodeFormState } = ComponentMod

if (!req.body) {
throw new Error('invariant: Missing request body.')
}

// TODO: add body limit

if (isMultipartAction) {
// TODO-APP: Add streaming support
const formData = await req.request.formData()
Expand Down Expand Up @@ -619,18 +622,43 @@ export async function handleAction({
decodeFormState,
} = require(`./react-server.node`)

const { Transform } =
require('node:stream') as typeof import('node:stream')
const bodySizeLimit = serverActions?.bodySizeLimit
? (
require('next/dist/compiled/bytes') as typeof import('bytes')
).parse(serverActions.bodySizeLimit)
: 1024 * 1024 // 1 MB

let size = 0
const body = req.body.pipe(
new Transform({
transform(chunk, encoding, callback) {
size += Buffer.byteLength(chunk, encoding)
if (size > bodySizeLimit) {
const { ApiError } = require('../api-utils')
callback(
new ApiError(
413,
`Body exceeded ${
serverActions?.bodySizeLimit ?? '1 MB'
} limit.
To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`
)
)
return
}

callback(null, chunk)
},
})
)

if (isMultipartAction) {
if (isFetchAction) {
const readableLimit = serverActions?.bodySizeLimit ?? '1 MB'
const limit = require('next/dist/compiled/bytes').parse(
readableLimit
)
const busboy = require('busboy')
const bb = busboy({
headers: req.headers,
limits: { fieldSize: limit },
})
req.body.pipe(bb)
const busboy = require('busboy') as typeof import('busboy')
const bb = busboy({ headers: req.headers })
body.pipe(bb)

bound = await decodeReplyFromBusboy(bb, serverModuleMap)
} else {
Expand All @@ -640,7 +668,19 @@ export async function handleAction({
method: 'POST',
// @ts-expect-error
headers: { 'Content-Type': contentType },
body: req.stream(),
body: new ReadableStream({
start: (controller) => {
body.on('data', (chunk) => {
controller.enqueue(new Uint8Array(chunk))
})
body.on('end', () => {
controller.close()
})
body.on('error', (err) => {
controller.error(err)
})
},
}),
duplex: 'half',
})
const formData = await fakeRequest.formData()
Expand All @@ -667,26 +707,13 @@ export async function handleAction({
}
}

const chunks = []

const chunks: Buffer[] = []
for await (const chunk of req.body) {
chunks.push(Buffer.from(chunk))
}

const actionData = Buffer.concat(chunks).toString('utf-8')

const readableLimit = serverActions?.bodySizeLimit ?? '1 MB'
const limit = require('next/dist/compiled/bytes').parse(readableLimit)

if (actionData.length > limit) {
const { ApiError } = require('../api-utils')
throw new ApiError(
413,
`Body exceeded ${readableLimit} limit.
To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit`
)
}

if (isURLEncodedAction) {
const formData = formDataFromSearchQueryString(actionData)
bound = await decodeReply(formData, serverModuleMap)
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit abce5cd

Please # to comment.