1212
1313let webpackServerMap ;
1414let ReactServerDOMServer ;
15- let Stream ;
16- let serverAct ;
17- let Busboy ;
18- let Request ;
15+ let EventEmitter ;
1916
2017describe ( 'ReactFlightDOMReplyNode' , ( ) => {
2118 beforeEach ( ( ) => {
2219 jest . resetModules ( ) ;
2320
24- serverAct = require ( 'internal-test-utils' ) . serverAct ;
25-
2621 jest . mock ( 'react' , ( ) => require ( 'react/react.react-server' ) ) ;
2722 jest . mock ( 'react-server-dom-webpack/server' , ( ) =>
2823 require ( 'react-server-dom-webpack/server.node' ) ,
2924 ) ;
3025 const WebpackMock = require ( './utils/WebpackMock' ) ;
3126 webpackServerMap = WebpackMock . webpackServerMap ;
3227 ReactServerDOMServer = require ( 'react-server-dom-webpack/server.node' ) ;
33- Stream = require ( 'stream' ) ;
34- Busboy = require ( 'busboy' ) ;
35- Request = require ( 'undici' ) . Request ;
28+ EventEmitter = require ( 'events' ) ;
3629 } ) ;
3730
38- async function createBusboyStreamFromFormData ( formData ) {
39- const request = new Request ( 'http://localhost' , {
40- method : 'POST' ,
41- body : formData ,
42- } ) ;
43- const contentType = request . headers . get ( 'content-type' ) ;
44- const arrayBuffer = await request . arrayBuffer ( ) ;
45- const buffer = Buffer . from ( arrayBuffer ) ;
46- const stream = new Stream . Readable ( ) ;
47- stream . push ( buffer ) ;
48- stream . push ( null ) ;
49- const busboy = Busboy ( { headers : { 'content-type' : contentType } } ) ;
50-
51- return { busboy, stream} ;
31+ function createMockBusboy ( ) {
32+ const emitter = new EventEmitter ( ) ;
33+ let destroyed = false ;
34+
35+ const busboy = {
36+ emitField ( name , value ) {
37+ if ( ! destroyed ) {
38+ emitter . emit ( 'field' , name , value ) ;
39+ }
40+ } ,
41+ finish ( ) {
42+ if ( ! destroyed ) {
43+ emitter . emit ( 'finish' ) ;
44+ }
45+ } ,
46+ on ( event , handler ) {
47+ emitter . on ( event , handler ) ;
48+ return busboy ;
49+ } ,
50+ destroy ( error ) {
51+ destroyed = true ;
52+ if ( error ) {
53+ process . nextTick ( ( ) => emitter . emit ( 'error' , error ) ) ;
54+ }
55+ } ,
56+ } ;
57+
58+ return busboy ;
5259 }
5360
5461 describe ( 'decodeReplyFromBusboy' , ( ) => {
5562 it ( 'should error when a stream chunk references itself' , async ( ) => {
56- const formData = new FormData ( ) ;
57- formData . append ( '0' , '["$R0"]' ) ;
58- const { busboy, stream} = await createBusboyStreamFromFormData ( formData ) ;
59-
60- await expect (
61- serverAct ( ( ) => {
62- const reply = ReactServerDOMServer . decodeReplyFromBusboy (
63- busboy ,
64- webpackServerMap ,
65- ) ;
66- stream . pipe ( busboy ) ;
67- return reply ;
68- } ) ,
69- ) . rejects . toThrow ( 'Already initialized stream.' ) ;
63+ const busboy = createMockBusboy ( ) ;
64+
65+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
66+ busboy ,
67+ webpackServerMap ,
68+ ) ;
69+
70+ busboy . emitField ( '0' , '["$R0"]' ) ;
71+ busboy . finish ( ) ;
72+
73+ await expect ( reply ) . rejects . toThrow ( 'Already initialized stream.' ) ;
7074 } ) ;
7175
7276 it ( 'should not hang when self-referencing promise is referenced' , async ( ) => {
73- const formData = new FormData ( ) ;
74- formData . append ( '0' , '{"a":"$1:"}' ) ;
75- formData . append ( '1' , '"$@1"' ) ;
76- const { busboy, stream} = await createBusboyStreamFromFormData ( formData ) ;
77-
78- await expect (
79- serverAct ( ( ) => {
80- const reply = ReactServerDOMServer . decodeReplyFromBusboy (
81- busboy ,
82- webpackServerMap ,
83- ) ;
84- stream . pipe ( busboy ) ;
85- return reply ;
86- } ) ,
87- ) . rejects . toThrow ( 'Invalid reference.' ) ;
77+ const busboy = createMockBusboy ( ) ;
78+
79+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
80+ busboy ,
81+ webpackServerMap ,
82+ ) ;
83+
84+ busboy . emitField ( '0' , '{"a":"$1:"}' ) ;
85+ busboy . emitField ( '1' , '"$@1"' ) ;
86+ busboy . finish ( ) ;
87+
88+ await expect ( reply ) . rejects . toThrow ( 'Invalid reference.' ) ;
8889 } ) ;
8990
9091 it . failing ( 'should not hang with direct forward references' , async ( ) => {
91- const formData = new FormData ( ) ;
9292 const n = 3 ; // TODO: Increase this to trigger the hanging.
93+ const busboy = createMockBusboy ( ) ;
94+
95+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
96+ busboy ,
97+ webpackServerMap ,
98+ ) ;
99+
100+ // Call .then synchronously before fields have been emitted.
101+ const promise = new Promise ( ( resolve , reject ) =>
102+ reply . then ( resolve , reject ) ,
103+ ) ;
104+
93105 for ( let i = 0 ; i < n ; i ++ ) {
94- formData . append (
106+ busboy . emitField (
95107 String ( i ) ,
96108 JSON . stringify ( [
97109 `$${ ( i + 1 ) . toString ( 16 ) } ` ,
@@ -100,63 +112,71 @@ describe('ReactFlightDOMReplyNode', () => {
100112 ] ) ,
101113 ) ;
102114 }
103- formData . append ( String ( n ) , JSON . stringify ( '' ) ) ;
104-
105- const { busboy, stream} = await createBusboyStreamFromFormData ( formData ) ;
106-
107- await expect (
108- // The hang won't reproduce if the callback for serverAct is not async.
109- // Otherwise .then would be called on the ReactPromise returned from
110- // decodeReplyFromBusboy and resolve synchronously.
111- serverAct ( async ( ) => {
112- const reply = ReactServerDOMServer . decodeReplyFromBusboy (
113- busboy ,
114- webpackServerMap ,
115- ) ;
116- stream . pipe ( busboy ) ;
117- return reply ;
118- } ) ,
119- ) . rejects . toThrow ( 'Maximum array nesting exceeded' ) ;
115+ busboy . emitField ( String ( n ) , JSON . stringify ( '' ) ) ;
116+ busboy . finish ( ) ;
117+
118+ await expect ( promise ) . rejects . toThrow ( 'Maximum array nesting exceeded' ) ;
119+ } ) ;
120+
121+ it ( 'should be able to resolve cyclic references in root models' , async ( ) => {
122+ const busboy = createMockBusboy ( ) ;
123+
124+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
125+ busboy ,
126+ webpackServerMap ,
127+ ) ;
128+
129+ busboy . emitField ( '0' , '["$1"]' ) ;
130+ busboy . emitField ( '1' , '"$0"' ) ;
131+ busboy . finish ( ) ;
132+ const result = await reply ;
133+
134+ expect ( result ) . toBeInstanceOf ( Array ) ;
135+ expect ( result [ 0 ] ) . toBe ( result ) ;
120136 } ) ;
121137
122138 it . failing (
123- 'should be able to resolve cyclic references in root models' ,
139+ 'should be able to resolve cyclic references in root models when calling .then synchronously ' ,
124140 async ( ) => {
125- const formData = new FormData ( ) ;
126- formData . append ( '0' , '["$1"]' ) ;
127- formData . append ( '1' , '"$0"' ) ;
128-
129- const { busboy, stream} = await createBusboyStreamFromFormData ( formData ) ;
130-
131- const result = await serverAct ( async ( ) => {
132- const reply = ReactServerDOMServer . decodeReplyFromBusboy (
133- busboy ,
134- webpackServerMap ,
135- ) ;
136- stream . pipe ( busboy ) ;
137- return reply ;
138- } ) ;
139-
140- expect ( result ) . toBeInstanceOf ( Array ) ;
141- expect ( result [ 0 ] ) . toBe ( result ) ;
141+ const busboy = createMockBusboy ( ) ;
142+
143+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
144+ busboy ,
145+ webpackServerMap ,
146+ ) ;
147+
148+ // Call .then synchronously before fields have been emitted.
149+ const promise = new Promise ( ( resolve , reject ) =>
150+ reply . then ( result => {
151+ try {
152+ expect ( result ) . toBeInstanceOf ( Array ) ;
153+ expect ( result [ 0 ] ) . toBe ( result ) ;
154+ resolve ( ) ;
155+ } catch ( err ) {
156+ reject ( err ) ;
157+ }
158+ } ) ,
159+ ) ;
160+
161+ busboy . emitField ( '0' , '["$1"]' ) ;
162+ busboy . emitField ( '1' , '"$0"' ) ;
163+ busboy . finish ( ) ;
164+ await promise ;
142165 } ,
143166 ) ;
144167
145168 it ( 'should be able to resolve cyclic references in arrays' , async ( ) => {
146- const formData = new FormData ( ) ;
147- formData . append ( '0' , '["$1"]' ) ;
148- formData . append ( '1' , '["$0"]' ) ;
169+ const busboy = createMockBusboy ( ) ;
149170
150- const { busboy, stream} = await createBusboyStreamFromFormData ( formData ) ;
171+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
172+ busboy ,
173+ webpackServerMap ,
174+ ) ;
151175
152- const result = await serverAct ( async ( ) => {
153- const reply = ReactServerDOMServer . decodeReplyFromBusboy (
154- busboy ,
155- webpackServerMap ,
156- ) ;
157- stream . pipe ( busboy ) ;
158- return reply ;
159- } ) ;
176+ busboy . emitField ( '0' , '["$1"]' ) ;
177+ busboy . emitField ( '1' , '["$0"]' ) ;
178+ busboy . finish ( ) ;
179+ const result = await reply ;
160180
161181 expect ( result ) . toBeInstanceOf ( Array ) ;
162182 expect ( result [ 0 ] ) . toBeInstanceOf ( Array ) ;
@@ -167,19 +187,15 @@ describe('ReactFlightDOMReplyNode', () => {
167187 it . failing (
168188 'should be able to resolve cyclic references in arrays when calling .then synchronously' ,
169189 async ( ) => {
170- const formData = new FormData ( ) ;
171- formData . append ( '0' , '["$1"]' ) ;
172- formData . append ( '1' , '["$0"]' ) ;
173-
174- const { busboy, stream} = await createBusboyStreamFromFormData ( formData ) ;
190+ const busboy = createMockBusboy ( ) ;
175191
176192 const reply = ReactServerDOMServer . decodeReplyFromBusboy (
177193 busboy ,
178194 webpackServerMap ,
179195 ) ;
180- stream . pipe ( busboy ) ;
181196
182- await new Promise ( ( resolve , reject ) =>
197+ // Call .then synchronously before fields have been emitted.
198+ const promise = new Promise ( ( resolve , reject ) =>
183199 reply . then ( result => {
184200 try {
185201 expect ( result ) . toBeInstanceOf ( Array ) ;
@@ -192,35 +208,31 @@ describe('ReactFlightDOMReplyNode', () => {
192208 }
193209 } ) ,
194210 ) ;
211+
212+ busboy . emitField ( '0' , '["$1"]' ) ;
213+ busboy . emitField ( '1' , '["$0"]' ) ;
214+ busboy . finish ( ) ;
215+ await promise ;
195216 } ,
196217 ) ;
197218
198219 it ( 'cannot parse __proto__ into an object property through references' , async ( ) => {
199- const busboyStream = new ( require ( 'node:events' ) . EventEmitter ) ( ) ;
220+ const busboy = createMockBusboy ( ) ;
200221
201- const pendingResult = ReactServerDOMServer . decodeReplyFromBusboy (
202- busboyStream ,
222+ const reply = ReactServerDOMServer . decodeReplyFromBusboy (
223+ busboy ,
203224 webpackServerMap ,
204225 ) ;
205226
227+ // Call .then synchronously before fields have been emitted.
206228 function ignore ( ) { }
207- pendingResult . then ( ignore , ignore ) ;
208-
209- await serverAct ( async ( ) => {
210- busboyStream . emit ( 'field' , '0' , '"$@1"' ) ;
211- } ) ;
212-
213- await serverAct ( async ( ) => { } ) ;
214-
215- await serverAct ( async ( ) => {
216- busboyStream . emit ( 'field' , '1' , '{"proto": true, "__proto__": "bad"}' ) ;
217- } ) ;
229+ reply . then ( ignore , ignore ) ;
218230
219- const result = await pendingResult ;
231+ busboy . emitField ( '0' , '"$@1"' ) ;
232+ busboy . emitField ( '1' , '{"proto": true, "__proto__": "bad"}' ) ;
233+ busboy . finish ( ) ;
234+ const result = await reply ;
220235
221- await serverAct ( async ( ) => {
222- busboyStream . emit ( 'finish' ) ;
223- } ) ;
224236 // eslint-disable-next-line no-proto
225237 expect ( result . __proto__ ) . toBe ( Object . prototype ) ;
226238 expect ( Object . getPrototypeOf ( result ) ) . toBe ( Object . prototype ) ;
0 commit comments