11"use strict" ;
22
3+ const http = require ( "node:http" ) ;
34const path = require ( "node:path" ) ;
45const util = require ( "node:util" ) ;
56const express = require ( "express" ) ;
@@ -8,7 +9,8 @@ const webpack = require("webpack");
89const WebSocket = require ( "ws" ) ;
910const Server = require ( "../../lib/Server" ) ;
1011const config = require ( "../fixtures/proxy-config/webpack.config" ) ;
11- const [ port1 , port2 , port3 , port4 ] = require ( "../ports-map" ) [ "proxy-option" ] ;
12+ const [ port1 , port2 , port3 , port4 , port5 ] =
13+ require ( "../ports-map" ) [ "proxy-option" ] ;
1214
1315const WebSocketServer = WebSocket . Server ;
1416const staticDirectory = path . resolve ( __dirname , "../fixtures/proxy-config" ) ;
@@ -659,6 +661,204 @@ describe("proxy option", () => {
659661 }
660662 } ) ;
661663
664+ describe ( "should not silently proxy dev-server HMR websocket to a permissive backend" , ( ) => {
665+ let server ;
666+ let backend ;
667+ let backendWss ;
668+ let backendUpgradeCount ;
669+
670+ const BACKEND_MESSAGE_TYPE = "backend-message" ;
671+
672+ beforeAll ( async ( ) => {
673+ backendUpgradeCount = 0 ;
674+
675+ backend = http . createServer ( ) ;
676+ backendWss = new WebSocketServer ( { server : backend } ) ;
677+ backendWss . on ( "connection" , ( connection ) => {
678+ backendUpgradeCount += 1 ;
679+ connection . send ( JSON . stringify ( { type : BACKEND_MESSAGE_TYPE } ) ) ;
680+ } ) ;
681+
682+ await new Promise ( ( resolve ) => {
683+ backend . listen ( port5 , resolve ) ;
684+ } ) ;
685+
686+ const compiler = webpack ( config ) ;
687+
688+ server = new Server (
689+ {
690+ hot : true ,
691+ allowedHosts : "all" ,
692+ webSocketServer : "ws" ,
693+ proxy : [
694+ {
695+ context : "/" ,
696+ target : `http://localhost:${ port5 } ` ,
697+ ws : true ,
698+ } ,
699+ ] ,
700+ port : port3 ,
701+ } ,
702+ compiler ,
703+ ) ;
704+
705+ await server . start ( ) ;
706+ } ) ;
707+
708+ afterAll ( async ( ) => {
709+ for ( const client of backendWss . clients ) {
710+ client . terminate ( ) ;
711+ }
712+ backendWss . close ( ) ;
713+ // Force-drop any lingering proxy-opened sockets so backend.close() does
714+ // not hang when the fix is missing and the proxy is mid-upgrade.
715+ backend . closeAllConnections ( ) ;
716+ await server . stop ( ) ;
717+ await new Promise ( ( resolve ) => {
718+ backend . close ( resolve ) ;
719+ } ) ;
720+ } ) ;
721+
722+ it ( "delivers the HMR control messages and never reaches the proxy target" , async ( ) => {
723+ const messages = [ ] ;
724+
725+ const ws = new WebSocket ( `ws://localhost:${ port3 } /ws` ) ;
726+
727+ await new Promise ( ( resolve , reject ) => {
728+ const timer = setTimeout ( ( ) => {
729+ reject (
730+ new Error (
731+ `Timed out waiting for HMR message. Got: ${ JSON . stringify ( messages ) } ` ,
732+ ) ,
733+ ) ;
734+ } , 3000 ) ;
735+
736+ ws . on ( "message" , ( raw ) => {
737+ const parsed = JSON . parse ( raw . toString ( ) ) ;
738+ messages . push ( parsed ) ;
739+ if ( parsed . type === "hot" ) {
740+ clearTimeout ( timer ) ;
741+ resolve ( ) ;
742+ }
743+ } ) ;
744+
745+ ws . on ( "error" , ( err ) => {
746+ clearTimeout ( timer ) ;
747+ reject ( err ) ;
748+ } ) ;
749+ } ) ;
750+
751+ ws . close ( ) ;
752+
753+ // Let the proxy finish its async forwarding so the assertion below sees
754+ // the upgrade attempt deterministically.
755+ await new Promise ( ( resolve ) => {
756+ setTimeout ( resolve , 300 ) ;
757+ } ) ;
758+
759+ expect ( messages . some ( ( m ) => m . type === "hot" ) ) . toBe ( true ) ;
760+ expect ( messages . some ( ( m ) => m . type === BACKEND_MESSAGE_TYPE ) ) . toBe ( false ) ;
761+ expect ( backendUpgradeCount ) . toBe ( 0 ) ;
762+ } ) ;
763+ } ) ;
764+
765+ describe ( "should not log proxy errors for the dev-server HMR upgrade" , ( ) => {
766+ let server ;
767+ let backend ;
768+ let stderrSpy ;
769+
770+ beforeAll ( async ( ) => {
771+ stderrSpy = jest
772+ . spyOn ( process . stderr , "write" )
773+ . mockImplementation ( ( ) => true ) ;
774+
775+ backend = http . createServer ( ) ;
776+ backend . on ( "upgrade" , ( req , socket ) => {
777+ socket . destroy ( ) ;
778+ } ) ;
779+ await new Promise ( ( resolve ) => {
780+ backend . listen ( port5 , resolve ) ;
781+ } ) ;
782+
783+ const compiler = webpack ( config ) ;
784+
785+ server = new Server (
786+ {
787+ hot : true ,
788+ allowedHosts : "all" ,
789+ webSocketServer : "ws" ,
790+ proxy : [
791+ {
792+ context : "/" ,
793+ target : `http://localhost:${ port5 } ` ,
794+ ws : true ,
795+ } ,
796+ ] ,
797+ port : port3 ,
798+ } ,
799+ compiler ,
800+ ) ;
801+
802+ await server . start ( ) ;
803+ } ) ;
804+
805+ afterAll ( async ( ) => {
806+ stderrSpy . mockRestore ( ) ;
807+ backend . closeAllConnections ( ) ;
808+ await server . stop ( ) ;
809+ await new Promise ( ( resolve ) => {
810+ backend . close ( resolve ) ;
811+ } ) ;
812+ } ) ;
813+
814+ it ( "does not surface any [HPM] error when the HMR client connects" , async ( ) => {
815+ const messages = [ ] ;
816+
817+ const ws = new WebSocket ( `ws://localhost:${ port3 } /ws` ) ;
818+
819+ await new Promise ( ( resolve , reject ) => {
820+ const timer = setTimeout ( ( ) => {
821+ reject (
822+ new Error (
823+ `Timed out waiting for HMR message. Got: ${ JSON . stringify ( messages ) } ` ,
824+ ) ,
825+ ) ;
826+ } , 3000 ) ;
827+
828+ ws . on ( "message" , ( raw ) => {
829+ const parsed = JSON . parse ( raw . toString ( ) ) ;
830+ messages . push ( parsed ) ;
831+ if ( parsed . type === "hot" ) {
832+ clearTimeout ( timer ) ;
833+ resolve ( ) ;
834+ }
835+ } ) ;
836+
837+ ws . on ( "error" , ( err ) => {
838+ clearTimeout ( timer ) ;
839+ reject ( err ) ;
840+ } ) ;
841+ } ) ;
842+
843+ ws . close ( ) ;
844+
845+ await new Promise ( ( resolve ) => {
846+ setTimeout ( resolve , 200 ) ;
847+ } ) ;
848+
849+ const hpmLines = stderrSpy . mock . calls
850+ . map ( ( c ) => c [ 0 ] )
851+ . join ( "" )
852+ . split ( "\n" )
853+ . filter ( ( line ) => line . includes ( "[HPM]" ) )
854+ . map ( ( line ) => line . replaceAll ( / l o c a l h o s t : \d + / g, "localhost:<port>" ) )
855+ . join ( "\n" ) ;
856+
857+ expect ( hpmLines ) . toBe ( "" ) ;
858+ expect ( messages . some ( ( m ) => m . type === "hot" ) ) . toBe ( true ) ;
859+ } ) ;
860+ } ) ;
861+
662862 describe ( "should supports http methods" , ( ) => {
663863 let server ;
664864 let req ;
0 commit comments