How to gracefully re-connect websocket? - javascript

I'm building a singe-page application using HTTP and Websockets. The user submits a form and I stream a response to the client. Below is a snippet client.
var html = `<!DOCTYPE html>
<meta charset="utf-8">
<head>
</head>
<body>
<script>
var ws = new WebSocket("ws://localhost:8000/ws")
ws.onmessage = function(e) {
document.getElementById("output").innerHTML += e.data + "<br>"
}
function submitFunction() {
document.getElementById("output").innerHTML += ""
return false
}
</script>
<form
enctype="multipart/x-www-form-urlencoded"
action="http://localhost:8000/"
method="post"
>`
This is the server. If the request is not a POST, I write/render the html (parseAndExecute) which establishes a new websocket connection. If the request is a POST (from the form), then I start processing and eventually write to the websocket.
func (c *Config) ServeHtml(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
//process
channel <- data
}
c.parseAndExecute(w)
}
func (sh *SocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
w.Write([]byte(fmt.Sprintf("", err)))
return
}
//defer ws.Close()
// discard received messages
go func(c *websocket.Conn) {
for {
if _, _, err := c.NextReader(); err != nil {
c.Close()
break
}
}
}(ws)
data <- channel
Everything works as I expect only if I do not refresh the page. If I don't refresh, I can keep submitting forms and see the different outputs come in line by line. To clarify, it actually only works if the page is already up so that parseAndExecute is never called. This function parses and executes html/template creating a new websocket client.
Any refresh of the page or initially browsing localhost:8000 would cause websocket: close sent on the server.
I'm not sure how to resolve this. Does the server to need to gracefully handle disconnections and allow re-connects? Or does the client need to do something? It seems like the server should upgrade any connection at /ws so it shouldn't matter how many new websocket clients are made but obviously my understanding is wrong.
I'm not closing the websocket connection on the server because it should be up for as long as the program is running. When the user stops the program, I assume it'll be automatically closed.
Full SocketHandler code:
func (sh *SocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
w.Write([]byte(fmt.Sprintf("", err)))
return
}
// discard received messages
go func(c *websocket.Conn) {
for {
if _, _, err := c.NextReader(); err != nil {
c.Close()
break
}
}
}(ws)
cmd := <-sh.cmdCh
log.Printf("Executing")
stdout, err := cmd.StdoutPipe()
if err != nil {
w.Write([]byte(err.Error()))
return
}
defer stdout.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
w.Write([]byte(err.Error()))
return
}
defer stderr.Close()
if err := cmd.Start(); err != nil {
w.Write([]byte(err.Error()))
return
}
s := bufio.NewScanner(io.MultiReader(stdout, stderr))
for s.Scan() {
err := ws.WriteMessage(1, s.Bytes())
if err != nil {
log.Printf("Error writing to client: %v", err)
ws.Close()
}
}
if err := cmd.Wait(); err != nil {
w.Write([]byte(err.Error()))
return
}
log.Println("Done")
}

Websocket server applications must handle errors on the connection by closing the connection and releasing resources associated with the connection.
Browsers close websocket connections when the containing page is refreshed. The server will eventually get a read or write error after the browser closes the connection.
Connection close is one of several possible errors that the server might encounter. The server application should handle all errors on the connection the same way: close the connection and release resources associated with the connection.
The typical application design is for the client to connect on page load and to reconnect (with backoff) after an error. The server assumes that clients will connect and disconnect over time.
The JS code can be improved by adding an onerror handler that reconnects with backoff. Depending on the application, you may also want to display UI indicating the connection status.
The Go code does not close the connection in all scenarios. The running command is the resource associated with the connection. The application does not kill this program on connection error. Here are some fixes:
Add defer ws.Close() after successful upgrade. Remove other direct calls to ws.Close() from SocketHandler.ServeHTTP. This ensures that ws.Close() is called in all scenarios.
Kill the command on exit from the read and write pumps. Move the read pump to after the command is started. Kill on return.
go func(c *websocket.Conn, cmd *exec.Command) {
defer c.Close()
defer cmd.Process.Kill()
for {
if _, _, err := c.NextReader(); err != nil {
break
}
}
}(ws, cmd)
Kill the command on exit from the write pump:
s := bufio.NewScanner(io.MultiReader(stdout, stderr))
for s.Scan() {
err := ws.WriteMessage(1, s.Bytes())
if err != nil {
break
}
}
cmd.Process.Kill()
I have not run or tested this code. It's possible that some of the details are wrong, but this outlines the general approach of closing the connection and releasing the resource.
Take a look at the Gorilla command example. The example shows how to pump stdin and stdout to a websocket. The example also handles more advanced features like checking the health of the connection with PING/PONG.

Related

Making a client side Websocket connection request to a Go HTTP endpoint that authenticates and upgrades request to WS

The title is basically the question I have. I was developing my Go server, and added the gorilla/ws library to the endpoint http://qwerty:1234/api/ws to handle connection upgrade requests.
This endpoint has a middleware before it that validates the Auth header's bearer token.
While developing I could customize the WS connection request in Postman to include the Auth header. And it successfully connected to the WS endpoint.
I'm new to WS. And I tried to develop the client side WS connection, lo, you can't add headers.
Is there a workaround to do what I am trying? Or is there a way to emulate what Postman does? Is there a Javascript WS library that I can use to do what I am trying?
Server code
// Router
r.Route("/api", func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(utils.AuthMiddleware)
r.Get("/contacts", controller.GetContacts)
r.Get("/ws", websocket.Handler)
r.Get("/ws/clients", websocket.TestHandler)
// Socket connection upgrade
func Handler(w http.ResponseWriter, r *http.Request) {
userDetails := r.Context().Value("userDetails").(jwt.MapClaims) // Populated by middleware
userId := int64(userDetails["UserId"].(float64))
if _, userPresent := wsClientsById[userId]; userPresent {
http.Error(w, "User already connected", http.StatusForbidden)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, "Error establishing Websocket connection", http.StatusInternalServerError)
fmt.Println(err)
return
}
conn.SetReadLimit(4096)
wsClientsById[userId] = conn
wsClientsByConn[conn] = userId
go wsConnHandler(conn)
}

How to properly refuse websocket upgrade request?

Sometimes I want to refuse a http client's request to upgrade connection to websocket.
Code
(using go's Gin and gorilla/websocket framework:)
To allow upgrade:
c, err := ctl.upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
err = c.WriteJSON(resp)
To refuse upgrade (due to invalid request params):
if contentId == "" || !exists {
// FIXME: provide response in a way that ws client can recognize & show tip?
ctx.String(http.StatusBadRequest, "invalid or non-existing contentId")
return
}
Explaination: Here to refuse the upgrade I just return a http 400 code, then terminate the connection, and didn't do the upgrade at all.
The issue
The problem to refuse websocket upgrade request with about code is that, the websocket client (e.g js), can't read data (text or json) in my response.
Code - client-side (js):
ws.onerror = function (evt) {
// TOOD: handle error, (e.g print error msg?),
print("ERROR");
}
It does print the "ERROR" on refuse, but after checking chrome developer tool about the evt object, can't find a way to get server response data, so I can't show tip to frontend UI with reason of refuse.
Questions
How to refuse websocket upgrade request properly, and let client be able to receive the returned reason/data ? (e.g client is js, server is go / gin / gorilla/websocket).
Is there a better way to refuse websocket upgrade request, other than return http code like 400?
To reject a websocket connection, do not upgrade the connection as described in the question.
The browser API does not provide information about why the connection was rejected because the information can violate the same-origin policy.
Do the following to send an error reason back to the client application or user:
Upgrade the connection.
Send a close message with the error reason.
Close the connection.
Here's an example:
c, err := ctl.upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
// TODO: handle error
}
if contentId == "" || !exists {
c.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.ClosePolicyViolation,
"bad content id or not exist"))
c.Close()
return
}
// Continue with non-error case here.
Access the reason from the close handler in JS:
ws.onclose = function (evt) {
if (evt.code == 1008) { // 1008 is policy violation
console.log(evt.reason)
}
}

Redis (ioredis) - Unable to catch connection error in order to handle them gracefully

I'm trying to gracefully handle redis errors, in order to bypass the error and do something else instead, instead of crashing my app.
But so far, I couldn't just catch the exception thrown by ioredis, which bypasses my try/catch and terminates the current process. This current behaviour doesn't allow me to gracefully handle the error and in order to fetch the data from an alternative system (instead of redis).
import { createLogger } from '#unly/utils-simple-logger';
import Redis from 'ioredis';
import epsagon from './epsagon';
const logger = createLogger({
label: 'Redis client',
});
/**
* Creates a redis client
*
* #param url Url of the redis client, must contain the port number and be of the form "localhost:6379"
* #param password Password of the redis client
* #param maxRetriesPerRequest By default, all pending commands will be flushed with an error every 20 retry attempts.
* That makes sure commands won't wait forever when the connection is down.
* Set to null to disable this behavior, and every command will wait forever until the connection is alive again.
* #return {Redis}
*/
export const getClient = (url = process.env.REDIS_URL, password = process.env.REDIS_PASSWORD, maxRetriesPerRequest = 20) => {
const client = new Redis(`redis://${url}`, {
password,
showFriendlyErrorStack: true, // See https://github.com/luin/ioredis#error-handling
lazyConnect: true, // XXX Don't attempt to connect when initializing the client, in order to properly handle connection failure on a use-case basis
maxRetriesPerRequest,
});
client.on('connect', function () {
logger.info('Connected to redis instance');
});
client.on('ready', function () {
logger.info('Redis instance is ready (data loaded from disk)');
});
// Handles redis connection temporarily going down without app crashing
// If an error is handled here, then redis will attempt to retry the request based on maxRetriesPerRequest
client.on('error', function (e) {
logger.error(`Error connecting to redis: "${e}"`);
epsagon.setError(e);
if (e.message === 'ERR invalid password') {
logger.error(`Fatal error occurred "${e.message}". Stopping server.`);
throw e; // Fatal error, don't attempt to fix
}
});
return client;
};
I'm simulating a bad password/url in order to see how redis reacts when misconfigured. I've set lazyConnect to true in order to handle errors on the caller.
But, when I define the url as localhoste:6379 (instead of localhost:6379), I get the following error:
server 2019-08-10T19:44:00.926Z [Redis client] error: Error connecting to redis: "Error: getaddrinfo ENOTFOUND localhoste localhoste:6379"
(x 20)
server 2019-08-10T19:44:11.450Z [Read cache] error: Reached the max retries per request limit (which is 20). Refer to "maxRetriesPerRequest" option for details.
Here is my code:
// Fetch a potential query result for the given query, if it exists in the cache already
let cachedItem;
try {
cachedItem = await redisClient.get(queryString); // This emit an error on the redis client, because it fails to connect (that's intended, to test the behaviour)
} catch (e) {
logger.error(e); // It never goes there, as the error isn't "thrown", but rather "emitted" and handled by redis its own way
epsagon.setError(e);
}
// If the query is cached, return the results from the cache
if (cachedItem) {
// return item
} else {} // fetch from another endpoint (fallback backup)
My understanding is that redis errors are handled through client.emit('error', error), which is async and the callee doesn't throw an error, which doesn't allow the caller to handle errors using try/catch.
Should redis errors be handled in a very particular way? Isn't it possible to catch them as we usually do with most errors?
Also, it seems redis retries 20 times to connect (by default) before throwing a fatal exception (process is stopped). But I'd like to handle any exception and deal with it my own way.
I've tested the redis client behaviour by providing bad connection data, which makes it impossible to connect as there is no redis instance available at that url, my goal is to ultimately catch all kinds of redis errors and handle them gracefully.
Connection errors are reported as an error event on the client Redis object.
According to the "Auto-reconnect" section of the docs, ioredis will automatically try to reconnect when the connection to Redis is lost (or, presumably, unable to be established in the first place). Only after maxRetriesPerRequest attempts will the pending commands "be flushed with an error", i.e. get to the catch here:
try {
cachedItem = await redisClient.get(queryString); // This emit an error on the redis client, because it fails to connect (that's intended, to test the behaviour)
} catch (e) {
logger.error(e); // It never goes there, as the error isn't "thrown", but rather "emitted" and handled by redis its own way
epsagon.setError(e);
}
Since you stop your program on the first error:
client.on('error', function (e) {
// ...
if (e.message === 'ERR invalid password') {
logger.error(`Fatal error occurred "${e.message}". Stopping server.`);
throw e; // Fatal error, don't attempt to fix
...the retries and the subsequent "flushing with an error" never get the chance to run.
Ignore the errors in client.on('error', and you should get the error returned from await redisClient.get().
Here is what my team has done with IORedis in a TypeScript project:
let redis;
const redisConfig: Redis.RedisOptions = {
port: parseInt(process.env.REDIS_PORT, 10),
host: process.env.REDIS_HOST,
autoResubscribe: false,
lazyConnect: true,
maxRetriesPerRequest: 0, // <-- this seems to prevent retries and allow for try/catch
};
try {
redis = new Redis(redisConfig);
const infoString = await redis.info();
console.log(infoString)
} catch (err) {
console.log(chalk.red('Redis Connection Failure '.padEnd(80, 'X')));
console.log(err);
console.log(chalk.red(' Redis Connection Failure'.padStart(80, 'X')));
// do nothing
} finally {
await redis.disconnect();
}

Is it possible to check for WebSocket 403 responses?

Is it possible to programmatically check if a WebSocket connection failed with a 403 response? For instance, with the following server code:
package main
import (
"errors"
"io"
"log"
"net/http"
"golang.org/x/net/websocket"
)
func main() {
handler := websocket.Handler(func(ws *websocket.Conn) {
io.Copy(ws, ws)
})
handshake := func(conf *websocket.Config, req *http.Request) error {
if req.URL.Path == "/sekret" {
return nil
}
return errors.New("Oops!")
}
server := &websocket.Server{
Handshake: handshake,
Handler: handler,
}
log.Fatal(http.ListenAndServe(":8080", server))
}
And the following sample JS connection that triggers a 403 response:
var ws = new WebSocket("ws://localhost:8080/something");
ws.onerror = console.log;
The error response is an Event with type "error". On the other hand, the following JS code triggers a net::ERR_CONNECTION_REFUSED (at least on Chrome):
var ws = new WebSocket("ws://localhost:8081/something");
ws.onerror = console.log;
The error event object looks almost exactly the same. Is there some way to distinguish error types in WebSocket connections from the client side? (I'm specifically looking for 403 responses, so I can alert the user to take special action.)
Apparently it's deliberate:
My understanding is that for security reasons, WebSocket error statuses are fairly restricted to limit the ability of malicious JS to probe an internal network.

Websocket handshake fails 404 (golang server)

I have a simple go web server which serves on port localhost:8080 an public folder containing both an html file as well as a client script with websocket logic.
in my main.go file
listener, err := net.listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
//full code in gist https://gist.github.com/Kielan/98706aaf5dc0be9d6fbe
then in my client script
try {
var sock = new WebSocket("ws://127.0.0.1:8080");
console.log("Websocket - status: " + sock.readyState);
sock.onopen = function(message) {
console.log("CONNECTION opened..." + this.readyState);
//onmessage, onerr, onclose, ect...
}
I get the error in chrome
WebSocket connection to 'ws://127.0.0.1:8080/' failed: Error during WebSocket handshake: Unexpected response code: 200
and Firefox
Firefox can't establish a connection to the server at ws://127.0.0.1:8080/.
I found this article referring to node.js indicating to add /websocket to my client websocket string, though it did not solve the problem and resulted in a 404
I thought response code 200 is good, do I need to convert the request to a websocket somehow and maybe it is defaulting to http? If so how can I do this?
Like JimB pointed out, you are not handling http nor websocket connections yet.
You can do websocket handling with the package github.com/gorilla/websocket
This is how a simple setup could look like:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
// wsHandler implements the Handler Interface
type wsHandler struct{}
func main() {
router := http.NewServeMux()
router.Handle("/", http.FileServer(http.Dir("./webroot"))) //handles static html / css etc. under ./webroot
router.Handle("/ws", wsHandler{}) //handels websocket connections
//serving
log.Fatal(http.ListenAndServe("localhost:8080", router))
}
func (wsh wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// upgrader is needed to upgrade the HTTP Connection to a websocket Connection
upgrader := &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
//Upgrading HTTP Connection to websocket connection
wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("error upgrading %s", err)
return
}
//handle your websockets with wsConn
}
In your Javascript you then need var sock = new WebSocket("ws://localhost/ws:8080"); obviously.

Categories