@@ -55,26 +55,57 @@ import { fromNodeHttp, normalizeUrl } from './http';
5555} ) ;
5656
5757describe ( 'fromNodeHttp()' , ( ) => {
58- test ( 'should resolve writes from the node write callback without waiting for drain ' , async ( ) => {
58+ test ( 'should resolve writes immediately when res. write returns true (no backpressure) ' , async ( ) => {
5959 const req = new EventEmitter ( ) as IncomingMessage & EventEmitter ;
6060 req . method = 'GET' ;
6161 req . url = '/' ;
6262 req . headers = { host : 'localhost' } ;
6363 ( req as any ) . socket = { } ;
6464
65- let writeCallback : ( ( error ?: Error | null ) => void ) | undefined ;
6665 const res = new EventEmitter ( ) as ServerResponse & EventEmitter ;
6766 Object . defineProperty ( res , 'closed' , { value : false , configurable : true } ) ;
6867 Object . defineProperty ( res , 'destroyed' , { value : false , configurable : true } ) ;
6968 res . setHeader = vi . fn ( ) ;
70- res . write = vi . fn ( ( _chunk : Uint8Array , cb ?: ( error ?: Error | null ) => void ) => {
71- writeCallback = cb ;
72- // Returning false simulates backpressure. This adapter does not attach
73- // drain listeners because ServerResponse.write() provides a per-write callback.
74- return false ;
69+ res . write = vi . fn ( ( ) => true ) as any ;
70+ // Mimic vanilla Node: 'finish' fires once res.end() has flushed.
71+ res . end = vi . fn ( ( ) => {
72+ queueMicrotask ( ( ) => res . emit ( 'finish' ) ) ;
73+ return res ;
7574 } ) as any ;
76- res . end = vi . fn ( ( cb ?: ( ) => void ) => {
77- cb ?.( ) ;
75+
76+ const requestEv = await fromNodeHttp ( new URL ( 'http://localhost/' ) , req , res , 'server' ) ;
77+ const writableStream = requestEv . getWritableStream (
78+ 200 ,
79+ new Headers ( [ [ 'Content-Type' , 'text/html; charset=utf-8' ] ] ) ,
80+ { headers : ( ) => [ ] } as any ,
81+ ( ) => { } ,
82+ undefined as any
83+ ) ;
84+ const writer = writableStream . getWriter ( ) ;
85+
86+ await writer . write ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) ;
87+ // No drain listener should be attached when there's no backpressure.
88+ expect ( res . listenerCount ( 'drain' ) ) . toBe ( 0 ) ;
89+
90+ await writer . close ( ) ;
91+ expect ( res . write ) . toHaveBeenCalledTimes ( 1 ) ;
92+ expect ( res . end ) . toHaveBeenCalledTimes ( 1 ) ;
93+ } ) ;
94+
95+ test ( 'should resolve writes via the drain event when res.write returns false' , async ( ) => {
96+ const req = new EventEmitter ( ) as IncomingMessage & EventEmitter ;
97+ req . method = 'GET' ;
98+ req . url = '/' ;
99+ req . headers = { host : 'localhost' } ;
100+ ( req as any ) . socket = { } ;
101+
102+ const res = new EventEmitter ( ) as ServerResponse & EventEmitter ;
103+ Object . defineProperty ( res , 'closed' , { value : false , configurable : true } ) ;
104+ Object . defineProperty ( res , 'destroyed' , { value : false , configurable : true } ) ;
105+ res . setHeader = vi . fn ( ) ;
106+ res . write = vi . fn ( ( ) => false ) as any ; // simulate backpressure
107+ res . end = vi . fn ( ( ) => {
108+ queueMicrotask ( ( ) => res . emit ( 'finish' ) ) ;
78109 return res ;
79110 } ) as any ;
80111
@@ -90,17 +121,52 @@ describe('fromNodeHttp()', () => {
90121
91122 const writePromise = writer . write ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) ;
92123 await Promise . resolve ( ) ;
93- // No drain listener should be attached in this path; Node calls the write
94- // callback once this chunk has flushed, even when write() returned false.
95- expect ( res . listenerCount ( 'drain' ) ) . toBe ( 0 ) ;
96- expect ( writeCallback ) . toBeDefined ( ) ;
124+ // A drain listener should be attached because res.write() returned false.
125+ expect ( res . listenerCount ( 'drain' ) ) . toBe ( 1 ) ;
97126
98- writeCallback ?. ( null ) ;
127+ res . emit ( 'drain' ) ;
99128 await writePromise ;
129+ expect ( res . listenerCount ( 'drain' ) ) . toBe ( 0 ) ;
130+
100131 await writer . close ( ) ;
132+ } ) ;
101133
102- expect ( res . write ) . toHaveBeenCalledTimes ( 1 ) ;
103- expect ( res . end ) . toHaveBeenCalledTimes ( 1 ) ;
134+ test ( 'should not hang when wrapper middleware drops the per-write callback (e.g. compression)' , async ( ) => {
135+ // Reproduces the regression from https://github.com/QwikDev/qwik/pull/8557:
136+ // `compression` middleware wraps res.write with a 2-arg signature
137+ // (chunk, encoding) and never invokes the optional callback.
138+ const req = new EventEmitter ( ) as IncomingMessage & EventEmitter ;
139+ req . method = 'GET' ;
140+ req . url = '/' ;
141+ req . headers = { host : 'localhost' } ;
142+ ( req as any ) . socket = { } ;
143+
144+ const res = new EventEmitter ( ) as ServerResponse & EventEmitter ;
145+ Object . defineProperty ( res , 'closed' , { value : false , configurable : true } ) ;
146+ Object . defineProperty ( res , 'destroyed' , { value : false , configurable : true } ) ;
147+ res . setHeader = vi . fn ( ) ;
148+ // Wrapper that mimics `compression`: callback is dropped.
149+ res . write = vi . fn ( ( _chunk : Uint8Array , _encoding ?: any ) => false ) as any ;
150+ res . end = vi . fn ( ( ) => res ) as any ; // also drops callback
151+
152+ const requestEv = await fromNodeHttp ( new URL ( 'http://localhost/' ) , req , res , 'server' ) ;
153+ const writableStream = requestEv . getWritableStream (
154+ 200 ,
155+ new Headers ( [ [ 'Content-Type' , 'text/html; charset=utf-8' ] ] ) ,
156+ { headers : ( ) => [ ] } as any ,
157+ ( ) => { } ,
158+ undefined as any
159+ ) ;
160+ const writer = writableStream . getWriter ( ) ;
161+
162+ const writePromise = writer . write ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) ;
163+ await Promise . resolve ( ) ;
164+ res . emit ( 'drain' ) ;
165+ await writePromise ; // would hang forever before the fix
166+
167+ const closePromise = writer . close ( ) ;
168+ res . emit ( 'finish' ) ; // close() must resolve even though res.end's cb is dropped
169+ await closePromise ;
104170 } ) ;
105171
106172 test ( 'should resolve the response when the writable stream is created' , async ( ) => {
@@ -118,8 +184,8 @@ describe('fromNodeHttp()', () => {
118184 cb ?.( null ) ;
119185 return true ;
120186 } ) as any ;
121- res . end = vi . fn ( ( cb ?: ( ) => void ) => {
122- cb ?. ( ) ;
187+ res . end = vi . fn ( ( ) => {
188+ queueMicrotask ( ( ) => res . emit ( 'finish' ) ) ;
123189 return res ;
124190 } ) as any ;
125191
0 commit comments