Handle 401 response

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.

1 Like

In the app at my work we monkey patch XMLHttpRequest to listen for 401s and then send in through a port when one happens.

1 Like

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.

You can look at https://github.com/ohanhi/elm-shared-state for more documentation of this approach.

4 Likes

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.

1 Like

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.

1 Like

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.

Yep once you want to logout you don’t even need a Cmd you can just add an old and nice href link

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?

Yes the init function triggers the clean of the local storage and the new url Cmds

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:

https://package.elm-lang.org/packages/the-sett/auth-elm/latest/

This gives me a set of constructors that can be used by a project using it to issue Cmds to make changes to the auth state:

login : Credentials -> Cmd Msg
refresh : Cmd Msg
logout : Cmd Msg
unauthed : Cmd Msg

The auth module has its own Msg type, so requires the top-level module to lift that into the auth module, by wrapping it in the top-level Msg type:

type TopMsg = 
    AuthMsg Auth.Msg
  | OtherMsgs ...

and also for the top-level module to hold the auth state and take note of changes.

Other pages can output Auth commands, and usually have an update signature like this to allow them to be returned:

module ChildPage exposing (..)

update : Msg -> Model -> (Model, Cmd Msg, Cmd Auth.Msg)

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.

1 Like

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(..)
  • In my example I need to do some wrapping of the messages in the child page
  • 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 -> ...

Hello, I also had this question when trying to reduce bolierplate. What I would do is to create a function to parse any HTTP error.

-- Error module
parseHttpError : Http.Error -> model -> ( model, Cmd msg )
parseHttpError httpError model =
    case httpError of
        Http.BadStatus 401 ->
            ( model
            , Cmd.batch [ Api.logout, Route.replaceUrl Route.Login ]
            )

        _ ->
            ( model
            , Cmd.none
            )

And usage in update :

CompletedLogin (Err httpError) ->
    Error.parseHttpError httpError model

I pass the model in case I want to do some updates to the model, like getting the 422 error status code and parsing the body to get invalid fields.

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.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.