http-proxy-middleware icon indicating copy to clipboard operation
http-proxy-middleware copied to clipboard

websocket proxy cannot upgrade websocket connection

Open desytech opened this issue 8 years ago • 20 comments

Expected behavior

HPM should upgrade websocket requests always

Actual behavior

HPM websocket proxy cannot upgrade websocket connections sporadically

Setup

  • http-proxy-middleware: 0.17.1
  • server: tornado 4.4.1 - Tornado Github
  • webpack: 1.13.2
  • webpack-dev-server: 1.16.1

proxy middleware configuration

 devServer: {
      proxy: {
        '/hyperguard/websocket/*': {
          target: 'ws://localhost:8082',
        ws: true
        }
      }
    }

server mounting

def serve_forever(self):
    app = self.__make_app()
    self.__http_server = HTTPServer(app)
    self.__http_server._handle_connection = self._handle_connection
    self.__http_server.listen(self.__port, self.__host)
    app.ssl_enabled = self.__ssl_options is not None
    self.__ioloop.start()

Howto Reproduce

http stream request

GET /hyperguard/websocket HTTP/1.1
Host: localhost:8079
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
origin: http://localhost:8079
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: yrOcNHCvvVuGgttYgv9nzA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

http stream response getting this response only sometimes, if not the websocket connection never be established

HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: Upgrade
sec-websocket-accept: I3u4B7iakSJWk4yLd02hfn+enus=
  • that issue also occurs on different platforms (fedora 22, ubuntu 14.04, centos7)
  • restarting webpack-dev-server doesnt change anything

desytech avatar Sep 22 '16 09:09 desytech

Thanks for reporting.

Can you provide info on the websocket client(s) you are using. And the frequency of the upgrade requests?

_.debounce is used to solve an issue: #57. Maybe that explains the sporadic behaviour when concurrent upgrade requests are made.

https://github.com/chimurai/http-proxy-middleware/blob/0cb6839ed2121f7dc8685f31a1e18b5069eeb092/lib/index.js#L15

Can you try to remove the debounce code and see if it solves the issue?

From:

var wsUpgradeDebounced  = _.debounce(handleUpgrade);

To:

var wsUpgradeDebounced  = handleUpgrade;

chimurai avatar Sep 22 '16 10:09 chimurai

We are using the websocket client implementation of Chrome(53.0.2785.116 m (64-bit)) and Firefox (47.0). Frequency: I try to establish only one connection on initialisation. Nothing changed on removing the debounce code.

desytech avatar Sep 22 '16 13:09 desytech

Just to confirm if the issue is related to HPM. Did you try to connect to the server directly? (without the proxy)

If is it HPM related; It can be either an issue in HPM configuration or a bug in HPM.

Try adding the option: changeOrigin: true Tornado might be refusing the request, since Host value in the request is different from the Tornado's host.

 devServer: {
      proxy: {
        '/hyperguard/websocket/*': {
          target: 'ws://localhost:8082',
          changeOrigin: true,
          ws: true
        }
      }
    }

chimurai avatar Sep 22 '16 20:09 chimurai

Direct connection

I think the issue is related to HPM, because if i request a websocket upgrade directly it always works as expected.

request (without HPM)

GET /hyperguard/websocket HTTP/1.1
Host: localhost:8082
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Sec-WebSocket-Version: 13
Origin: https://localhost:8082
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: 7XjMl4KmXDJa/TWfb7vabQ==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

immediatlely response (without HPM)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: iC7Eh/cX6YgchA7mJQ950DWmIiQ=

Workaround

I figured out a way to reproduce a workaround with tornado + HPM + webpack-dev-server. If the problem occurs i just have to request the websocket proxy path via http e.g http://localhost:8079/hyperguard/websocket

response

HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Length: 34
Date: Fri, 23 Sep 2016 09:13:38 GMT
Server: TornadoServer/4.4.1
Content-Type: text/html; charset=UTF-8
Connection: keep-alive

Can "Upgrade" only to "WebSocket".

If i go back to my working url e.g. http://localhost:8079 the websocket connection can be upgraded as expected.

Q&R

  • Adding changeOrigin option has no effect.
  • Tornado actually allows alternate origins.

desytech avatar Sep 23 '16 10:09 desytech

Can you try setting your target with the http protocol:

target: 'http://localhost:8082'

chimurai avatar Sep 23 '16 11:09 chimurai

Changing the protocol of the target option has no effect. I have to debug the issue in-depth. It cannot be excluded that the problem occures on tornados side.

desytech avatar Sep 23 '16 12:09 desytech

Think I've exhausted the common configurations. :)

FYI: HPM uses http-proxy to do the actual proxying. (https://github.com/nodejitsu/node-http-proxy) Worth checking to see if you'll get the same issue when you're just using http-proxy.

chimurai avatar Sep 23 '16 12:09 chimurai

It looks like I get this with webpack-dev-server 1.16.2 also. I get this on the server:

[HPM] GET /websocket -> ws://localhost:8080/websocket
[HPM] Upgrading to WebSocket

Chrome reports:

(index):37 WebSocket connection to 'ws://127.0.0.1:8888/websocket' failed: Connection closed before receiving a handshake response

When using Fiddler it reports:

[Fiddler] ReadResponse() failed: The server did not return a complete response for this request. Server returned 0 bytes.

I'm using a similar config:

proxy: {
    '/websocket': {
        ws: true,
        target: 'ws://localhost:8080/websocket',
        logLevel: 'debug'
    }
}

One thing I do see which hasn't been reported before is nothing happens if I perform raw websocket connection on a cold start, it just hangs. I only get the above output if I first attempt a basic GET to http://127.0.0.1:8888/websocket first which appears to warm-up HPM. I have tried connecting directly to ws://localhost:8080/websocket and it does work, I've also tried changing ws:// to http:// to no avail.

The closest issue I've found in the http-proxy repo is nodejitsu/node-http-proxy#577 but it's really old. nodejitsu/node-http-proxy#891 might also be related. I've also raised this on StackOverflow.

dansiviter avatar Nov 16 '16 11:11 dansiviter

@dansiviter, could you try to change your target to ws://localhost:8080? AFAIK the path /websocket already gets appended (unless you use the pathRewrite option).

SpaceK33z avatar Nov 16 '16 11:11 SpaceK33z

Thanks, one step closer as that worked. However, I still need to warm up the connection with a GET before it'll connect. Any ideas?

For anyone who needs it my config is:

proxy: {
    '/websocket': {
        ws: true,
        target: 'http://localhost:8080'
    }
}

Which ultimately proxies to ws://localhost:8080/websocket so appending the path and changing the scheme automatically.

dansiviter avatar Nov 16 '16 11:11 dansiviter

debugged that problem weeks ago rudimentary, it seems that the http proxy middleware which is applied to the express server never be executed. maybe a side effect with sockjs which is used to handle the hot realoading websocket channel.

desytech avatar Nov 16 '16 12:11 desytech

@dansiviter this SO answer might be helpful: http://stackoverflow.com/a/32943389/465887

I'm working on enabling websocket proxying in Create React App (https://github.com/facebookincubator/create-react-app/issues/1013) and it looks like I can manually watch for the "upgrade" request coming from the devServer, and then call HPM's "upgrade" function.

CRA is using HPM directly though, instead of the devServer.proxy config, so it might not apply to your use case, but this is the code I'm using to set it up (so far it seems to be working but I haven't tested thoroughly yet):

    // Pass the scope regex both to Express and to the middleware for proxying
    // of both HTTP and WebSockets to work without false positives.
    var hpm = httpProxyMiddleware(pathname => mayProxy.test(pathname), {
      target: proxy,
      logLevel: 'debug',
      onError: onProxyError(proxy),
      secure: false,
      changeOrigin: true,
      ws: true
    });
    devServer.use(mayProxy, hpm);

    devServer.listeningApp.on('upgrade', hpm.upgrade);

dceddia avatar Nov 23 '16 02:11 dceddia

I had problems with the proxy websocket closing right after connecting. Found out that socket io was closing unhandled requests. If you are using socket io, the fix is to set the destroyUpgrade option to false:

var express = require('express');
var app = express();
var http = require('http').Server(app);
var proxy = require('http-proxy-middleware');
var io = require('socket.io')(http, {destroyUpgrade: false});

mixxen avatar Nov 28 '16 14:11 mixxen

Also seeing this issue. @mixxen's fix seems to have worked for me.

dcartertwo avatar Mar 15 '17 17:03 dcartertwo

Apologies for commenting on an ancient ticket, but I managed to hit this issue (ws proxy only starts working after manually performing a GET call).

app.use(
    '/ws/live',
    createProxyMiddleware({
        target: process.env.API,
        ws: true, // enable websocket proxy
        changeOrigin: true,
        logLevel : 'debug'
    })
);

... "http-proxy-middleware": "^1.0.6", "react": "^17.0.1", "react-scripts": "4.0.0", ....

I understand that no resolution was found, but does anyone know any workaround (better than manually doing a GET, I mean)?

torrejonv avatar Nov 14 '20 05:11 torrejonv

Same here. AFAIU issue on the middleware happens if server is trying to send data socket which has already been closed, but I haven't dig it enough yet. I'll try to find more details, just wanted to underline that issue persists in some conditions.

adolgoff avatar Nov 25 '20 22:11 adolgoff

Got stuck with this issue in my CRA app. I have changed my setupProxy.js to the following and It looks like it's working for me:

app.use(
    '/',
    createProxyMiddleware('/ws', {
      target: process.env.API,
      changeOrigin: true,
      secure: false,
      ws: true,
    })
  );

So that the request to React App itself works as a "first manual GET" for the WebSocket (it actually registers the "upgrade" handler)

limion avatar Oct 10 '22 15:10 limion

Got stuck with this issue in my CRA app. I have changed my setupProxy.js to the following and It looks like it's working for me:

app.use(
    '/',
    createProxyMiddleware('/ws', {
      target: process.env.API,
      changeOrigin: true,
      secure: false,
      ws: true,
    })
  );

So that the request to React App itself works as a "first manual GET" for the WebSocket.

I am not entirely sure how this works... but it does! Thank you for sharing this workaround!

torrejonv avatar Oct 10 '22 15:10 torrejonv

@torrejonv from my understanding it works like lazy loading. Until you make a request to the route where proxy middleware is registered there will be no "upgrade" handler added to the HTTP server. With this ☝️ approach, any request passes through the proxy middleware so that we have the "upgrade" handler registered from the very beginning.

limion avatar Oct 11 '22 07:10 limion

@torrejonv from my understanding it works like lazy loading. Until you make a request to the route where proxy middleware is registered there will be no "upgrade" handler added to the HTTP server. With this ☝️ approach, any request passes through the proxy middleware so that we have the "upgrade" handler registered from the very beginning.

Thank you for the explanation. Very ingenious indeed!

torrejonv avatar Oct 11 '22 07:10 torrejonv