11import * as net from 'net' ;
2+ import * as tls from 'tls' ;
23import * as http2 from 'http2' ;
34import { expect } from 'chai' ;
45import { DestroyableServer , makeDestroyable } from 'destroyable-server' ;
@@ -140,4 +141,157 @@ accept-encoding: gzip, deflate
140141
141142 } ) ;
142143
144+ describe ( "HTTP/1.1 pipelining" , ( ) => {
145+
146+ it ( "rejects pipelined echo request when it's the second request" , async ( ) => {
147+ const host = `localhost:${ serverPort } ` ;
148+ const request1 = `GET /status/200 HTTP/1.1\r\nHost: ${ host } \r\n\r\n` ;
149+ const request2 = `GET /echo HTTP/1.1\r\nHost: ${ host } \r\nConnection: close\r\n\r\n` ;
150+
151+ const socket = tls . connect ( {
152+ host : 'localhost' ,
153+ port : serverPort ,
154+ servername : 'http1.localhost' ,
155+ rejectUnauthorized : false
156+ } ) ;
157+
158+ await new Promise < void > ( ( resolve ) => socket . on ( 'secureConnect' , resolve ) ) ;
159+
160+ // Send both requests at once (pipelined)
161+ socket . write ( request1 + request2 ) ;
162+
163+ const response = await new Promise < string > ( ( resolve ) => {
164+ const chunks : Buffer [ ] = [ ] ;
165+ socket . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
166+ socket . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) . toString ( ) ) ) ;
167+ } ) ;
168+
169+ socket . destroy ( ) ;
170+
171+ // Parse the two responses (filter empty strings from split)
172+ const responses = response . split ( / (? = H T T P \/ 1 \. 1 ) / ) . filter ( r => r . length > 0 ) ;
173+ expect ( responses . length ) . to . equal ( 2 ) ;
174+
175+ // First response should be 200 from /status/200
176+ expect ( responses [ 0 ] ) . to . include ( 'HTTP/1.1 200' ) ;
177+
178+ // Second response (echo) should be 400 because pipelining is detected
179+ const echoResponse = responses [ 1 ] ;
180+ expect ( echoResponse ) . to . include ( 'HTTP/1.1 400' ) ;
181+ expect ( echoResponse ) . to . include ( 'pipelining' ) ;
182+ } ) ;
183+
184+ it ( "rejects pipelined echo request when it's the first request" , async ( ) => {
185+ const host = `localhost:${ serverPort } ` ;
186+ const request1 = `GET /echo HTTP/1.1\r\nHost: ${ host } \r\n\r\n` ;
187+ const request2 = `GET /status/200 HTTP/1.1\r\nHost: ${ host } \r\nConnection: close\r\n\r\n` ;
188+
189+ const socket = tls . connect ( {
190+ host : 'localhost' ,
191+ port : serverPort ,
192+ servername : 'http1.localhost' ,
193+ rejectUnauthorized : false
194+ } ) ;
195+
196+ await new Promise < void > ( ( resolve ) => socket . on ( 'secureConnect' , resolve ) ) ;
197+
198+ // Send both requests at once (pipelined)
199+ socket . write ( request1 + request2 ) ;
200+
201+ const response = await new Promise < string > ( ( resolve ) => {
202+ const chunks : Buffer [ ] = [ ] ;
203+ socket . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
204+ socket . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) . toString ( ) ) ) ;
205+ } ) ;
206+
207+ socket . destroy ( ) ;
208+
209+ // First response (echo) should be 400 because pipelining is detected
210+ expect ( response ) . to . include ( 'HTTP/1.1 400' ) ;
211+ expect ( response ) . to . include ( 'pipelining' ) ;
212+ } ) ;
213+
214+ it ( "rejects both pipelined echo requests" , async ( ) => {
215+ const host = `localhost:${ serverPort } ` ;
216+ const request1 = `GET /echo HTTP/1.1\r\nHost: ${ host } \r\n\r\n` ;
217+ const request2 = `GET /echo HTTP/1.1\r\nHost: ${ host } \r\nConnection: close\r\n\r\n` ;
218+
219+ const socket = tls . connect ( {
220+ host : 'localhost' ,
221+ port : serverPort ,
222+ servername : 'http1.localhost' ,
223+ rejectUnauthorized : false
224+ } ) ;
225+
226+ await new Promise < void > ( ( resolve ) => socket . on ( 'secureConnect' , resolve ) ) ;
227+ socket . write ( request1 + request2 ) ;
228+
229+ const response = await new Promise < string > ( ( resolve ) => {
230+ const chunks : Buffer [ ] = [ ] ;
231+ socket . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
232+ socket . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) . toString ( ) ) ) ;
233+ } ) ;
234+
235+ socket . destroy ( ) ;
236+
237+ // Both echo requests should return 400 due to pipelining
238+ const responses = response . split ( / (? = H T T P \/ 1 \. 1 ) / ) . filter ( r => r . length > 0 ) ;
239+ expect ( responses . length ) . to . equal ( 2 ) ;
240+ expect ( responses [ 0 ] ) . to . include ( 'HTTP/1.1 400' ) ;
241+ expect ( responses [ 0 ] ) . to . include ( 'pipelining' ) ;
242+ expect ( responses [ 1 ] ) . to . include ( 'HTTP/1.1 400' ) ;
243+ expect ( responses [ 1 ] ) . to . include ( 'pipelining' ) ;
244+ } ) ;
245+
246+ it ( "works correctly with sequential requests on keep-alive connection" , async ( ) => {
247+ const host = `localhost:${ serverPort } ` ;
248+
249+ const socket = tls . connect ( {
250+ host : 'localhost' ,
251+ port : serverPort ,
252+ servername : 'http1.localhost' ,
253+ rejectUnauthorized : false
254+ } ) ;
255+
256+ await new Promise < void > ( ( resolve ) => socket . on ( 'secureConnect' , resolve ) ) ;
257+
258+ // Send first request and wait for response
259+ const request1 = `GET /status/200 HTTP/1.1\r\nHost: ${ host } \r\n\r\n` ;
260+ socket . write ( request1 ) ;
261+
262+ // Wait for first response
263+ const response1 = await new Promise < string > ( ( resolve ) => {
264+ let data = '' ;
265+ const onData = ( chunk : Buffer ) => {
266+ data += chunk . toString ( ) ;
267+ // Check if we have a complete response (ends with \r\n\r\n for no body)
268+ if ( data . includes ( '\r\n\r\n' ) ) {
269+ socket . removeListener ( 'data' , onData ) ;
270+ resolve ( data ) ;
271+ }
272+ } ;
273+ socket . on ( 'data' , onData ) ;
274+ } ) ;
275+
276+ expect ( response1 ) . to . include ( 'HTTP/1.1 200' ) ;
277+
278+ // Now send second request (echo)
279+ const request2 = `GET /echo HTTP/1.1\r\nHost: ${ host } \r\nConnection: close\r\n\r\n` ;
280+ socket . write ( request2 ) ;
281+
282+ const response2 = await new Promise < string > ( ( resolve ) => {
283+ const chunks : Buffer [ ] = [ ] ;
284+ socket . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
285+ socket . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) . toString ( ) ) ) ;
286+ } ) ;
287+
288+ socket . destroy ( ) ;
289+
290+ // Echo should work and return the raw request
291+ expect ( response2 ) . to . include ( 'HTTP/1.1 200' ) ;
292+ expect ( response2 ) . to . include ( 'GET /echo HTTP/1.1' ) ;
293+ } ) ;
294+
295+ } ) ;
296+
143297} ) ;
0 commit comments