Skip to content

Commit 86893d4

Browse files
committed
[Fizz] Fallback to client replaying actions if we're trying to serialize 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)
1 parent 7e3000e commit 86893d4

7 files changed

+147
-72
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5d29478716cefbf8290dfdd19129156c2ac75973
1+
6bac4f2f31378cd58dffe6181e00639366a6081a

compiled/facebook-www/ReactDOMServer-dev.classic.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require('react');
2020
var ReactDOM = require('react-dom');
2121

22-
var ReactVersion = '19.0.0-www-classic-f3d84129';
22+
var ReactVersion = '19.0.0-www-classic-b547cfd9';
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require('warning');
@@ -2572,11 +2572,7 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
25722572
function pushAdditionalFormField(value, key) {
25732573
var target = this;
25742574
target.push(startHiddenInputChunk);
2575-
2576-
if (typeof value !== 'string') {
2577-
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.');
2578-
}
2579-
2575+
validateAdditionalFormField(value);
25802576
pushStringAttribute(target, 'name', key);
25812577
pushStringAttribute(target, 'value', value);
25822578
target.push(endOfStartTagSelfClosing);
@@ -2589,14 +2585,35 @@ function pushAdditionalFormFields(target, formData) {
25892585
}
25902586
}
25912587

2588+
function validateAdditionalFormField(value, key) {
2589+
if (typeof value !== 'string') {
2590+
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'Will fallback to client hydration.');
2591+
}
2592+
}
2593+
2594+
function validateAdditionalFormFields(formData) {
2595+
if (formData != null) {
2596+
// $FlowFixMe[prop-missing]: FormData has forEach.
2597+
formData.forEach(validateAdditionalFormField);
2598+
}
2599+
2600+
return formData;
2601+
}
2602+
25922603
function getCustomFormFields(resumableState, formAction) {
25932604
var customAction = formAction.$$FORM_ACTION;
25942605

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

25982609
try {
2599-
return formAction.$$FORM_ACTION(prefix);
2610+
var customFields = formAction.$$FORM_ACTION(prefix);
2611+
2612+
if (customFields) {
2613+
validateAdditionalFormFields(customFields.data);
2614+
}
2615+
2616+
return customFields;
26002617
} catch (x) {
26012618
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
26022619
// Rethrow suspense.

compiled/facebook-www/ReactDOMServer-dev.modern.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require('react');
2020
var ReactDOM = require('react-dom');
2121

22-
var ReactVersion = '19.0.0-www-modern-f9bdfca0';
22+
var ReactVersion = '19.0.0-www-modern-ce28692a';
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require('warning');
@@ -2572,11 +2572,7 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
25722572
function pushAdditionalFormField(value, key) {
25732573
var target = this;
25742574
target.push(startHiddenInputChunk);
2575-
2576-
if (typeof value !== 'string') {
2577-
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.');
2578-
}
2579-
2575+
validateAdditionalFormField(value);
25802576
pushStringAttribute(target, 'name', key);
25812577
pushStringAttribute(target, 'value', value);
25822578
target.push(endOfStartTagSelfClosing);
@@ -2589,14 +2585,35 @@ function pushAdditionalFormFields(target, formData) {
25892585
}
25902586
}
25912587

2588+
function validateAdditionalFormField(value, key) {
2589+
if (typeof value !== 'string') {
2590+
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'Will fallback to client hydration.');
2591+
}
2592+
}
2593+
2594+
function validateAdditionalFormFields(formData) {
2595+
if (formData != null) {
2596+
// $FlowFixMe[prop-missing]: FormData has forEach.
2597+
formData.forEach(validateAdditionalFormField);
2598+
}
2599+
2600+
return formData;
2601+
}
2602+
25922603
function getCustomFormFields(resumableState, formAction) {
25932604
var customAction = formAction.$$FORM_ACTION;
25942605

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

25982609
try {
2599-
return formAction.$$FORM_ACTION(prefix);
2610+
var customFields = formAction.$$FORM_ACTION(prefix);
2611+
2612+
if (customFields) {
2613+
validateAdditionalFormFields(customFields.data);
2614+
}
2615+
2616+
return customFields;
26002617
} catch (x) {
26012618
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
26022619
// Rethrow suspense.

compiled/facebook-www/ReactDOMServer-prod.classic.js

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -445,17 +445,25 @@ var actionJavaScriptURL = escapeTextForBrowser(
445445
);
446446
function pushAdditionalFormField(value, key) {
447447
this.push('<input type="hidden"');
448-
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
448+
validateAdditionalFormField(value);
449449
pushStringAttribute(this, "name", key);
450450
pushStringAttribute(this, "value", value);
451451
this.push("/>");
452452
}
453+
function validateAdditionalFormField(value) {
454+
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
455+
}
453456
function getCustomFormFields(resumableState, formAction) {
454457
if ("function" === typeof formAction.$$FORM_ACTION) {
455458
var id = resumableState.nextFormID++;
456459
resumableState = resumableState.idPrefix + id;
457460
try {
458-
return formAction.$$FORM_ACTION(resumableState);
461+
var customFields = formAction.$$FORM_ACTION(resumableState);
462+
if (customFields) {
463+
var formData = customFields.data;
464+
null != formData && formData.forEach(validateAdditionalFormField);
465+
}
466+
return customFields;
459467
} catch (x) {
460468
if ("object" === typeof x && null !== x && "function" === typeof x.then)
461469
throw x;
@@ -2627,16 +2635,16 @@ function createRenderState(resumableState, generateStaticMarkup) {
26272635
"\x3c/script>"
26282636
);
26292637
bootstrapScriptContent = idPrefix + "P:";
2630-
var JSCompiler_object_inline_segmentPrefix_1631 = idPrefix + "S:";
2638+
var JSCompiler_object_inline_segmentPrefix_1633 = idPrefix + "S:";
26312639
idPrefix += "B:";
2632-
var JSCompiler_object_inline_preconnects_1645 = new Set(),
2633-
JSCompiler_object_inline_fontPreloads_1646 = new Set(),
2634-
JSCompiler_object_inline_highImagePreloads_1647 = new Set(),
2635-
JSCompiler_object_inline_styles_1648 = new Map(),
2636-
JSCompiler_object_inline_bootstrapScripts_1649 = new Set(),
2637-
JSCompiler_object_inline_scripts_1650 = new Set(),
2638-
JSCompiler_object_inline_bulkPreloads_1651 = new Set(),
2639-
JSCompiler_object_inline_preloads_1652 = {
2640+
var JSCompiler_object_inline_preconnects_1647 = new Set(),
2641+
JSCompiler_object_inline_fontPreloads_1648 = new Set(),
2642+
JSCompiler_object_inline_highImagePreloads_1649 = new Set(),
2643+
JSCompiler_object_inline_styles_1650 = new Map(),
2644+
JSCompiler_object_inline_bootstrapScripts_1651 = new Set(),
2645+
JSCompiler_object_inline_scripts_1652 = new Set(),
2646+
JSCompiler_object_inline_bulkPreloads_1653 = new Set(),
2647+
JSCompiler_object_inline_preloads_1654 = {
26402648
images: new Map(),
26412649
stylesheets: new Map(),
26422650
scripts: new Map(),
@@ -2673,7 +2681,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
26732681
scriptConfig.moduleScriptResources[href] = null;
26742682
scriptConfig = [];
26752683
pushLinkImpl(scriptConfig, props);
2676-
JSCompiler_object_inline_bootstrapScripts_1649.add(scriptConfig);
2684+
JSCompiler_object_inline_bootstrapScripts_1651.add(scriptConfig);
26772685
bootstrapChunks.push('<script src="', escapeTextForBrowser(src));
26782686
"string" === typeof integrity &&
26792687
bootstrapChunks.push('" integrity="', escapeTextForBrowser(integrity));
@@ -2714,7 +2722,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
27142722
(props.moduleScriptResources[scriptConfig] = null),
27152723
(props = []),
27162724
pushLinkImpl(props, integrity),
2717-
JSCompiler_object_inline_bootstrapScripts_1649.add(props),
2725+
JSCompiler_object_inline_bootstrapScripts_1651.add(props),
27182726
bootstrapChunks.push(
27192727
'<script type="module" src="',
27202728
escapeTextForBrowser(i)
@@ -2729,7 +2737,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
27292737
bootstrapChunks.push('" async="">\x3c/script>');
27302738
return {
27312739
placeholderPrefix: bootstrapScriptContent,
2732-
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1631,
2740+
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1633,
27332741
boundaryPrefix: idPrefix,
27342742
startInlineScript: "<script>",
27352743
htmlChunks: null,
@@ -2749,14 +2757,14 @@ function createRenderState(resumableState, generateStaticMarkup) {
27492757
charsetChunks: [],
27502758
viewportChunks: [],
27512759
hoistableChunks: [],
2752-
preconnects: JSCompiler_object_inline_preconnects_1645,
2753-
fontPreloads: JSCompiler_object_inline_fontPreloads_1646,
2754-
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1647,
2755-
styles: JSCompiler_object_inline_styles_1648,
2756-
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1649,
2757-
scripts: JSCompiler_object_inline_scripts_1650,
2758-
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1651,
2759-
preloads: JSCompiler_object_inline_preloads_1652,
2760+
preconnects: JSCompiler_object_inline_preconnects_1647,
2761+
fontPreloads: JSCompiler_object_inline_fontPreloads_1648,
2762+
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1649,
2763+
styles: JSCompiler_object_inline_styles_1650,
2764+
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1651,
2765+
scripts: JSCompiler_object_inline_scripts_1652,
2766+
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1653,
2767+
preloads: JSCompiler_object_inline_preloads_1654,
27602768
stylesToHoist: !1,
27612769
generateStaticMarkup: generateStaticMarkup
27622770
};
@@ -5691,4 +5699,4 @@ exports.renderToString = function (children, options) {
56915699
'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'
56925700
);
56935701
};
5694-
exports.version = "19.0.0-www-classic-a976e819";
5702+
exports.version = "19.0.0-www-classic-444074d7";

compiled/facebook-www/ReactDOMServer-prod.modern.js

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -445,17 +445,25 @@ var actionJavaScriptURL = escapeTextForBrowser(
445445
);
446446
function pushAdditionalFormField(value, key) {
447447
this.push('<input type="hidden"');
448-
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
448+
validateAdditionalFormField(value);
449449
pushStringAttribute(this, "name", key);
450450
pushStringAttribute(this, "value", value);
451451
this.push("/>");
452452
}
453+
function validateAdditionalFormField(value) {
454+
if ("string" !== typeof value) throw Error(formatProdErrorMessage(480));
455+
}
453456
function getCustomFormFields(resumableState, formAction) {
454457
if ("function" === typeof formAction.$$FORM_ACTION) {
455458
var id = resumableState.nextFormID++;
456459
resumableState = resumableState.idPrefix + id;
457460
try {
458-
return formAction.$$FORM_ACTION(resumableState);
461+
var customFields = formAction.$$FORM_ACTION(resumableState);
462+
if (customFields) {
463+
var formData = customFields.data;
464+
null != formData && formData.forEach(validateAdditionalFormField);
465+
}
466+
return customFields;
459467
} catch (x) {
460468
if ("object" === typeof x && null !== x && "function" === typeof x.then)
461469
throw x;
@@ -2627,16 +2635,16 @@ function createRenderState(resumableState, generateStaticMarkup) {
26272635
"\x3c/script>"
26282636
);
26292637
bootstrapScriptContent = idPrefix + "P:";
2630-
var JSCompiler_object_inline_segmentPrefix_1618 = idPrefix + "S:";
2638+
var JSCompiler_object_inline_segmentPrefix_1620 = idPrefix + "S:";
26312639
idPrefix += "B:";
2632-
var JSCompiler_object_inline_preconnects_1632 = new Set(),
2633-
JSCompiler_object_inline_fontPreloads_1633 = new Set(),
2634-
JSCompiler_object_inline_highImagePreloads_1634 = new Set(),
2635-
JSCompiler_object_inline_styles_1635 = new Map(),
2636-
JSCompiler_object_inline_bootstrapScripts_1636 = new Set(),
2637-
JSCompiler_object_inline_scripts_1637 = new Set(),
2638-
JSCompiler_object_inline_bulkPreloads_1638 = new Set(),
2639-
JSCompiler_object_inline_preloads_1639 = {
2640+
var JSCompiler_object_inline_preconnects_1634 = new Set(),
2641+
JSCompiler_object_inline_fontPreloads_1635 = new Set(),
2642+
JSCompiler_object_inline_highImagePreloads_1636 = new Set(),
2643+
JSCompiler_object_inline_styles_1637 = new Map(),
2644+
JSCompiler_object_inline_bootstrapScripts_1638 = new Set(),
2645+
JSCompiler_object_inline_scripts_1639 = new Set(),
2646+
JSCompiler_object_inline_bulkPreloads_1640 = new Set(),
2647+
JSCompiler_object_inline_preloads_1641 = {
26402648
images: new Map(),
26412649
stylesheets: new Map(),
26422650
scripts: new Map(),
@@ -2673,7 +2681,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
26732681
scriptConfig.moduleScriptResources[href] = null;
26742682
scriptConfig = [];
26752683
pushLinkImpl(scriptConfig, props);
2676-
JSCompiler_object_inline_bootstrapScripts_1636.add(scriptConfig);
2684+
JSCompiler_object_inline_bootstrapScripts_1638.add(scriptConfig);
26772685
bootstrapChunks.push('<script src="', escapeTextForBrowser(src));
26782686
"string" === typeof integrity &&
26792687
bootstrapChunks.push('" integrity="', escapeTextForBrowser(integrity));
@@ -2714,7 +2722,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
27142722
(props.moduleScriptResources[scriptConfig] = null),
27152723
(props = []),
27162724
pushLinkImpl(props, integrity),
2717-
JSCompiler_object_inline_bootstrapScripts_1636.add(props),
2725+
JSCompiler_object_inline_bootstrapScripts_1638.add(props),
27182726
bootstrapChunks.push(
27192727
'<script type="module" src="',
27202728
escapeTextForBrowser(i)
@@ -2729,7 +2737,7 @@ function createRenderState(resumableState, generateStaticMarkup) {
27292737
bootstrapChunks.push('" async="">\x3c/script>');
27302738
return {
27312739
placeholderPrefix: bootstrapScriptContent,
2732-
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1618,
2740+
segmentPrefix: JSCompiler_object_inline_segmentPrefix_1620,
27332741
boundaryPrefix: idPrefix,
27342742
startInlineScript: "<script>",
27352743
htmlChunks: null,
@@ -2749,14 +2757,14 @@ function createRenderState(resumableState, generateStaticMarkup) {
27492757
charsetChunks: [],
27502758
viewportChunks: [],
27512759
hoistableChunks: [],
2752-
preconnects: JSCompiler_object_inline_preconnects_1632,
2753-
fontPreloads: JSCompiler_object_inline_fontPreloads_1633,
2754-
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1634,
2755-
styles: JSCompiler_object_inline_styles_1635,
2756-
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1636,
2757-
scripts: JSCompiler_object_inline_scripts_1637,
2758-
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1638,
2759-
preloads: JSCompiler_object_inline_preloads_1639,
2760+
preconnects: JSCompiler_object_inline_preconnects_1634,
2761+
fontPreloads: JSCompiler_object_inline_fontPreloads_1635,
2762+
highImagePreloads: JSCompiler_object_inline_highImagePreloads_1636,
2763+
styles: JSCompiler_object_inline_styles_1637,
2764+
bootstrapScripts: JSCompiler_object_inline_bootstrapScripts_1638,
2765+
scripts: JSCompiler_object_inline_scripts_1639,
2766+
bulkPreloads: JSCompiler_object_inline_bulkPreloads_1640,
2767+
preloads: JSCompiler_object_inline_preloads_1641,
27602768
stylesToHoist: !1,
27612769
generateStaticMarkup: generateStaticMarkup
27622770
};
@@ -5669,4 +5677,4 @@ exports.renderToString = function (children, options) {
56695677
'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'
56705678
);
56715679
};
5672-
exports.version = "19.0.0-www-modern-54e820e3";
5680+
exports.version = "19.0.0-www-modern-df91acfc";

compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,11 +2569,7 @@ var startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
25692569
function pushAdditionalFormField(value, key) {
25702570
var target = this;
25712571
target.push(startHiddenInputChunk);
2572-
2573-
if (typeof value !== 'string') {
2574-
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.');
2575-
}
2576-
2572+
validateAdditionalFormField(value);
25772573
pushStringAttribute(target, 'name', key);
25782574
pushStringAttribute(target, 'value', value);
25792575
target.push(endOfStartTagSelfClosing);
@@ -2586,14 +2582,35 @@ function pushAdditionalFormFields(target, formData) {
25862582
}
25872583
}
25882584

2585+
function validateAdditionalFormField(value, key) {
2586+
if (typeof value !== 'string') {
2587+
throw new Error('File/Blob fields are not yet supported in progressive forms. ' + 'Will fallback to client hydration.');
2588+
}
2589+
}
2590+
2591+
function validateAdditionalFormFields(formData) {
2592+
if (formData != null) {
2593+
// $FlowFixMe[prop-missing]: FormData has forEach.
2594+
formData.forEach(validateAdditionalFormField);
2595+
}
2596+
2597+
return formData;
2598+
}
2599+
25892600
function getCustomFormFields(resumableState, formAction) {
25902601
var customAction = formAction.$$FORM_ACTION;
25912602

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

25952606
try {
2596-
return formAction.$$FORM_ACTION(prefix);
2607+
var customFields = formAction.$$FORM_ACTION(prefix);
2608+
2609+
if (customFields) {
2610+
validateAdditionalFormFields(customFields.data);
2611+
}
2612+
2613+
return customFields;
25972614
} catch (x) {
25982615
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
25992616
// Rethrow suspense.

0 commit comments

Comments
 (0)