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

[Fizz] Fallback to client replaying actions if we're trying to serialize a Blob #28987

Merged
merged 2 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -1019,12 +1019,7 @@ function pushAdditionalFormField(
): void {
const target: Array<Chunk | PrecomputedChunk> = this;
target.push(startHiddenInputChunk);
if (typeof value !== 'string') {
throw new Error(
'File/Blob fields are not yet supported in progressive forms. ' +
'It probably means you are closing over binary data or FormData in a Server Action.',
);
}
validateAdditionalFormField(value, key);
pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -1040,6 +1035,23 @@ function pushAdditionalFormFields(
}
}

function validateAdditionalFormField(value: string | File, key: string): void {
if (typeof value !== 'string') {
throw new Error(
'File/Blob fields are not yet supported in progressive forms. ' +
'Will fallback to client hydration.',
);
}
}

function validateAdditionalFormFields(formData: void | null | FormData) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}
return formData;
}

function getCustomFormFields(
resumableState: ResumableState,
formAction: any,
Expand All @@ -1048,7 +1060,11 @@ function getCustomFormFields(
if (typeof customAction === 'function') {
const prefix = makeFormFieldPrefix(resumableState);
try {
return formAction.$$FORM_ACTION(prefix);
const customFields = formAction.$$FORM_ACTION(prefix);
if (customFields) {
validateAdditionalFormFields(customFields.data);
}
return customFields;
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Rethrow suspense.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ global.ReadableStream =
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

// Polyfill stream methods on JSDOM.
global.Blob.prototype.stream = function () {
const impl = Object.getOwnPropertySymbols(this)[0];
const buffer = this[impl]._buffer;
return new ReadableStream({
start(c) {
c.enqueue(new Uint8Array(buffer));
c.close();
},
});
};

global.Blob.prototype.text = async function () {
const impl = Object.getOwnPropertySymbols(this)[0];
return this[impl]._buffer.toString('utf8');
};

// Don't wait before processing work on the server.
// TODO: we can replace this with FlightServer.act().
global.setTimeout = cb => cb();
Expand Down Expand Up @@ -962,4 +979,80 @@ describe('ReactFlightDOMForm', () => {
expect(form2.textContent).toBe('error message');
expect(form2.firstChild.tagName).toBe('DIV');
});

// @gate enableAsyncActions && enableBinaryFlight
it('useActionState can return binary state during MPA form submission', async () => {
const serverAction = serverExports(
async function action(prevState, formData) {
return new Blob([new Uint8Array([104, 105])]);
},
);

let blob;

function Form({action}) {
const [errorMsg, dispatch] = useActionState(action, null);
let text;
if (errorMsg) {
blob = errorMsg;
text = React.use(blob.text());
}
return <form action={dispatch}>{text}</form>;
}

const FormRef = await clientExports(Form);

const rscStream = ReactServerDOMServer.renderToReadableStream(
<FormRef action={serverAction} />,
webpackMap,
);
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
await readIntoContainer(ssrStream);

const form1 = container.getElementsByTagName('form')[0];
expect(form1.textContent).toBe('');

async function submitTheForm() {
const form = container.getElementsByTagName('form')[0];
const {formState} = await submit(form);

// Simulate an MPA form submission by resetting the container and
// rendering again.
container.innerHTML = '';

const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
{formState, root: <FormRef action={serverAction} />},
webpackMap,
);
const postbackResponse =
await ReactServerDOMClient.createFromReadableStream(postbackRscStream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
postbackResponse.root,
{formState: postbackResponse.formState},
);
await readIntoContainer(postbackSsrStream);
}

await expect(submitTheForm).toErrorDev(
'Warning: Failed to serialize an action for progressive enhancement:\n' +
'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.',
);

expect(blob instanceof Blob).toBe(true);
expect(blob.size).toBe(2);

const form2 = container.getElementsByTagName('form')[0];
expect(form2.textContent).toBe('hi');
});
});
2 changes: 1 addition & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@
"477": "React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React.",
"478": "Thenable should have already resolved. This is a bug in React.",
"479": "Cannot update optimistic state while rendering.",
"480": "File/Blob fields are not yet supported in progressive forms. It probably means you are closing over binary data or FormData in a Server Action.",
"480": "File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.",
"481": "Tried to encode a Server Action from a different instance than the encoder is from. This is a bug in React.",
"482": "async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.",
"483": "Hooks are not supported inside an async component. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.",
Expand Down