Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Timeouts & Cancellation

Goal: Learn how to prevent the program from freezing on slow responses.

Source code: chapter4/src/uptimemon.nim

Our current program works fine with the well-behaving URIs we've tested so far: all these locations either respond quickly or quickly return an error.

However, not all requests will go smoothly when you face the real web. Poor connections, slow servers, anti-bot checks, and access restrictions result in responses that may take long to complete or even never complete. One "misbehaving" request can negatively affect the entire program.

For example, try adding an IP address that never responds to the list:

const uris = @[
  "https://duckduckgo.com/?q=chronos", "https://mock.codes/403", "http://10.255.255.1",
]

Run the program and you'll see that it'll run for 10+ seconds, stuck on this last IP.

Let's add a timeout to our requests to cancel slow requests before they ruin our app: if a request takes longer than 5 seconds, we cancel it.

import std/sequtils
import chronos/apps/http/httpclient

const uris = @[
  "https://duckduckgo.com/?q=chronos", "https://mock.codes/403", "http://10.255.255.1",
]

proc check(session: HttpSessionRef, uri: string) {.async: (raises: [CancelledError]).} =
  try:
    let response = await session.fetch(parseUri(uri)).wait(5.seconds)

    if response.status == 200:
      echo "[OK] " & uri
    else:
      echo "[NOK] " & uri & ": " & $response.status
  except HttpError, FuturePendingError, AsyncTimeoutError:
    echo "[ERR] " & uri & ": " & getCurrentExceptionMsg()

proc check(uris: seq[string]) {.async: (raises: []).} =
  let
    session = HttpSessionRef.new()
    futures = uris.mapIt(session.check(it))

  try:
    await allFutures(futures)
  except CancelledError:
    await cancelAndWait(futures)
  finally:
    await session.closeWait()

when isMainModule:
  waitFor check(uris)

Here's the part that changed:

proc check(session: HttpSessionRef, uri: string) {.async: (raises: [CancelledError]).} =
  try:
    let response = await session.fetch(parseUri(uri)).wait(5.seconds)

    if response.status == 200:
      echo "[OK] " & uri
    else:
      echo "[NOK] " & uri & ": " & $response.status
  except HttpError, FuturePendingError, AsyncTimeoutError:
    echo "[ERR] " & uri & ": " & getCurrentExceptionMsg()
  1. We use the .wait(timeout) modifier on our fetch future.
  2. If the request takes longer than the provided duration, .wait() automatically cancels the underlying future and raises an AsyncTimeoutError.
  3. We catch this error alongside other expected exceptions in our except block.

Info

In Nim, there are several ways to capture the message from an exception:

  • using getCurrentExceptionMsg(), as we do in this tutorial
  • using except <Exception> as e and then calling e.msg

Both variants have their advantages and limitations. For example, the as syntax can be used only with one exception type at a time while a lonely except used with getCurrentExceptionMsg() allows to capture multiple exception types in one statement.

On the other hand, because e.msg is guaranteed to capture a particular exception type, it's more deterministic and gives better control over exception handling logic.

The rule of thumb is that when your exception handling is simple (like we have in this tutorial—we simply echo the message regardless of the exception type), getCurrentExceptionMsg() is a simpler, more readable option, but if elaborate exception handling is an essential part of your business logic, you should prefer except <Exception> as e ... e.msg syntax.

Run the program again and you'll see it complete in roughly 5 seconds, i.e. our timeout.

Warning

One important thing to notice here is that adding a timeout won't save us from slow DNS resolutions.

Before we can make an HTTP request, we need to resolve the target hostname, i.e. get the IP address that corresponds to the given hostname. This is called DNS resolution and it is a blocking operation in Chronos.

For valid URIs, DNS resolution happens quickly enough to not interfere with the main logic. However, for invalid URIs (e.g. https://123.456.789.90) the resolution can stall for several seconds.

The main takeaway here is don't check invalid URIs.