How to gracefully handle HTTP responses indicating I need to refresh a token

I’m writing an Elm front end for a Drupal backend. Drupal has a REST API that requires me to pass in a token in the X-CSRF-Token header for POST/PUT/DELETE. The token is valid for 180 seconds. I will get a 403 response when the token is expired. I can then get a new token at /rest/session/token and then simply retry the request.

How do I handle this retry transparently in Elm? In an OO language this would have been easy: create a new http class, inheriting from HttpClient, override put()/post()/delete() and handle the retry. Done.

This doesn’t seem to be particularly easy with Elm. I currently have to handle this case (for every request) so far in my update function. So how do you do this in Elm but transparent, so the abstraction doesn’t leak into my Main.elm?

Some more details:

This is how I create a put request for example:

put : String -> String -> Http.Body -> Decoder a -> Http.Request a
put url token body decoder =
  Http.request
    { method = "PUT"
    , headers = [ Http.header "X-CSRF-Token" token ]
    , url = url
    , body = body
    , expect = Http.expectJson decoder
    , timeout = Nothing
    , withCredentials = False
    }

And the token at /rest/session/token is simply returned as plain text. My request for that is:

refreshTokenRequest =
    let
        url = "/rest/session/token?_format=json"
        request =
            Http.getString url
    in
        request
1 Like

I think you can make use of tasks here. Tasks have the ability to recover from an error.

I’d write a requestWithCsrf function that takes the “request config”. It performs the request, and if the request fails because of an expired CSRF token, it performs a request for the CSRF token, then retries the original request. At the end, it returns the request’s result, and the CSRF token. It might be the original token, or it might be a new one.

I know you said “so the abstraction doesn’t leak into my Main.elm”. But sometimes it’s easier to think about how to do it through the normal update cycle first. Elm doesn’t encourage us to abstract in the direction of “how to do do a generic token refresh + resubmit”. Once we have it down for a single concrete case, going through Main, then we can see if and how we might want to abstract it further. At least, that’s how I would approach it.

Like most things in Elm it helps to have a data structure in mind.

type Response a
    = Response a
    | TokenExpired (String -> Http.Request a)

So either the response is ok, and we get some a; or we get a token-expired response, and a function that we can feed the refreshed token into in order to retry the original request.

Keep in mind that this is all assuming there was no other Http.Error. This structure would be wrapped in a Result, as usual for attempting http tasks: Result Http.Error (Response Something).

Then we can use Http.toTask and Task.andThen to do the refresh + resubmit in one Cmd.

type Msg
    = SomeConcreteResponse (Result Http.Error (Response SomethingConcrete))
    |  -- more ....

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        SomeConcreteResponse (Ok response) ->
            case response of
                Response r ->
                    -- no token expired, proceed on the golden path
                TokenExpired toNewRequest ->
                    ( model,
                      refreshTokenRequest
                          |> Http.toTask
                          |> Task.andThen toNewRequest
                          |> Task.attempt SomeConcreteResponse
                   )
        SomeConcreteResponse (Err error) ->
            -- handle http error

Once you implement a second one of these concrete response handlers, then it might occur to you there’s a way of writing a generic helper function for the refresh + resubmit bit, and save some typing.

(There’s another piece of this of course, which is how to write your put so you end up with a Http.Request (Response a), how you insert the String -> Http.Request a into it in the case of a 403 response, etc. But I thought I’d see what you thought of this approach first.)

1 Like

That’s basically what I have, although not as fancy!

PS: because of the 403, you end up in the error branch, i.e. someConcreateResponse (Err (Http.BadStatus response)).

1 Like

That sounds interesting, I need to study this. Completely new to Elm (and functional programming), so sometimes struggling with the exact syntax.

Really? That’s odd, I was dealing with a 409 response the other day and it definitely came in on the success branch (in Chrome). I hope it’s not browser-dependent.

Yes. Take a look at Task.andThen and Task.onError (since your 403 is coming through on the error branch). You could chain together the initial put with the “refresh and resubmit” chain I did above, if the initial put results in a 403, and then you could do it all within one Cmd (one update cycle). Not sure if the resulting code is any clearer, but with helper functions it wouldn’t be too bad.

1 Like

Can’t be the browser. Only 2xx and 3xx responses are defined as Ok in the HTTP RFC. And in Http.js we have:

	{
		response.body = xhr.responseText;
		return _elm_lang$core$Native_Scheduler.fail({
			ctor: 'BadStatus',
			_0: response
		});
	}

So your 409 can’t have come in via the Ok branch.

1 Like

You’re right - eyes were playing tricks on me a couple days ago. Thanks.

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