taoensso / sente

Realtime web comms library for Clojure/Script
https://www.taoensso.com/sente
Eclipse Public License 1.0
1.74k stars 193 forks source link

Ability to control auto retry based on error #405

Closed Jomagus closed 2 years ago

Jomagus commented 2 years ago

Is there a good way to stop sente from auto retrying to establish a connection based on the error that triggered the retry?

In my case I have a server that requires auth. If the given auth is invalid it blocks the WebSocket or returns 401 Unauthorized via HTTP. But when a sente client tries to connect without valid auth (for example because someone reloaded a guarded url with an expired session) it does not expose that error and instead tries to connect in vain. First sente tries to establish a websocket connection but fails. Then it falls back to ajax, but that also does not succeed because of the 401. Then it simply tries again and again with its backoff.

My Problem is that I do not get access to the underlying error, so I can not disconnect sente manually and redirect to a login page. One can watch for :chsk/state events, but the client state only includes the :last-ws-error, :last-ws-close (both only applicable to ws connections and give the rather inexpressive error 1006) and the :last-close key. The :last-close is :downgrading-ws-to-ajax in my given example, which could also have other reasons.

I currently only see two options: To always fire my own http request to ensure valid auth before trying to establish any sente connection or to extract the http error deep from within sente internals:

(let [chsk-client (make-channel-socket-client!
                    endpoint
                    js/csrfToken
                    {:type :auto})]
  (start-chsk-router!
   (:ch-recv chsk-client)
   (fn [{:keys [id]}]
     (when (= id :chsk/state)
       (-> chsk-client
           :chsk
           :impl_
           deref
           :curr-xhr_
           deref
           (. getLastErrorCode)
           (not= goog.net.ErrorCode/NO_ERROR)
           (when
            (chsk-disconnect! (:chsk chsk-client))))))))

The first way seems wasteful/redundant and the second just plain wrong.

I don't know what the best way is to solve this problem. Is it to expose the underlying http error directly in the client state just like the ws error? Or is a :should-retry-fn as a make-channel-socket-client! option better? Or is there already a way to solve my problem that I just did not see?

ptaoussanis commented 2 years ago

@Jomagus Hi there! So there's 2 broad approaches to handling auth with Sente:

  1. Put the entire channel socket behind authentication. a. Use a standard HTTP request to ensure authentication before trying to open a channel socket. b. Use the channel socket itself for authentication.
  2. Put specific resources behind authentication.

It sounds like you're currently trying to do # 1 (in particular you'd like to do # 1b).

My preference is usually to do # 2 instead. I.e.:

Some reasons for this preference include:

Off hand I can't think of any specific reason why one might prefer # 1b to # 2, and (as you observed) # 1b would involve some non-trivial changes and extra complexity - so would recommend trying #2 if that sounds reasonable?

Jomagus commented 2 years ago

Hey, thank you very much for the fast and detailed answer!

If I understand you correctly, I think that my problems stems from wanting to do 1 and 2 at the same time. I want an entire channel socket behind authentication, because the channel socket itself is a specific resource in my application.

Basically I have different chatrooms[^1], each backed by it's own channel socket. On the home page users can login (with standard HTTP Post against an login endpoint) and only after successful auth the user is redirected to the chatroom, with the chatroom id being part of the url/route. (And that url/route in turn is wrapped with ring middleware in the router on the server to only allow authenticated access.)

My problem manifested, when users open a specific chatroom url directly without valid auth. I programmed the client to always establish a sente connection when opening a chatroom url. (So that if a user closes the browser tab by accident and reopens it, the connection gets reestablished automatically since the auth cookie is still valid.[^2]) This seemed very elegant at the time, since all chatroom communication is done fully over the specific channel socket backing that chatroom. But when the auth is not valid, I wanted to redirect them to the home page and got stuck.

Treating every client that is connected against a specific channel socket as authenticated for the specific chatroom had additional benefits for me, like easier server broadcasts (just a message to every connected client, since they are all in the same room and allowed to receive them) and not having to map user ids (connected to a single shared channel socket) to chatrooms (in which they are currently active).

With that in mind I will probably use a standard HTTP Request to check auth validity before establishing a channel socket connection if the navigation to the specific room was not done by my own frontend but instead was done via the browser url.

Switching to 2 like you suggested would require me to redesign quite a bit of the application and I got 1a effectivly already implemented. Under the assumption that I only use a single channel socket I would definitely leave the socket itself unauthenticated like you suggested.

I hope I don't wasted your time with this, but I wanted to explain my use case in this answer, since maybe others in the future stumble across this thread or my case is one you hadn't yet thought about.

[^1]: In the app a chatroom is used per university lecture for audience interaction. Because of this a user is never needed to be able to be in more than one room at a time or to interact with other rooms. [^2]: This is also is the case if a student bookmarks a room for the lecture, since rooms may be reused throughout the term.

ptaoussanis commented 2 years ago

No problem, and likewise thanks for the thoughtful and well-written reply! (Relatedly: this is the first time I've seen footnotes being used on GitHub, didn't even know that was a feature- absolutely going to start using these myself!)

I'd probably suggest that a single global channel socket with app-specific state (e.g. mapping user ids to chat rooms) is more flexible in the long-term (e.g. if you might later want to allow a single user to be in multiple rooms, or if you want to easily broadcast a system message to all users in all rooms, etc.).

App-specific state can also be handy if you want to track common things like how long ago each user was last active so that you can mark folks as online/idle/etc. Sente's out-the-box connected clients state is intentionally minimal.

Having said that, your current approach is certainly reasonable- especially if you're not expecting to want the extra flexibility.

With that in mind I will probably use a standard HTTP Request to check auth validity before establishing a channel socket connection if the navigation to the specific room was not done by my own frontend but instead was done via the browser url.

That also sounds reasonable.

I hope I don't wasted your time with this

Not at all :-)

but I wanted to explain my use case in this answer, since maybe others in the future stumble across this thread or my case is one you hadn't yet thought about.

Much appreciated, thanks! There's not a lot of documentation currently available re: auth patterns, so it'll be handy to have this searchable in the issues 👍

Best of luck with the app! Feel free to close if you're happy, or follow-up if there's anything else I can assist with.