In my application, I have a Main.elm that receives updates from many submodules. If a submodule receives a 401 error, I would like the update in Main.elm to redirect the user to login, and remove the stored session token.
The architecture of my application is similar to the one in elm-spa-example 1, where my update in main takes a msg that consist of the submodule, the submodule message and the response, e. g. “SubA SubA.msg response”. How can i match if the response is Http.Err Http.Badstatus 401 for every request one place, and if so, redirect to login? One option would be to see if the request is 401 either in every submodule or for every msg update in every submodule, but that seems like a bad option given the amount of copy-paste.
Based on my google results, one option might seem to be using Http.task with resolver, but I’m not convinced that is the best option either.
If you want to use a centralized approach to handling something that happens in multiple places in your app, you need to provide an upward flow of information.
The way I do it is to have each page that can generate events that need to be handled globally have an update that returns (Model, Cmd Msg, Context.Msg) where the Model and the Msg are defined in the page and the Context.Msg is defined in a Context.elm file. An alternative name for Context is Session or SharedState. I usually have the user auth data in the Context state together with some configurations and some cache of the database data that allows me to avoid some calls to the backend.
In most update branches from the pages I return Context.Pass as a message that’s a no-op on the Context state (e.g. (newModel, someCmd, Context.Pass) ). In your 401 case, each page that throws that could return a Context.Relogin msg that would wipe the auth data and allow the main update to redirect.
Both of the previously mentioned solutions are much too convoluted for what you want.
Simply represent your page state at the application level as the union of
type PageState model
= Loading
| Loaded model
| Error SomeErrorType
Then the application can handle the error states, in this case if the Error is Error Unauthorized or whatever else you want.
This also gives you the benefit of being able to conveniently render either a centralized or page specific loading message, and not have your sub pages handle anything other than loaded data.
Well I have something like that recently and what I did was add a /logout URL that have a simple view with a loading CSS animation. Then I can also use that view to clean up the credentials in case I detect they are outdated and that helps me to differentiate (for a different reason) if the 401 is cause because a workflow cause or because the user credentials are invalid but I think that’s out of the scope of your question.
In a couple applications I’ve tracked api rate limits by using Response Msg msg to track in-flight requests. So instead of specifying your response tagger as BlogPosts it would be Response << Blogposts
However I was able to treat the entire sub message as a black box and just call update again. You would have to see if Response (msg (Result Http.Error data)) would work out. Looking at that probably not. Perhaps Response ((Result Http.Error data) -> Msg) (Result Http.Error data), passing the actual message as a parameter.
Do you use a Browser.application route for this? Seems like quite a clean way of doing it, then the child module wanting to logout just issues a Cmd to go to that route.
Sure you can put in an href link for a logout control in the UI, but the OP is doing it in response to getting a 401 (or maybe 403) code back from the server - so in that case just issue a Cmd to navigate to the route?
My way of doing it is taking @pdamoc’s idea even further - I fully encapsulate the auth session state in its own package, which I then re-use accross my various projects:
I like this because I put it all in a re-usable package with a simple to use API. Whenever I get a 401/403, an unauthed : Cmd Msg is created. The actual Auth API details and things like token refreshes is all hidden within the Auth package.
Update: I’ve edited my original post. I moved it here as it was probably too verbose.
And I uploaded my example code here
As an alternative to passing an external message back to the parent
(which I think is certainly a good option!) you could allow the Main.elm to look at your child page messages.
In general this is not a good practice but if it’s done in a controlled way I think it could be considered.
(note: this is just an approach I drafted out quickly as an example for this post and I haven’t really tried it in a real application)
Disadvantages
You need to expose the internals of your Msg from your child module Msg(..)
You still need to do it for every page in the Main.elm update case switch…
Advantages
Detecting 401 and 403 could be done by a generic function: AuthMsg#withAuth in my example. This is used when the HTTP GET request is executed (SomePage.elm#someHttpGet)
You can deal with the two authentication error messages globally in your Main.elm
Main.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
SomePageMsg (SomePage.AuthFailMsg failType) ->
case failType of
AuthMsg.Unauthenticated -> handleUnauthenticated model
AuthMsg.Unauthorized -> handleUnauthorized model
SomePageMsg somePageMsg -> ...
Thank you for all the responses and suggestions! Did not anticipate this many.
If I understand most of the suggestions correctly, it would require me to rewrite many of my updates to return (Model, Cmd Msg, Cmd Auth.Msg) or similar, and that I would need to handle the 401-error at least to some degree in every module. At the moment, I return (Model, Cmd Msg) from my modules, thought I have been thinking about changing it so that I have one message that is to be handled separately.
Something close to pdamoc might be a good solution if I end up doing that, but the solution suggested by rlopezc sound like more what I was hoping for, where I only need to intersect one place, instead of changing every module. So I will try to look at how to integrate the parseHttpError with my code, and see if that works.
EDIT: after taking a closer look, it seems like I would need to both call the parseHttpError and see if it is a 401, and if not handle the error messages that was returned in the response. Given that this would also require adding some logic in every module, I will instead try to look into the shared-state example pdamoc suggested, since i have also thought about using something similar in other cases.
Another option would be to convert the error messages to a custom error and handle that in your code.
In other words, express in a type the fact that you expect a specific set of errors.
For example, you could have:
type Error
= Unauthenticated
| ConnectionProblems
| ServerError String
You would then convert the Http.Error to one of these errors an react in you code to them. (i.e. redirect for Unauthenticated, suggest checking connection/retry for ConnectionProblems and log to Rollbar the ServerError.
I ended up doing a mix of suggested solutions, by having an errorHandler that checked what kind of error it was, and that was called in every update-method where an error was returned. I then added a ‘’’ case of ‘’’ in each of the up-date methods, where I redirected if it was an ‘’’ Unauthenticated ‘’’.
I don’t think it is the best solution, because I feel I had to copy paste a lot, but it works for now.