@@ -859,134 +859,14 @@ describe("proxy option", () => {
859859 } ) ;
860860 } ) ;
861861
862- describe ( "HMR upgrade path matching is exact when dispatching upgrades " , ( ) => {
862+ describe ( "HMR upgrade dispatching to user proxies " , ( ) => {
863863 let server ;
864864 let backend ;
865865 let backendUpgradeCount ;
866866
867- beforeAll ( async ( ) => {
868- backendUpgradeCount = 0 ;
869-
870- backend = http . createServer ( ) ;
871- new WebSocketServer ( { server : backend } ) . on ( "connection" , ( ) => {
872- backendUpgradeCount += 1 ;
873- } ) ;
874-
875- await new Promise ( ( resolve ) => {
876- backend . listen ( port5 , resolve ) ;
877- } ) ;
878-
879- const compiler = webpack ( config ) ;
880-
881- server = new Server (
882- {
883- hot : true ,
884- allowedHosts : "all" ,
885- webSocketServer : "ws" ,
886- proxy : [
887- {
888- context : "/" ,
889- target : `http://localhost:${ port5 } ` ,
890- ws : true ,
891- } ,
892- ] ,
893- port : port3 ,
894- } ,
895- compiler ,
896- ) ;
897-
898- await server . start ( ) ;
899- } ) ;
900-
901- afterAll ( async ( ) => {
902- backend . closeAllConnections ( ) ;
903- await server . stop ( ) ;
904- await new Promise ( ( resolve ) => {
905- backend . close ( resolve ) ;
906- } ) ;
907- } ) ;
908-
909- // The HMR server's default path is /ws. The dispatch matches the path
910- // exactly, the same way the underlying `ws` server (`shouldHandle`) does, so
911- // only the configured path is recognized as the HMR socket. The query
912- // string is stripped before comparison (again like `ws`), so `/ws?token=1`
913- // is still recognized as the HMR path.
914- const hmrVariants = [
915- [ "exact path" , "/ws" ] ,
916- [ "path with query string" , "/ws?token=1" ] ,
917- ] ;
918-
919- it . each ( hmrVariants ) (
920- "treats %s (%s) as the HMR upgrade path (not forwarded to the proxy)" ,
921- async ( _label , path ) => {
922- const before = backendUpgradeCount ;
923-
924- const ws = new WebSocket ( `ws://localhost:${ port3 } ${ path } ` ) ;
925- ws . on ( "error" , ( ) => { } ) ;
926-
927- await new Promise ( ( resolve ) => {
928- setTimeout ( resolve , 400 ) ;
929- } ) ;
930-
931- try {
932- ws . close ( ) ;
933- } catch {
934- // ignore close errors on already-failed sockets
935- }
936-
937- await new Promise ( ( resolve ) => {
938- setTimeout ( resolve , 200 ) ;
939- } ) ;
940-
941- expect ( backendUpgradeCount ) . toBe ( before ) ;
942- } ,
943- ) ;
944-
945- // Leading double-slash, case, trailing slash and percent-encoding are all
946- // significant: none equals the configured HMR path under the raw comparison
947- // `ws` uses, so they are forwarded to the user proxy (which is what `ws`
948- // would do — it refuses to serve them).
949- const proxiedVariants = [
950- [ "leading double slash" , "//ws" ] ,
951- [ "trailing slash" , "/ws/" ] ,
952- [ "uppercase" , "/WS" ] ,
953- [ "mixed case" , "/wS" ] ,
954- [ "percent-encoded path" , "/%77%73" ] ,
955- ] ;
956-
957- it . each ( proxiedVariants ) (
958- "forwards %s (%s) to the user proxy" ,
959- async ( _label , path ) => {
960- const before = backendUpgradeCount ;
961-
962- const ws = new WebSocket ( `ws://localhost:${ port3 } ${ path } ` ) ;
963- ws . on ( "error" , ( ) => { } ) ;
964-
965- await new Promise ( ( resolve ) => {
966- setTimeout ( resolve , 400 ) ;
967- } ) ;
968-
969- try {
970- ws . close ( ) ;
971- } catch {
972- // ignore close errors on already-failed sockets
973- }
974-
975- await new Promise ( ( resolve ) => {
976- setTimeout ( resolve , 200 ) ;
977- } ) ;
978-
979- expect ( backendUpgradeCount ) . toBe ( before + 1 ) ;
980- } ,
981- ) ;
982- } ) ;
983-
984- describe ( "HMR upgrade path matching honors a custom webSocketServer path" , ( ) => {
985- let server ;
986- let backend ;
987- let backendUpgradeCount ;
988-
989- beforeAll ( async ( ) => {
867+ // Start a backend WebSocket server (the user proxy target) and a dev-server
868+ // proxying everything to it, with the given dev-server options merged in.
869+ const setup = async ( devServerOptions ) => {
990870 backendUpgradeCount = 0 ;
991871
992872 backend = http . createServer ( ) ;
@@ -998,13 +878,10 @@ describe("proxy option", () => {
998878 backend . listen ( port5 , resolve ) ;
999879 } ) ;
1000880
1001- const compiler = webpack ( config ) ;
1002-
1003881 server = new Server (
1004882 {
1005883 hot : true ,
1006884 allowedHosts : "all" ,
1007- webSocketServer : { type : "ws" , options : { path : "/custom-hmr" } } ,
1008885 proxy : [
1009886 {
1010887 context : "/" ,
@@ -1013,27 +890,33 @@ describe("proxy option", () => {
1013890 } ,
1014891 ] ,
1015892 port : port3 ,
893+ ...devServerOptions ,
1016894 } ,
1017- compiler ,
895+ webpack ( config ) ,
1018896 ) ;
1019897
1020898 await server . start ( ) ;
1021- } ) ;
899+ } ;
1022900
1023- afterAll ( async ( ) => {
901+ const teardown = async ( ) => {
1024902 backend . closeAllConnections ( ) ;
1025903 await server . stop ( ) ;
1026904 await new Promise ( ( resolve ) => {
1027905 backend . close ( resolve ) ;
1028906 } ) ;
1029- } ) ;
907+ } ;
1030908
1031- // The dispatch reads the HMR path from the configured `webSocketServer`
1032- // options, not a hardcoded `/ws`.
1033- it ( "treats the configured path (/custom-hmr) as the HMR upgrade path (not forwarded to the proxy)" , async ( ) => {
909+ // Open a WebSocket to `path` and report whether the dev-server completed the
910+ // handshake (`opened`) and whether the upgrade was forwarded to the backend
911+ // proxy (`forwarded`).
912+ const probe = async ( path ) => {
1034913 const before = backendUpgradeCount ;
1035914
1036- const ws = new WebSocket ( `ws://localhost:${ port3 } /custom-hmr` ) ;
915+ let opened = false ;
916+ const ws = new WebSocket ( `ws://localhost:${ port3 } ${ path } ` ) ;
917+ ws . on ( "open" , ( ) => {
918+ opened = true ;
919+ } ) ;
1037920 ws . on ( "error" , ( ) => { } ) ;
1038921
1039922 await new Promise ( ( resolve ) => {
@@ -1050,106 +933,111 @@ describe("proxy option", () => {
1050933 setTimeout ( resolve , 200 ) ;
1051934 } ) ;
1052935
1053- expect ( backendUpgradeCount ) . toBe ( before ) ;
1054- } ) ;
936+ return { opened , forwarded : backendUpgradeCount > before } ;
937+ } ;
1055938
1056- // The default `/ws` is no longer special once a custom path is configured,
1057- // so it is forwarded to the user proxy like any other path.
1058- it ( "forwards the default path (/ws) to the user proxy when it is not the configured HMR path" , async ( ) => {
1059- const before = backendUpgradeCount ;
939+ // Behavior shared by every WebSocket server implementation: the HMR socket
940+ // is served locally and never forwarded, while any path the HMR server does
941+ // not own falls through to the user proxy. SockJS serves its transport under
942+ // `/<prefix>/<server>/<session>/websocket`, not the bare `/ws`.
943+ const serverTypes = [
944+ { type : "ws" , hmrPath : "/ws" , nonHmrPath : "/not-hmr" } ,
945+ {
946+ type : "sockjs" ,
947+ hmrPath : "/ws/000/abcd1234/websocket" ,
948+ nonHmrPath : "/not-hmr" ,
949+ } ,
950+ ] ;
1060951
1061- const ws = new WebSocket ( `ws://localhost:${ port3 } /ws` ) ;
1062- ws . on ( "error" , ( ) => { } ) ;
952+ for ( const { type, hmrPath, nonHmrPath } of serverTypes ) {
953+ describe ( `with webSocketServerType: ${ type } ` , ( ) => {
954+ beforeAll ( ( ) => setup ( { webSocketServer : type } ) ) ;
1063955
1064- await new Promise ( ( resolve ) => {
1065- setTimeout ( resolve , 400 ) ;
1066- } ) ;
956+ afterAll ( teardown ) ;
1067957
1068- try {
1069- ws . close ( ) ;
1070- } catch {
1071- // ignore close errors on already-failed sockets
1072- }
958+ it ( "serves the HMR upgrade locally and does not forward it to the proxy" , async ( ) => {
959+ const { opened, forwarded } = await probe ( hmrPath ) ;
1073960
1074- await new Promise ( ( resolve ) => {
1075- setTimeout ( resolve , 200 ) ;
961+ expect ( opened ) . toBe ( true ) ;
962+ expect ( forwarded ) . toBe ( false ) ;
963+ } ) ;
964+
965+ it ( "forwards a non-HMR upgrade to the user proxy" , async ( ) => {
966+ const { forwarded } = await probe ( nonHmrPath ) ;
967+
968+ expect ( forwarded ) . toBe ( true ) ;
969+ } ) ;
1076970 } ) ;
971+ }
1077972
1078- expect ( backendUpgradeCount ) . toBe ( before + 1 ) ;
1079- } ) ;
1080- } ) ;
973+ // `ws`-specific: the dispatch compares the path exactly the same way
974+ // `WebSocketServer#shouldHandle` does, so only the configured path (query
975+ // stripped) is the HMR socket; every other variant is forwarded.
976+ describe ( "with the `ws` server, path matching is exact" , ( ) => {
977+ beforeAll ( ( ) => setup ( { webSocketServer : "ws" } ) ) ;
1081978
1082- describe ( "HMR upgrade path matching is skipped when no webSocketServer is configured" , ( ) => {
1083- let server ;
1084- let backend ;
1085- let backendUpgradeCount ;
979+ afterAll ( teardown ) ;
1086980
1087- beforeAll ( async ( ) => {
1088- backendUpgradeCount = 0 ;
981+ it . each ( [
982+ [ "exact path" , "/ws" ] ,
983+ [ "path with query string" , "/ws?token=1" ] ,
984+ ] ) ( "treats %s (%s) as the HMR upgrade path" , async ( _label , path ) => {
985+ const { forwarded } = await probe ( path ) ;
1089986
1090- backend = http . createServer ( ) ;
1091- new WebSocketServer ( { server : backend } ) . on ( "connection" , ( ) => {
1092- backendUpgradeCount += 1 ;
987+ expect ( forwarded ) . toBe ( false ) ;
1093988 } ) ;
1094989
1095- await new Promise ( ( resolve ) => {
1096- backend . listen ( port5 , resolve ) ;
1097- } ) ;
990+ it . each ( [
991+ [ "leading double slash" , "//ws" ] ,
992+ [ "trailing slash" , "/ws/" ] ,
993+ [ "uppercase" , "/WS" ] ,
994+ [ "mixed case" , "/wS" ] ,
995+ [ "percent-encoded path" , "/%77%73" ] ,
996+ ] ) ( "forwards %s (%s) to the user proxy" , async ( _label , path ) => {
997+ const { forwarded } = await probe ( path ) ;
1098998
1099- const compiler = webpack ( config ) ;
999+ expect ( forwarded ) . toBe ( true ) ;
1000+ } ) ;
1001+ } ) ;
11001002
1101- server = new Server (
1102- {
1103- hot : false ,
1104- liveReload : false ,
1105- allowedHosts : "all" ,
1106- webSocketServer : false ,
1107- proxy : [
1108- {
1109- context : "/" ,
1110- target : `http://localhost:${ port5 } ` ,
1111- ws : true ,
1112- } ,
1113- ] ,
1114- port : port3 ,
1115- } ,
1116- compiler ,
1003+ // The HMR path is read from the configured `webSocketServer` options, not a
1004+ // hardcoded `/ws`.
1005+ describe ( "with a custom `ws` path" , ( ) => {
1006+ beforeAll ( ( ) =>
1007+ setup ( {
1008+ webSocketServer : { type : "ws" , options : { path : "/custom-hmr" } } ,
1009+ } ) ,
11171010 ) ;
11181011
1119- await server . start ( ) ;
1120- } ) ;
1012+ afterAll ( teardown ) ;
11211013
1122- afterAll ( async ( ) => {
1123- backend . closeAllConnections ( ) ;
1124- await server . stop ( ) ;
1125- await new Promise ( ( resolve ) => {
1126- backend . close ( resolve ) ;
1014+ it ( "treats the configured path (/custom-hmr) as the HMR upgrade path" , async ( ) => {
1015+ const { forwarded } = await probe ( "/custom-hmr" ) ;
1016+
1017+ expect ( forwarded ) . toBe ( false ) ;
1018+ } ) ;
1019+
1020+ it ( "forwards the default path (/ws) once it is no longer the HMR path" , async ( ) => {
1021+ const { forwarded } = await probe ( "/ws" ) ;
1022+
1023+ expect ( forwarded ) . toBe ( true ) ;
11271024 } ) ;
11281025 } ) ;
11291026
11301027 // With no HMR server there is no socket to protect, so the filter never
11311028 // engages and even `/ws` is forwarded to the user proxy.
1132- it ( "forwards /ws to the user proxy because there is no HMR socket to protect" , async ( ) => {
1133- const before = backendUpgradeCount ;
1134-
1135- const ws = new WebSocket ( `ws://localhost:${ port3 } /ws` ) ;
1136- ws . on ( "error" , ( ) => { } ) ;
1029+ describe ( "without a webSocketServer" , ( ) => {
1030+ beforeAll ( ( ) =>
1031+ setup ( { hot : false , liveReload : false , webSocketServer : false } ) ,
1032+ ) ;
11371033
1138- await new Promise ( ( resolve ) => {
1139- setTimeout ( resolve , 400 ) ;
1140- } ) ;
1034+ afterAll ( teardown ) ;
11411035
1142- try {
1143- ws . close ( ) ;
1144- } catch {
1145- // ignore close errors on already-failed sockets
1146- }
1036+ it ( "forwards /ws to the user proxy because there is no HMR socket to protect" , async ( ) => {
1037+ const { forwarded } = await probe ( "/ws" ) ;
11471038
1148- await new Promise ( ( resolve ) => {
1149- setTimeout ( resolve , 200 ) ;
1039+ expect ( forwarded ) . toBe ( true ) ;
11501040 } ) ;
1151-
1152- expect ( backendUpgradeCount ) . toBe ( before + 1 ) ;
11531041 } ) ;
11541042 } ) ;
11551043
0 commit comments