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.)