33import assert from "assert" ;
44import http from "http" ;
55import https from "https" ;
6+ import net from "net" ;
67import { Transform , Writable } from "stream" ;
78import { ReadableStream } from "stream/web" ;
89import { URL } from "url" ;
@@ -17,7 +18,7 @@ import {
1718 _headersFromIncomingRequest ,
1819 logResponse ,
1920} from "@miniflare/core" ;
20- import { prefixError , randomHex } from "@miniflare/shared" ;
21+ import { Log , prefixError , randomHex } from "@miniflare/shared" ;
2122import { coupleWebSocket } from "@miniflare/web-sockets" ;
2223import { BodyInit , Headers } from "undici" ;
2324import { getAccessibleHosts } from "./helpers" ;
@@ -143,6 +144,111 @@ export type RequestListener = (
143144 res ?: http . ServerResponse
144145) => Promise < Response | undefined > ;
145146
147+ async function writeResponse (
148+ response : Response ,
149+ res : http . ServerResponse ,
150+ liveReload = false ,
151+ log ?: Log
152+ ) {
153+ const headers : http . OutgoingHttpHeaders = { } ;
154+ // eslint-disable-next-line prefer-const
155+ for ( let [ key , value ] of response . headers ) {
156+ key = key . toLowerCase ( ) ;
157+ if ( key === "set-cookie" ) {
158+ // Multiple Set-Cookie headers should be treated as separate headers
159+ // @ts -expect-error getAll is added to the Headers prototype by
160+ // importing @miniflare /core
161+ headers [ "set-cookie" ] = response . headers . getAll ( "set-cookie" ) ;
162+ } else {
163+ headers [ key ] = value ;
164+ }
165+ }
166+
167+ // Use body's actual length instead of the Content-Length header if set,
168+ // see https://github.com/cloudflare/miniflare/issues/148. We also might
169+ // need to adjust this later for live reloading so hold onto it.
170+ const contentLengthHeader = response . headers . get ( "Content-Length" ) ;
171+ const contentLength =
172+ _getBodyLength ( response ) ??
173+ ( contentLengthHeader === null ? null : parseInt ( contentLengthHeader ) ) ;
174+ if ( contentLength !== null ) headers [ "content-length" ] = contentLength ;
175+
176+ // If a Content-Encoding is set, and the user hasn't encoded the body,
177+ // we're responsible for doing so.
178+ const encoders : Transform [ ] = [ ] ;
179+ if ( headers [ "content-encoding" ] && response . encodeBody === "auto" ) {
180+ // Content-Length will be wrong as it's for the decoded length
181+ delete headers [ "content-length" ] ;
182+ // Reverse of https://github.com/nodejs/undici/blob/48d9578f431cbbd6e74f77455ba92184f57096cf/lib/fetch/index.js#L1660
183+ const codings = headers [ "content-encoding" ]
184+ . toString ( )
185+ . toLowerCase ( )
186+ . split ( "," )
187+ . map ( ( x ) => x . trim ( ) ) ;
188+ for ( const coding of codings ) {
189+ if ( / ( x - ) ? g z i p / . test ( coding ) ) {
190+ encoders . push ( zlib . createGzip ( ) ) ;
191+ } else if ( / ( x - ) ? d e f l a t e / . test ( coding ) ) {
192+ encoders . push ( zlib . createDeflate ( ) ) ;
193+ } else if ( coding === "br" ) {
194+ encoders . push ( zlib . createBrotliCompress ( ) ) ;
195+ } else {
196+ // Unknown encoding, don't do any encoding at all
197+ log ?. warn ( `Unknown encoding \"${ coding } \", sending plain response...` ) ;
198+ delete headers [ "content-encoding" ] ;
199+ encoders . length = 0 ;
200+ break ;
201+ }
202+ }
203+ }
204+
205+ // Add live reload script if enabled, this isn't an already encoded
206+ // response, and it's HTML
207+ const liveReloadEnabled =
208+ liveReload &&
209+ response . encodeBody === "auto" &&
210+ response . headers . get ( "content-type" ) ?. toLowerCase ( ) . includes ( "text/html" ) ;
211+
212+ // If Content-Length is specified, and we're live-reloading, we'll
213+ // need to adjust it to make room for the live reload script
214+ if ( liveReloadEnabled && contentLength !== null ) {
215+ if ( ! isNaN ( contentLength ) ) {
216+ // Append length of live reload script
217+ headers [ "content-length" ] = contentLength + liveReloadScriptLength ;
218+ }
219+ }
220+
221+ res . writeHead ( response . status , headers ) ;
222+
223+ // `initialStream` is the stream we'll write the response to. It
224+ // should end up as the first encoder, piping to the next encoder,
225+ // and finally piping to the response:
226+ //
227+ // encoders[0] (initialStream) -> encoders[1] -> res
228+ //
229+ // Not using `pipeline(passThrough, ...encoders, res)` here as that
230+ // gives a premature close error with server sent events. This also
231+ // avoids creating an extra stream even when we're not encoding.
232+ let initialStream : Writable = res ;
233+ for ( let i = encoders . length - 1 ; i >= 0 ; i -- ) {
234+ encoders [ i ] . pipe ( initialStream ) ;
235+ initialStream = encoders [ i ] ;
236+ }
237+
238+ // Response body may be null if empty
239+ if ( response . body ) {
240+ for await ( const chunk of response . body ) {
241+ if ( chunk ) initialStream . write ( chunk ) ;
242+ }
243+
244+ if ( liveReloadEnabled ) {
245+ initialStream . write ( liveReloadScript ) ;
246+ }
247+ }
248+
249+ initialStream . end ( ) ;
250+ }
251+
146252export function createRequestListener < Plugins extends HTTPPluginSignatures > (
147253 mf : MiniflareCore < Plugins >
148254) : RequestListener {
@@ -184,109 +290,8 @@ export function createRequestListener<Plugins extends HTTPPluginSignatures>(
184290 response = await mf . dispatchFetch ( request ) ;
185291 waitUntil = response . waitUntil ( ) ;
186292 status = response . status ;
187- const headers : http . OutgoingHttpHeaders = { } ;
188- // eslint-disable-next-line prefer-const
189- for ( let [ key , value ] of response . headers ) {
190- key = key . toLowerCase ( ) ;
191- if ( key === "set-cookie" ) {
192- // Multiple Set-Cookie headers should be treated as separate headers
193- // @ts -expect-error getAll is added to the Headers prototype by
194- // importing @miniflare /core
195- headers [ "set-cookie" ] = response . headers . getAll ( "set-cookie" ) ;
196- } else {
197- headers [ key ] = value ;
198- }
199- }
200-
201- // Use body's actual length instead of the Content-Length header if set,
202- // see https://github.com/cloudflare/miniflare/issues/148. We also might
203- // need to adjust this later for live reloading so hold onto it.
204- const contentLengthHeader = response . headers . get ( "Content-Length" ) ;
205- const contentLength =
206- _getBodyLength ( response ) ??
207- ( contentLengthHeader === null ? null : parseInt ( contentLengthHeader ) ) ;
208- if ( contentLength !== null ) headers [ "content-length" ] = contentLength ;
209-
210- // If a Content-Encoding is set, and the user hasn't encoded the body,
211- // we're responsible for doing so.
212- const encoders : Transform [ ] = [ ] ;
213- if ( headers [ "content-encoding" ] && response . encodeBody === "auto" ) {
214- // Content-Length will be wrong as it's for the decoded length
215- delete headers [ "content-length" ] ;
216- // Reverse of https://github.com/nodejs/undici/blob/48d9578f431cbbd6e74f77455ba92184f57096cf/lib/fetch/index.js#L1660
217- const codings = headers [ "content-encoding" ]
218- . toString ( )
219- . toLowerCase ( )
220- . split ( "," )
221- . map ( ( x ) => x . trim ( ) ) ;
222- for ( const coding of codings ) {
223- if ( / ( x - ) ? g z i p / . test ( coding ) ) {
224- encoders . push ( zlib . createGzip ( ) ) ;
225- } else if ( / ( x - ) ? d e f l a t e / . test ( coding ) ) {
226- encoders . push ( zlib . createDeflate ( ) ) ;
227- } else if ( coding === "br" ) {
228- encoders . push ( zlib . createBrotliCompress ( ) ) ;
229- } else {
230- // Unknown encoding, don't do any encoding at all
231- mf . log . warn (
232- `Unknown encoding \"${ coding } \", sending plain response...`
233- ) ;
234- delete headers [ "content-encoding" ] ;
235- encoders . length = 0 ;
236- break ;
237- }
238- }
239- }
240-
241- // Add live reload script if enabled, this isn't an already encoded
242- // response, and it's HTML
243- const liveReloadEnabled =
244- HTTPPlugin . liveReload &&
245- response . encodeBody === "auto" &&
246- response . headers
247- . get ( "content-type" )
248- ?. toLowerCase ( )
249- . includes ( "text/html" ) ;
250-
251- // If Content-Length is specified, and we're live-reloading, we'll
252- // need to adjust it to make room for the live reload script
253- if ( liveReloadEnabled && contentLength !== null ) {
254- if ( ! isNaN ( contentLength ) ) {
255- // Append length of live reload script
256- headers [ "content-length" ] = contentLength + liveReloadScriptLength ;
257- }
258- }
259-
260- res ?. writeHead ( status , headers ) ;
261-
262- // Response body may be null if empty
263293 if ( res ) {
264- // `initialStream` is the stream we'll write the response to. It
265- // should end up as the first encoder, piping to the next encoder,
266- // and finally piping to the response:
267- //
268- // encoders[0] (initialStream) -> encoders[1] -> res
269- //
270- // Not using `pipeline(passThrough, ...encoders, res)` here as that
271- // gives a premature close error with server sent events. This also
272- // avoids creating an extra stream even when we're not encoding.
273- let initialStream : Writable = res ;
274- for ( let i = encoders . length - 1 ; i >= 0 ; i -- ) {
275- encoders [ i ] . pipe ( initialStream ) ;
276- initialStream = encoders [ i ] ;
277- }
278-
279- if ( response . body ) {
280- for await ( const chunk of response . body ) {
281- if ( chunk ) initialStream . write ( chunk ) ;
282- }
283-
284- if ( liveReloadEnabled ) {
285- initialStream . write ( liveReloadScript ) ;
286- }
287- }
288-
289- initialStream . end ( ) ;
294+ await writeResponse ( response , res , HTTPPlugin . liveReload , mf . log ) ;
290295 }
291296 } catch ( e : any ) {
292297 // MIME types aren't case sensitive
@@ -394,9 +399,27 @@ export async function createServer<Plugins extends HTTPPluginSignatures>(
394399
395400 // Check web socket response was returned
396401 const webSocket = response ?. webSocket ;
397- if ( response ?. status !== 101 || ! webSocket ) {
398- socket . write ( "HTTP/1.1 500 Internal Server Error\r\n\r\n" ) ;
399- socket . destroy ( ) ;
402+ if ( response ?. status === 101 && webSocket ) {
403+ // Accept and couple the Web Socket
404+ extraHeaders . set ( request , response . headers ) ;
405+ webSocketServer . handleUpgrade ( request , socket as any , head , ( ws ) => {
406+ void coupleWebSocket ( ws , webSocket ) ;
407+ webSocketServer . emit ( "connection" , ws , request ) ;
408+ } ) ;
409+ return ;
410+ }
411+
412+ // Otherwise, we'll be returning a regular HTTP response
413+ const res = new http . ServerResponse ( request ) ;
414+ // `socket` is guaranteed to be an instance of `net.Socket`:
415+ // https://nodejs.org/api/http.html#event-upgrade_1
416+ assert ( socket instanceof net . Socket ) ;
417+ res . assignSocket ( socket ) ;
418+
419+ // If no response was provided, or it was an "ok" response, log an error
420+ if ( ! response || ( 200 <= response . status && response . status < 300 ) ) {
421+ res . writeHead ( 500 ) ;
422+ res . end ( ) ;
400423 mf . log . error (
401424 new TypeError (
402425 "Web Socket request did not return status 101 Switching Protocols response with Web Socket"
@@ -405,12 +428,9 @@ export async function createServer<Plugins extends HTTPPluginSignatures>(
405428 return ;
406429 }
407430
408- // Accept and couple the Web Socket
409- extraHeaders . set ( request , response . headers ) ;
410- webSocketServer . handleUpgrade ( request , socket as any , head , ( ws ) => {
411- void coupleWebSocket ( ws , webSocket ) ;
412- webSocketServer . emit ( "connection" , ws , request ) ;
413- } ) ;
431+ // Otherwise, send the response as is (e.g. unauthorised),
432+ // always disabling live-reload as this is a WebSocket upgrade
433+ await writeResponse ( response , res , false /* liveReload */ , mf . log ) ;
414434 }
415435 } ) ;
416436 const reloadListener = ( ) => {
0 commit comments