Building cross-site (CORS) Socket.IO/WebSocket in Go on GKE, Google Cloud Platform (GCP)

If you are going to build your own Socket.IO server to keep the connections between your clients and servers via WebSocket in Golang instead of NodeJS. Here are our stories that may save your time and energy.

At the very beginning, you will get two packages after you did quick searches on Google, those are github.com/gorilla/websocket and github.com/googollee/go-socket.io. One is for WebSocket and the other is for Socket.IO.

Since Socket.IO has been a while in our tech stack and we believe that Socket.IO will help us avoid the legacy protocol and some connection routine issues. We decided to use Socket.IO as our solution for the coming project due to the rush schedule, we didn't spend much time on this topic.

For Socket.IO in Go, it's very easy to make a workable example for both client and server sides by following the README of github.com/googollee/go-socket.io. And after thinking about the design for connection management (Using queue and pubsub pattern), you will jump to conclusions easily and think the POC (Proof of Concept) is done.

Note: We initialize the connection on client-side like this,
socket = io('https://a.com', {
  path: '/oursocketio',
  transports: ['websocket'],
});

// Only using polling when reconnect_attempt
socket.on('reconnect_attempt', () => {
  socket.io.opts.transports = ['polling', 'websocket'];
});
Then the story will begin...
WebSocket connection failed: Error during WebSocket handshake: Unexpected response code: 400
After googling, you may consider this error is a CORS problem. That is a common issue if you implement your Socket.IO/WebSocket server on domainA.com and you want to allow domainB.com to connect with your server. (If you use pure WebSocket client to connect to a Socket.IO server, you may get the similar error due to the implementation.)

Then you will try adding the Access-Control-Allow-OriginAccess-Control-Request-Method, etc into your response's header on the server-side.

And you won't forget the non-simple requests and their good friend, Preflight Requests. Long story short, for some security reasons, web browsers will send an OPTION request before a non-simple request for cross-site resource sharing.

Since we had a middleware to check all the requests for authentication and authorization, we need to do a specific modification for OPTION requests to fix the "Preflight" issue.

Sometimes, the annoyed CORS issue will still be there to bother you after adding a lot of "Access-*" headers into your requests. At this moment, you can probably check this MDN page carefully. Maybe you will find out the clues like this:
However, if the request is one that triggers a preflight due to the presence of the Authorization header in the request, you won’t be able to work around the limitation using the steps above. And you won’t be able to work around it at all unless you have control over the server the request is being made to.
If you have an Authorization header (For example, Bearer token.) in your HTTP requests. You should double confirm that you did not only set a wildcard (*) to Access-Control-Allow-Headers for Authorization as described below.
Note: for more details, check this Access-Control-Allow-Headers - HTTP | MDN.

The next you may get
Error during WebSocket handshake: Unexpected response code: 403
When you think you finally finished the CORS issue.

If you are using go-socket.io v1.4.2 or older versions. You need to override the (websocket.Default).CheckOrigin() like this for checking the origin.
func NewSocketServer(ctx context.Context, mux *http.ServeMux) error {
    pt := polling.Default

    wt := websocket.Default
        wt.CheckOrigin = func(req *http.Request) bool {
            // Check the origin here
            // We use "return true" here for demo, NOT RECOMMENDED. 
            return true
    }

    server, err := socketio.NewServer(&engineio.Options{
        Transports: []transport.Transport{
            pt,
            wt,
        },
    })

    if err != nil {
        return err
    }
    socketioSrv = server

Then we received many logs:
websocket: close 1006 (abnormal closure): unexpected EOF
It reminded us that we need to set up a backend config for increasing the timeout. But you still need to keep in mind you can not extend the timeout due to the documentation:
When sending WebSocket traffic to an HTTP(S) load balancer, the backend service timeout is interpreted as the maximum amount of time that a WebSocket, idle or active, can remain open.
At last, you can have your own SocketIO server happily ever after. (Hopefully.)

留言