Skip to content

Commit

Permalink
[Fizz] Fallback to client replaying actions if we're trying to serial…
Browse files Browse the repository at this point in the history
…ize a Blob (#28987)

This follows the same principle as in #28611.

We cannot serialize Blobs of a form data into HTML because you can't
initialize a file input to some value. However the serialization of
state in an Action can contain blobs. In this case we do error but
outside the try/catch that recovers to error to client replaying instead
of MPA mode. This errors earlier to ensure that this works.

Testing this is a bit annoying because JSDOM doesn't have any of the
Blob methods but the Blob needs to be compatible with FormData and the
FormData needs to be compatible with `<form>` nodes in these tests. So I
polyfilled those in JSDOM with some hacks.

A possible future enhancement would be to encode these blobs in a base64
mode instead and have some way to receive them on the server. It's just
a matter of layering this. I think the RSC layer's `FORM_DATA`
implementation can pass some flag to encode as base64 and then have
decodeAction include some way to parse them. That way this case would
work in MPA mode too.

DiffTrain build for [6bac4f2](6bac4f2)
  • Loading branch information
sebmarkbage committed May 8, 2024
1 parent 7e3000e commit 86893d4
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 72 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5d29478716cefbf8290dfdd19129156c2ac75973
6bac4f2f31378cd58dffe6181e00639366a6081a
31 changes: 24 additions & 7 deletions compiled/facebook-www/ReactDOMServer-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require('react');
var ReactDOM = require('react-dom');

var ReactVersion = '19.0.0-www-classic-f3d84129';
var ReactVersion = '19.0.0-www-classic-b547cfd9';

// This refers to a WWW module.
var warningWWW = require('warning');
Expand Down Expand Up @@ -2572,11 +2572,7 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(value, key) {
var target = 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);
pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -2589,14 +2585,35 @@ function pushAdditionalFormFields(target, formData) {
}
}

function validateAdditionalFormField(value, key) {
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) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}

return formData;
}

function getCustomFormFields(resumableState, formAction) {
var customAction = formAction.$$FORM_ACTION;

if (typeof customAction === 'function') {
var prefix = makeFormFieldPrefix(resumableState);

try {
return formAction.$$FORM_ACTION(prefix);
var 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
31 changes: 24 additions & 7 deletions compiled/facebook-www/ReactDOMServer-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require('react');
var ReactDOM = require('react-dom');

var ReactVersion = '19.0.0-www-modern-f9bdfca0';
var ReactVersion = '19.0.0-www-modern-ce28692a';

// This refers to a WWW module.
var warningWWW = require('warning');
Expand Down Expand Up @@ -2572,11 +2572,7 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(value, key) {
var target = 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);
pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -2589,14 +2585,35 @@ function pushAdditionalFormFields(target, formData) {
}
}

function validateAdditionalFormField(value, key) {
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) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}

return formData;
}

function getCustomFormFields(resumableState, formAction) {
var customAction = formAction.$$FORM_ACTION;

if (typeof customAction === 'function') {
var prefix = makeFormFieldPrefix(resumableState);

try {
return formAction.$$FORM_ACTION(prefix);
var 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
54 changes: 31 additions & 23 deletions compiled/facebook-www/ReactDOMServer-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,17 +445,25 @@ var actionJavaScriptURL = escapeTextForBrowser(
);
function pushAdditionalFormField(value, key) {
this.push('<input type="hidden"');
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
validateAdditionalFormField(value);
pushStringAttribute(this, "name", key);
pushStringAttribute(this, "value", value);
this.push("/>");
}
function validateAdditionalFormField(value) {
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
}
function getCustomFormFields(resumableState, formAction) {
if ("function" === typeof formAction.$$FORM_ACTION) {
var id = resumableState.nextFormID++;
resumableState = resumableState.idPrefix + id;
try {
return formAction.$$FORM_ACTION(resumableState);
var customFields = formAction.$$FORM_ACTION(resumableState);
if (customFields) {
var formData = customFields.data;
null != formData && formData.forEach(validateAdditionalFormField);
}
return customFields;
} catch (x) {
if ("object" === typeof x && null !== x && "function" === typeof x.then)
throw x;
Expand Down Expand Up @@ -2627,16 +2635,16 @@ function createRenderState(resumableState, generateStaticMarkup) {
"\x3c/script>"
);
bootstrapScriptContent = idPrefix + "P:";
var JSCompiler_object_inline_segmentPrefix_1631 = idPrefix + "S:";
var JSCompiler_object_inline_segmentPrefix_1633 = idPrefix + "S:";
idPrefix += "B:";
var JSCompiler_object_inline_preconnects_1645 = new Set(),
JSCompiler_object_inline_fontPreloads_1646 = new Set(),
JSCompiler_object_inline_highImagePreloads_1647 = new Set(),
JSCompiler_object_inline_styles_1648 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1649 = new Set(),
JSCompiler_object_inline_scripts_1650 = new Set(),
JSCompiler_object_inline_bulkPreloads_1651 = new Set(),
JSCompiler_object_inline_preloads_1652 = {
var JSCompiler_object_inline_preconnects_1647 = new Set(),
JSCompiler_object_inline_fontPreloads_1648 = new Set(),
JSCompiler_object_inline_highImagePreloads_1649 = new Set(),
JSCompiler_object_inline_styles_1650 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1651 = new Set(),
JSCompiler_object_inline_scripts_1652 = new Set(),
JSCompiler_object_inline_bulkPreloads_1653 = new Set(),
JSCompiler_object_inline_preloads_1654 = {
images: new Map(),
stylesheets: new Map(),
scripts: new Map(),
Expand Down Expand Up @@ -2673,7 +2681,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
scriptConfig.moduleScriptResources[href] = null;
scriptConfig = [];
pushLinkImpl(scriptConfig, props);
JSCompiler_object_inline_bootstrapScripts_1649.add(scriptConfig);
JSCompiler_object_inline_bootstrapScripts_1651.add(scriptConfig);
bootstrapChunks.push('<script src="', escapeTextForBrowser(src));
"string" === typeof integrity &&
bootstrapChunks.push('" integrity="', escapeTextForBrowser(integrity));
Expand Down Expand Up @@ -2714,7 +2722,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
(props.moduleScriptResources[scriptConfig] = null),
(props = []),
pushLinkImpl(props, integrity),
JSCompiler_object_inline_bootstrapScripts_1649.add(props),
JSCompiler_object_inline_bootstrapScripts_1651.add(props),
bootstrapChunks.push(
'<script type="module" src="',
escapeTextForBrowser(i)
Expand All @@ -2729,7 +2737,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
bootstrapChunks.push('" async="">\x3c/script>');
return {
placeholderPrefix: bootstrapScriptContent,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1631,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1633,
boundaryPrefix: idPrefix,
startInlineScript: "<script>",
htmlChunks: null,
Expand All @@ -2749,14 +2757,14 @@ function createRenderState(resumableState, generateStaticMarkup) {
charsetChunks: [],
viewportChunks: [],
hoistableChunks: [],
preconnects: JSCompiler_object_inline_preconnects_1645,
fontPreloads: JSCompiler_object_inline_fontPreloads_1646,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1647,
styles: JSCompiler_object_inline_styles_1648,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1649,
scripts: JSCompiler_object_inline_scripts_1650,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1651,
preloads: JSCompiler_object_inline_preloads_1652,
preconnects: JSCompiler_object_inline_preconnects_1647,
fontPreloads: JSCompiler_object_inline_fontPreloads_1648,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1649,
styles: JSCompiler_object_inline_styles_1650,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1651,
scripts: JSCompiler_object_inline_scripts_1652,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1653,
preloads: JSCompiler_object_inline_preloads_1654,
stylesToHoist: !1,
generateStaticMarkup: generateStaticMarkup
};
Expand Down Expand Up @@ -5691,4 +5699,4 @@ exports.renderToString = function (children, options) {
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
);
};
exports.version = "19.0.0-www-classic-a976e819";
exports.version = "19.0.0-www-classic-444074d7";
54 changes: 31 additions & 23 deletions compiled/facebook-www/ReactDOMServer-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,17 +445,25 @@ var actionJavaScriptURL = escapeTextForBrowser(
);
function pushAdditionalFormField(value, key) {
this.push('<input type="hidden"');
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
validateAdditionalFormField(value);
pushStringAttribute(this, "name", key);
pushStringAttribute(this, "value", value);
this.push("/>");
}
function validateAdditionalFormField(value) {
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
}
function getCustomFormFields(resumableState, formAction) {
if ("function" === typeof formAction.$$FORM_ACTION) {
var id = resumableState.nextFormID++;
resumableState = resumableState.idPrefix + id;
try {
return formAction.$$FORM_ACTION(resumableState);
var customFields = formAction.$$FORM_ACTION(resumableState);
if (customFields) {
var formData = customFields.data;
null != formData && formData.forEach(validateAdditionalFormField);
}
return customFields;
} catch (x) {
if ("object" === typeof x && null !== x && "function" === typeof x.then)
throw x;
Expand Down Expand Up @@ -2627,16 +2635,16 @@ function createRenderState(resumableState, generateStaticMarkup) {
"\x3c/script>"
);
bootstrapScriptContent = idPrefix + "P:";
var JSCompiler_object_inline_segmentPrefix_1618 = idPrefix + "S:";
var JSCompiler_object_inline_segmentPrefix_1620 = idPrefix + "S:";
idPrefix += "B:";
var JSCompiler_object_inline_preconnects_1632 = new Set(),
JSCompiler_object_inline_fontPreloads_1633 = new Set(),
JSCompiler_object_inline_highImagePreloads_1634 = new Set(),
JSCompiler_object_inline_styles_1635 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1636 = new Set(),
JSCompiler_object_inline_scripts_1637 = new Set(),
JSCompiler_object_inline_bulkPreloads_1638 = new Set(),
JSCompiler_object_inline_preloads_1639 = {
var JSCompiler_object_inline_preconnects_1634 = new Set(),
JSCompiler_object_inline_fontPreloads_1635 = new Set(),
JSCompiler_object_inline_highImagePreloads_1636 = new Set(),
JSCompiler_object_inline_styles_1637 = new Map(),
JSCompiler_object_inline_bootstrapScripts_1638 = new Set(),
JSCompiler_object_inline_scripts_1639 = new Set(),
JSCompiler_object_inline_bulkPreloads_1640 = new Set(),
JSCompiler_object_inline_preloads_1641 = {
images: new Map(),
stylesheets: new Map(),
scripts: new Map(),
Expand Down Expand Up @@ -2673,7 +2681,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
scriptConfig.moduleScriptResources[href] = null;
scriptConfig = [];
pushLinkImpl(scriptConfig, props);
JSCompiler_object_inline_bootstrapScripts_1636.add(scriptConfig);
JSCompiler_object_inline_bootstrapScripts_1638.add(scriptConfig);
bootstrapChunks.push('<script src="', escapeTextForBrowser(src));
"string" === typeof integrity &&
bootstrapChunks.push('" integrity="', escapeTextForBrowser(integrity));
Expand Down Expand Up @@ -2714,7 +2722,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
(props.moduleScriptResources[scriptConfig] = null),
(props = []),
pushLinkImpl(props, integrity),
JSCompiler_object_inline_bootstrapScripts_1636.add(props),
JSCompiler_object_inline_bootstrapScripts_1638.add(props),
bootstrapChunks.push(
'<script type="module" src="',
escapeTextForBrowser(i)
Expand All @@ -2729,7 +2737,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
bootstrapChunks.push('" async="">\x3c/script>');
return {
placeholderPrefix: bootstrapScriptContent,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1618,
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1620,
boundaryPrefix: idPrefix,
startInlineScript: "<script>",
htmlChunks: null,
Expand All @@ -2749,14 +2757,14 @@ function createRenderState(resumableState, generateStaticMarkup) {
charsetChunks: [],
viewportChunks: [],
hoistableChunks: [],
preconnects: JSCompiler_object_inline_preconnects_1632,
fontPreloads: JSCompiler_object_inline_fontPreloads_1633,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1634,
styles: JSCompiler_object_inline_styles_1635,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1636,
scripts: JSCompiler_object_inline_scripts_1637,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1638,
preloads: JSCompiler_object_inline_preloads_1639,
preconnects: JSCompiler_object_inline_preconnects_1634,
fontPreloads: JSCompiler_object_inline_fontPreloads_1635,
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1636,
styles: JSCompiler_object_inline_styles_1637,
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1638,
scripts: JSCompiler_object_inline_scripts_1639,
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1640,
preloads: JSCompiler_object_inline_preloads_1641,
stylesToHoist: !1,
generateStaticMarkup: generateStaticMarkup
};
Expand Down Expand Up @@ -5669,4 +5677,4 @@ exports.renderToString = function (children, options) {
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
);
};
exports.version = "19.0.0-www-modern-54e820e3";
exports.version = "19.0.0-www-modern-df91acfc";
29 changes: 23 additions & 6 deletions compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -2569,11 +2569,7 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(value, key) {
var target = 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);
pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
Expand All @@ -2586,14 +2582,35 @@ function pushAdditionalFormFields(target, formData) {
}
}

function validateAdditionalFormField(value, key) {
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) {
if (formData != null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(validateAdditionalFormField);
}

return formData;
}

function getCustomFormFields(resumableState, formAction) {
var customAction = formAction.$$FORM_ACTION;

if (typeof customAction === 'function') {
var prefix = makeFormFieldPrefix(resumableState);

try {
return formAction.$$FORM_ACTION(prefix);
var 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

0 comments on commit 86893d4

Please sign in to comment.