-
Notifications
You must be signed in to change notification settings - Fork 26.1k
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
Enable progressive enhanced form actions through decodeAction #49187
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -171,7 +171,7 @@ | |
}): Promise<undefined | RenderResult | 'not-found'> { | ||
let actionId = req.headers[ACTION.toLowerCase()] as string | ||
const contentType = req.headers['content-type'] | ||
const isFormAction = | ||
const isURLEncodedAction = | ||
req.method === 'POST' && contentType === 'application/x-www-form-urlencoded' | ||
const isMultipartAction = | ||
req.method === 'POST' && contentType?.startsWith('multipart/form-data') | ||
|
@@ -181,7 +181,7 @@ | |
typeof actionId === 'string' && | ||
req.method === 'POST' | ||
|
||
if (isFetchAction || isFormAction || isMultipartAction) { | ||
if (isFetchAction || isURLEncodedAction || isMultipartAction) { | ||
let bound = [] | ||
|
||
const workerName = 'app' + pathname | ||
|
@@ -210,7 +210,7 @@ | |
await actionAsyncStorage.run({ isAction: true }, async () => { | ||
if (process.env.NEXT_RUNTIME === 'edge') { | ||
// Use react-server-dom-webpack/server.edge | ||
const { decodeReply } = ComponentMod | ||
const { decodeReply, decodeAction } = ComponentMod | ||
|
||
const webRequest = req as unknown as WebNextRequest | ||
if (!webRequest.body) { | ||
|
@@ -220,7 +220,14 @@ | |
if (isMultipartAction) { | ||
// TODO-APP: Add streaming support | ||
const formData = await webRequest.request.formData() | ||
bound = await decodeReply(formData, serverModuleMap) | ||
if (isFetchAction) { | ||
bound = await decodeReply(formData, serverModuleMap) | ||
} else { | ||
const action = await decodeAction(formData, serverModuleMap) | ||
await action() | ||
// Skip the fetch path | ||
return | ||
} | ||
} else { | ||
let actionData = '' | ||
|
||
|
@@ -234,16 +241,9 @@ | |
actionData += new TextDecoder().decode(value) | ||
} | ||
|
||
if (isFormAction) { | ||
if (isURLEncodedAction) { | ||
const formData = formDataFromSearchQueryString(actionData) | ||
actionId = formData.get('$$id') as string | ||
|
||
if (!actionId) { | ||
// Return if no action ID is found, it could be a regular POST request | ||
return | ||
} | ||
formData.delete('$$id') | ||
bound = [formData] | ||
bound = await decodeReply(formData, serverModuleMap) | ||
} else { | ||
bound = await decodeReply(actionData, serverModuleMap) | ||
} | ||
|
@@ -253,28 +253,41 @@ | |
const { | ||
decodeReply, | ||
decodeReplyFromBusboy, | ||
decodeAction, | ||
} = require(`react-server-dom-webpack/server.node`) | ||
|
||
if (isMultipartAction) { | ||
const busboy = require('busboy') | ||
const bb = busboy({ headers: req.headers }) | ||
req.pipe(bb) | ||
if (isFetchAction) { | ||
const busboy = require('busboy') | ||
const bb = busboy({ headers: req.headers }) | ||
req.pipe(bb) | ||
|
||
bound = await decodeReplyFromBusboy(bb, serverModuleMap) | ||
bound = await decodeReplyFromBusboy(bb, serverModuleMap) | ||
} else { | ||
// React doesn't yet publish a busboy version of decodeAction | ||
// so we polyfill the parsing of FormData. | ||
const { Readable } = require('stream') | ||
const UndiciRequest = require('undici').Request | ||
const fakeRequest = new UndiciRequest('http://localhost', { | ||
method: 'POST', | ||
headers: { 'Content-Type': req.headers['content-type'] }, | ||
body: Readable.toWeb(req), | ||
duplex: 'half', | ||
}) | ||
const formData = await fakeRequest.formData() | ||
const action = await decodeAction(formData, serverModuleMap) | ||
await action() | ||
// Skip the fetch path | ||
return | ||
} | ||
} else { | ||
const { parseBody } = | ||
require('../api-utils/node') as typeof import('../api-utils/node') | ||
const actionData = (await parseBody(req, '1mb')) || '' | ||
|
||
if (isFormAction) { | ||
actionId = actionData.$$id as string | ||
if (!actionId) { | ||
// Return if no action ID is found, it could be a regular POST request | ||
return | ||
} | ||
if (isURLEncodedAction) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't ever use URL encoded bodies atm but if we ever did, it'd be equivalent of passing formData. |
||
const formData = formDataFromSearchQueryString(actionData) | ||
formData.delete('$$id') | ||
bound = [formData] | ||
bound = await decodeReply(formData, serverModuleMap) | ||
} else { | ||
bound = await decodeReply(actionData, serverModuleMap) | ||
} | ||
|
@@ -302,9 +315,7 @@ | |
} | ||
}) | ||
|
||
if (actionResult) { | ||
return actionResult | ||
} | ||
return actionResult | ||
} catch (err) { | ||
if (isRedirectError(err)) { | ||
if (process.env.NEXT_RUNTIME === 'edge') { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,7 +80,10 @@ createNextDescribe( | |
}) | ||
|
||
it('should support notFound', async () => { | ||
const browser = await next.browser('/server') | ||
const browser = await next.browser('/server', { | ||
// TODO we should also test this with javascript on but not-found is not implemented yet. | ||
disableJavaScript: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably run all these tests with and without JS. |
||
}) | ||
|
||
await browser.elementByCss('#nowhere').click() | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an unfortunate hack. I'm not sure if there's a better way to get these imports.
I'll probably just add a
decodeActionFromBusboy
for Node.js in React longer term that polyfills this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For
Request
, this is better as we haveedge-runtime
imported already:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If use
undici
, we need to change it torequire('next/dist/compiled/undici')
(there's also a lint error for this).