Http.Error Timeout variant without configurable timeout in Elm 0.19
0 reputation · 04 Oct 2020, 21:04 UTC
0 reputation · 04 Oct 2020, 21:04 UTC
The core Http.Error type includes a Timeout variant, but Http.request accepts no timeout parameter, so developers must implement application‑level deadlines manually.
Typical patterns race Http.request against Process.sleep using Task and then filter messages with a request identifier to ignore late‑arriving successes.
Because the Elm runtime cannot abort the underlying browser request, a race condition may deliver both a success message and a timeout message for the same request, requiring defensive ignore logic and increasing update complexity.
What mechanisms can ensure that a timeout yields a single, final state transition for a given request without wasting bandwidth or leaking timers?
34775 reputation · 05 Oct 2020, 01:17 UTC
Use Task.race to pit Http.toTask request against a Process.sleep ms |> Task.andThen (always (Task.fail Timeout)) task, tagging both with a unique requestId. In update, keep a Dict RequestId RequestState in your model and discard any response whose requestId is absent or marked Cancelled. This guarantees exactly one state transition per logical request.
Elm 0.19's Http.request exposes no timeout parameter; the Http.Error.Timeout variant only surfaces when the browser's native timeout fires (typically 30–300 seconds, unconfigurable from Elm). The runtime cannot abort the underlying fetch/XHR, so both the success and the application-level timeout task can complete. The requestId guard in the model makes the second arrival a no-op, yielding a single, final transition.
type alias RequestId = Int
type RequestState = Pending | Completed | Cancelled
withTimeout : Int -> Request -> (Result Http.Error a -> msg) -> (RequestId, Cmd msg)
withTimeout ms request toMsg =
let
requestId = nextRequestId model
timeoutTask =
Process.sleep ms
|> Task.andThen (always (Task.fail (Http.Timeout "application timeout")))
racedTask =
Task.race (Http.toTask request) timeoutTask
|> Task.map (toMsg requestId)
in
( requestId
, Task.perform identity racedTask
)
In update, on toMsg requestId result:
requestId not in model.pending → ignore.result is Ok → update state to Completed, remove requestId, process payload.result is Err Timeout → update state to Cancelled, remove requestId, handle timeout.Bandwidth cannot be saved—the browser continues the download regardless. The pattern only prevents duplicate state transitions. Timer leaks are avoided because Task.race cancels the losing task internally; removing the requestId from the pending dict ensures a late timeout message becomes a no-op without any lingering Process.sleep references.
requestId in update and confirm only one message per request reaches the update function.Task.race cancellation semantics are as documented (winner cancels loser).Task.perform instead of Task.attempt, ensure the timeout task shares the same requestId so cancellation logic stays symmetric.One diagnostic detail that would change the recommendation: are you using Http.expectString or a custom Http.Expect? Custom expectations may require wrapping the expectation itself in the race rather than the raw request.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.