Introduction

Chronos implements the async/await paradigm in a self-contained library using macro and closure iterator transformation features provided by Nim.

Features include:

  • Asynchronous socket and process I/O
  • HTTP client / server with SSL/TLS support out of the box (no OpenSSL needed)
  • Synchronization primitivies like queues, events and locks
  • Cancellation
  • Efficient dispatch pipeline with excellent multi-platform support
  • Exception effect support

Installation

Install chronos using nimble:

nimble install chronos

or add a dependency to your .nimble file:

requires "chronos"

and start using it:

import chronos/apps/http/httpclient

proc retrievePage*(uri: string): Future[string] {.async.} =
  # Create a new HTTP session
  let httpSession = HttpSessionRef.new()
  try:
    # Fetch page contents
    let resp = await httpSession.fetch(parseUri(uri))
    # Convert response to a string, assuming its encoding matches the terminal!
    bytesToString(resp.data)
  finally: # Close the session
    await noCancel(httpSession.closeWait())

echo waitFor retrievePage(
  "https://raw.githubusercontent.com/status-im/nim-chronos/master/README.md")

There are more examples throughout the manual!

Platform support

Several platforms are supported, with different backend options:

  • Windows: IOCP
  • Linux: epoll / poll
  • OSX / BSD: kqueue / poll
  • Android / Emscripten / posix: poll

API documentation

This guide covers basic usage of chronos - for details, see the API reference.

Examples

Examples are available in the docs/examples/ folder.

Basic concepts

Threads

TCP

  • tcpserver - Simple TCP/IP v4/v6 echo server

HTTP

  • httpget - Downloading a web page using the http client
  • twogets - Download two pages concurrently
  • middleware - Deploy multiple HTTP server middlewares

Concepts

Async/await is a programming model that relies on cooperative multitasking to coordinate the concurrent execution of procedures, using event notifications from the operating system or other treads to resume execution.

Code execution happens in a loop that alternates between making progress on tasks and handling events.

The dispatcher

The event handler loop is called a "dispatcher" and a single instance per thread is created, as soon as one is needed.

Scheduling is done by calling async procedures that return Future objects - each time a procedure is unable to make further progress, for example because it's waiting for some data to arrive, it hands control back to the dispatcher which ensures that the procedure is resumed when ready.

A single thread, and thus a single dispatcher, is typically able to handle thousands of concurrent in-progress requests.

The Future type

Future objects encapsulate the outcome of executing an async procedure. The Future may be pending meaning that the outcome is not yet known or finished meaning that the return value is available, the operation failed with an exception or was cancelled.

Inside an async procedure, you can await the outcome of another async procedure - if the Future representing that operation is still pending, a callback representing where to resume execution will be added to it and the dispatcher will be given back control to deal with other tasks.

When a Future is finished, all its callbacks are scheduled to be run by the dispatcher, thus continuing any operations that were waiting for an outcome.

The poll call

To trigger the processing step of the dispatcher, we need to call poll() - either directly or through a wrapper like runForever() or waitFor().

Each call to poll handles any file descriptors, timers and callbacks that are ready to be processed.

Using waitFor, the result of a single asynchronous operation can be obtained:

proc myApp() {.async.} =
  echo "Waiting for a second..."
  await sleepAsync(1.seconds)
  echo "done!"

waitFor myApp()

It is also possible to keep running the event loop forever using runForever:

proc myApp() {.async.} =
  while true:
    await sleepAsync(1.seconds)
    echo "A bit more than a second passed!"

let future = myApp()
runForever()

Such an application never terminates, thus it is rare that applications are structured this way.

Both `waitFor` and `runForever` call `poll` which offers fine-grained
control over the event loop steps.

Nested calls to `poll` - directly or indirectly via `waitFor` and `runForever`
are not allowed.

Cancellation

Any pending Future can be cancelled. This can be used for timeouts, to start multiple parallel operations and cancel the rest as soon as one finishes, to initiate the orderely shutdown of an application etc.

## Simple cancellation example

import chronos

proc someTask() {.async.} = await sleepAsync(10.minutes)

proc cancellationExample() {.async.} =
  # Start a task but don't wait for it to finish
  let future = someTask()
  future.cancelSoon()
  # `cancelSoon` schedules but does not wait for the future to get cancelled -
  # it might still be pending here

  let future2 = someTask() # Start another task concurrently
  await future2.cancelAndWait()
  # Using `cancelAndWait`, we can be sure that `future2` is either
  # complete, failed or cancelled at this point. `future` could still be
  # pending!
  assert future2.finished()

waitFor(cancellationExample())

Even if cancellation is initiated, it is not guaranteed that the operation gets cancelled - the future might still be completed or fail depending on the order of events in the dispatcher and the specifics of the operation.

If the future indeed gets cancelled, await will raise a CancelledError as is likely to happen in the following example:

proc c1 {.async.} =
  echo "Before sleep"
  try:
    await sleepAsync(10.minutes)
    echo "After sleep" # not reach due to cancellation
  except CancelledError as exc:
    echo "We got cancelled!"
    # `CancelledError` is typically re-raised to notify the caller that the
    # operation is being cancelled
    raise exc

proc c2 {.async.} =
  await c1()
  echo "Never reached, since the CancelledError got re-raised"

let work = c2()
waitFor(work.cancelAndWait())

The CancelledError will now travel up the stack like any other exception. It can be caught for instance to free some resources and is then typically re-raised for the whole chain operations to get cancelled.

Alternatively, the cancellation request can be translated to a regular outcome of the operation - for example, a read operation might return an empty result.

Cancelling an already-finished Future has no effect, as the following example of downloading two web pages concurrently shows:

## Make two http requests concurrently and output the one that wins

import chronos
import ./httpget

proc twoGets() {.async.} =
  let
    futs = @[
      # Both pages will start downloading concurrently...
      httpget.retrievePage("https://duckduckgo.com/?q=chronos"),
      httpget.retrievePage("https://www.google.fr/search?q=chronos")
    ]

  # Wait for at least one request to finish..
  let winner = await one(futs)
  # ..and cancel the others since we won't need them
  for fut in futs:
    # Trying to cancel an already-finished future is harmless
    fut.cancelSoon()

  # An exception could be raised here if the winning request failed!
  echo "Result: ", winner.read()

waitFor(twoGets())

Ownership

When calling a procedure that returns a Future, ownership of that Future is shared between the callee that created it and the caller that waits for it to be finished.

The Future can be thought of as a single-item channel between a producer and a consumer. The producer creates the Future and is responsible for completing or failing it while the caller waits for completion and may cancel it.

Although it is technically possible, callers must not complete or fail futures and callees or other intermediate observers must not cancel them as this may lead to panics and shutdown (ie if the future is completed twice or a cancalletion is not handled by the original caller).

noCancel

Certain operations must not be cancelled for semantic reasons. Common scenarios include closeWait that releases a resources irrevocably and composed operations whose individual steps should be performed together or not at all.

In such cases, the noCancel modifier to await can be used to temporarily disable cancellation propagation, allowing the operation to complete even if the caller initiates a cancellation request:

proc deepSleep(dur: Duration) {.async.} =
  # `noCancel` prevents any cancellation request by the caller of `deepSleep`
  # from reaching `sleepAsync` - even if `deepSleep` is cancelled, its future
  # will not complete until the sleep finishes.
  await noCancel sleepAsync(dur)

let future = deepSleep(10.minutes)

# This will take ~10 minutes even if we try to cancel the call to `deepSleep`!
await cancelAndWait(future)

join

The join modifier to await allows cancelling an async procedure without propagating the cancellation to the awaited operation. This is useful when await:ing a Future for monitoring purposes, ie when a procedure is not the owner of the future that's being await:ed.

One situation where this happens is when implementing the "observer" pattern, where a helper monitors an operation it did not initiate:

var tick: Future[void]
proc ticker() {.async.} =
  while true:
    tick = sleepAsync(1.second)
    await tick
    echo "tick!"

proc tocker() {.async.} =
  # This operation does not own or implement the operation behind `tick`,
  # so it should not cancel it when `tocker` is cancelled
  await join tick
  echo "tock!"

let
  fut = ticker() # `ticker` is now looping and most likely waiting for `tick`
  fut2 = tocker() # both `ticker` and `tocker` are waiting for `tick`

# We don't want `tocker` to cancel a future that was created in `ticker`
waitFor fut2.cancelAndWait()

waitFor fut # keeps printing `tick!` every second.

Compile-time configuration

chronos contains several compile-time configuration options enabling stricter compile-time checks and debugging helpers whose runtime cost may be significant.

Strictness options generally will become default in future chronos releases and allow adapting existing code without changing the new version - see the config.nim module for more information.

Async procedures

Async procedures are those that interact with chronos to cooperatively suspend and resume their execution depending on the completion of other async procedures, timers, tasks on other threads or asynchronous I/O scheduled with the operating system.

Async procedures are marked with the {.async.} pragma and return a Future indicating the state of the operation.

The async pragma

The {.async.} pragma will transform a procedure (or a method) returning a Future into a closure iterator. If there is no return type specified, Future[void] is returned.

proc p() {.async.} =
  await sleepAsync(100.milliseconds)

echo p().type # prints "Future[system.void]"

await keyword

The await keyword operates on Future instances typically returned from an async procedure.

Whenever await is encountered inside an async procedure, control is given back to the dispatcher for as many steps as it's necessary for the awaited future to complete, fail or be cancelled. await calls the equivalent of Future.read() on the completed future to return the encapsulated value when the operation finishes.

proc p1() {.async.} =
  await sleepAsync(1.seconds)

proc p2() {.async.} =
  await sleepAsync(1.seconds)

proc p3() {.async.} =
  let
    fut1 = p1()
    fut2 = p2()
  # Just by executing the async procs, both resulting futures entered the
  # dispatcher queue and their "clocks" started ticking.
  await fut1
  await fut2
  # Only one second passed while awaiting them both, not two.

waitFor p3()
Because `async` procedures are executed concurrently, they are subject to many
of the same risks that typically accompany multithreaded programming.

In particular, if two `async` procedures have access to the same mutable state,
the value before and after `await` might not be the same as the order of execution is not guaranteed!

Raw async procedures

Raw async procedures are those that interact with chronos via the Future type but whose body does not go through the async transformation.

Such functions are created by adding raw: true to the async parameters:

proc rawAsync(): Future[void] {.async: (raw: true).} =
  let fut = newFuture[void]("rawAsync")
  fut.complete()
  fut

Raw functions must not raise exceptions directly - they are implicitly declared as raises: [] - instead they should store exceptions in the returned Future:

proc rawFailure(): Future[void] {.async: (raw: true).} =
  let fut = newFuture[void]("rawAsync")
  fut.fail((ref ValueError)(msg: "Oh no!"))
  fut

Raw procedures can also use checked exceptions:

proc rawAsyncRaises(): Future[void] {.async: (raw: true, raises: [IOError]).} =
  let fut = newFuture[void]()
  assert not (compiles do: fut.fail((ref ValueError)(msg: "uh-uh")))
  fut.fail((ref IOError)(msg: "IO"))
  fut

Callbacks and closures

Callback/closure types are declared using the async annotation as usual:

type MyCallback = proc(): Future[void] {.async.}

proc runCallback(cb: MyCallback) {.async: (raises: []).} =
  try:
    await cb()
  except CatchableError:
    discard # handle errors as usual

When calling a callback, it is important to remember that it may raise exceptions that need to be handled.

Checked exceptions can be used to limit the exceptions that a callback can raise:

type MyEasyCallback = proc(): Future[void] {.async: (raises: []).}

proc runCallback(cb: MyEasyCallback) {.async: (raises: [])} =
  await cb()

Errors and exceptions

Exceptions

Exceptions inheriting from CatchableError interrupt execution of an async procedure. The exception is placed in the Future.error field while changing the status of the Future to Failed and callbacks are scheduled.

When a future is read or awaited the exception is re-raised, traversing the async execution chain until handled.

proc p1() {.async.} =
  await sleepAsync(1.seconds)
  raise newException(ValueError, "ValueError inherits from CatchableError")

proc p2() {.async.} =
  await sleepAsync(1.seconds)

proc p3() {.async.} =
  let
    fut1 = p1()
    fut2 = p2()
  await fut1
  echo "unreachable code here"
  await fut2

# `waitFor()` would call `Future.read()` unconditionally, which would raise the
# exception in `Future.error`.
let fut3 = p3()
while not(fut3.finished()):
  poll()

echo "fut3.state = ", fut3.state # "Failed"
if fut3.failed():
  echo "p3() failed: ", fut3.error.name, ": ", fut3.error.msg
  # prints "p3() failed: ValueError: ValueError inherits from CatchableError"

You can put the await in a try block, to deal with that exception sooner:

proc p3() {.async.} =
  let
    fut1 = p1()
    fut2 = p2()
  try:
    await fut1
  except CachableError:
    echo "p1() failed: ", fut1.error.name, ": ", fut1.error.msg
  echo "reachable code here"
  await fut2

Because chronos ensures that all exceptions are re-routed to the Future, poll will not itself raise exceptions.

poll may still panic / raise Defect if such are raised in user code due to undefined behavior.

Checked exceptions

By specifying a raises list to an async procedure, you can check which exceptions can be raised by it:

proc p1(): Future[void] {.async: (raises: [IOError]).} =
  assert not (compiles do: raise newException(ValueError, "uh-uh"))
  raise newException(IOError, "works") # Or any child of IOError

proc p2(): Future[void] {.async, (raises: [IOError]).} =
  await p1() # Works, because await knows that p1
             # can only raise IOError

Under the hood, the return type of p1 will be rewritten to an internal type which will convey raises informations to await.

Most `async` include `CancelledError` in the list of `raises`, indicating that
the operation they implement might get cancelled resulting in neither value nor
error!

When using checked exceptions, the Future type is modified to include raises information - it can be constructed with the Raising helper:

# Create a variable of the type that will be returned by a an async function
# raising `[CancelledError]`:
var fut: Future[int].Raising([CancelledError])
`Raising` creates a specialization of `InternalRaisesFuture` type - as the name
suggests, this is an internal type whose implementation details are likely to
change in future `chronos` versions.

The Exception type

Exceptions deriving from Exception are not caught by default as these may include Defect and other forms undefined or uncatchable behavior.

Because exception effect tracking is turned on for async functions, this may sometimes lead to compile errors around forward declarations, methods and closures as Nim conservatively asssumes that any Exception might be raised from those.

Make sure to explicitly annotate these with {.raises.}:

# Forward declarations need to explicitly include a raises list:
proc myfunction() {.raises: [ValueError].}

# ... as do `proc` types
type MyClosure = proc() {.raises: [ValueError].}

proc myfunction() =
  raise (ref ValueError)(msg: "Implementation here")

let closure: MyClosure = myfunction

Compatibility modes

Individual functions. For compatibility, async functions can be instructed to handle Exception as well, specifying handleException: true. Any Exception that is not a Defect and not a CatchableError will then be caught and remapped to AsyncExceptionError:

proc raiseException() {.async: (handleException: true, raises: [AsyncExceptionError]).} =
  raise (ref Exception)(msg: "Raising Exception is UB")

proc callRaiseException() {.async: (raises: []).} =
  try:
    await raiseException()
  except AsyncExceptionError as exc:
    # The original Exception is available from the `parent` field
    echo exc.parent.msg

Global flag. This mode can be enabled globally with -d:chronosHandleException as a help when porting code to chronos. The behavior in this case will be that:

  1. old-style functions annotated with plain async will behave as if they had been annotated with async: (handleException: true).

    This is functionally equivalent to async: (handleException: true, raises: [CatchableError]) and will, as before, remap any Exception that is not Defect into AsyncExceptionError, while also allowing any CatchableError (including AsyncExceptionError) to get through without compilation errors.

  2. New-style functions with async: (raises: [...]) annotations or their own handleException annotations will not be affected.

The rationale here is to allow one to incrementally introduce exception annotations and get compiler feedback while not requiring that every bit of legacy code is updated at once.

This should be used sparingly and with care, however, as global configuration settings may interfere with libraries that use chronos leading to unexpected behavior.

Threads

While the cooperative async model offers an efficient model for dealing with many tasks that often are blocked on I/O, it is not suitable for long-running computations that would prevent concurrent tasks from progressing.

Multithreading offers a way to offload heavy computations to be executed in parallel with the async work, or, in cases where a single event loop gets overloaded, to manage multiple event loops in parallel.

For interaction between threads, the ThreadSignalPtr type (found in the (chronos/threadsync)(https://github.com/status-im/nim-chronos/blob/master/chronos/threadsync.nim) module) is used - both to wait for notifications coming from other threads and to notify other threads of progress from within an async procedure.

import chronos, chronos/threadsync
import os

type
  Context = object
    # Context allocated by `createShared` should contain no garbage-collected
    # types!
    signal: ThreadSignalPtr
    value: int

proc myThread(ctx: ptr Context) {.thread.} =
  echo "Doing some work in a thread"
  sleep(3000)
  ctx.value = 42
  echo "Done, firing the signal"
  discard ctx.signal.fireSync().expect("correctly initialized signal should not fail")

proc main() {.async.} =
  let
    signal = ThreadSignalPtr.new().expect("free file descriptor for signal")
    context = createShared(Context)
  context.signal = signal

  var thread: Thread[ptr Context]

  echo "Starting thread"
  createThread(thread, myThread, context)

  await signal.wait()

  echo "Work done: ", context.value

  joinThread(thread)

  signal.close().expect("closing once works")
  deallocShared(context)

waitFor main()

Tips, tricks and best practices

Timeouts

To prevent a single task from taking too long, withTimeout can be used:

## Simple timeouts
import chronos

proc longTask {.async.} =
  try:
    await sleepAsync(10.minutes)
  except CancelledError as exc:
    echo "Long task was cancelled!"
    raise exc # Propagate cancellation to the next operation

proc simpleTimeout() {.async.} =
  let
    task = longTask() # Start a task but don't `await` it

  if not await task.withTimeout(1.seconds):
    echo "Timeout reached - withTimeout should have cancelled the task"
  else:
    echo "Task completed"

waitFor simpleTimeout()

When several tasks should share a single timeout, a common timer can be created with sleepAsync:

## Single timeout for several operations
import chronos

proc shortTask {.async.} =
  try:
    await sleepAsync(1.seconds)
  except CancelledError as exc:
    echo "Short task was cancelled!"
    raise exc # Propagate cancellation to the next operation

proc composedTimeout()  {.async.} =
  let
    # Common timout for several sub-tasks
    timeout = sleepAsync(10.seconds)

  while not timeout.finished():
    let task = shortTask() # Start a task but don't `await` it
    if (await race(task, timeout)) == task:
      echo "Ran one more task"
    else:
      # This cancellation may or may not happen as task might have finished
      # right at the timeout!
      task.cancelSoon()

waitFor composedTimeout()

discard

When calling an asynchronous procedure without await, the operation is started but its result is not processed until corresponding Future is read.

It is therefore important to never discard futures directly - instead, one can discard the result of awaiting the future or use asyncSpawn to monitor the outcome of the future as if it were running in a separate thread.

Similar to threads, tasks managed by asyncSpawn may causes the application to crash if any exceptions leak out of it - use checked exceptions to avoid this problem.

## The peculiarities of `discard` in `async` procedures
import chronos

proc failingOperation() {.async.} =
  echo "Raising!"
  raise (ref ValueError)(msg: "My error")

proc myApp() {.async.} =
  # This style of discard causes the `ValueError` to be discarded, hiding the
  # failure of the operation - avoid!
  discard failingOperation()

  proc runAsTask(fut: Future[void]): Future[void] {.async: (raises: []).} =
    # runAsTask uses `raises: []` to ensure at compile-time that no exceptions
    # escape it!
    try:
      await fut
    except CatchableError as exc:
      echo "The task failed! ", exc.msg

  # asyncSpawn ensures that errors don't leak unnoticed from tasks without
  # blocking:
  asyncSpawn runAsTask(failingOperation())

  # If we didn't catch the exception with `runAsTask`, the program will crash:
  asyncSpawn failingOperation()

waitFor myApp()

Porting code to chronos v4

Thanks to its macro support, Nim allows async/await to be implemented in libraries with only minimal support from the language - as such, multiple async libraries exist, including chronos and asyncdispatch, and more may come to be developed in the futures.

Chronos v3

Chronos v4 introduces new features for IPv6, exception effects, a stand-alone Future type as well as several other changes - when upgrading from chronos v3, here are several things to consider:

  • Exception handling is now strict by default - see the error handling chapter for how to deal with raises effects
  • AsyncEventBus was removed - use AsyncEventQueue instead
  • Future.value and Future.error panic when accessed in the wrong state
  • Future.read and Future.readError raise FutureError instead of ValueError when accessed in the wrong state

asyncdispatch

Code written for asyncdispatch and chronos looks similar but there are several differences to be aware of:

  • chronos has its own dispatch loop - you can typically not mix chronos and asyncdispatch in the same thread
  • import chronos instead of import asyncdispatch
  • cleanup is important - make sure to use closeWait to release any resources you're using or file descriptor and other leaks will ensue
  • cancellation support means that CancelledError may be raised from most {.async.} functions
  • Calling yield directly in tasks is not supported - instead, use awaitne.
  • asyncSpawn is used instead of asyncCheck - note that exceptions raised in tasks that are asyncSpawn:ed cause panic

Supporting multiple backends

Libraries built on top of async/await may wish to support multiple async backends - the best way to do so is to create separate modules for each backend that may be imported side-by-side - see nim-metrics for an example.

An alternative way is to select backend using a global compile flag - this method makes it diffucult to compose applications that use both backends as may happen with transitive dependencies, but may be appropriate in some cases - libraries choosing this path should call the flag asyncBackend, allowing applications to choose the backend with -d:asyncBackend=<backend_name>.

Known async backends include:

  • chronos - this library (-d:asyncBackend=chronos)
  • asyncdispatch the standard library asyncdispatch module (-d:asyncBackend=asyncdispatch)
  • none - -d:asyncBackend=none - disable async support completely

none can be used when a library supports both a synchronous and asynchronous API, to disable the latter.

HTTP server middleware

Chronos provides a powerful mechanism for customizing HTTP request handlers via middlewares.

A middleware is a coroutine that can modify, block or filter HTTP request.

Single HTTP server could support unlimited number of middlewares, but you need to consider that each request in worst case could go through all the middlewares, and therefore a huge number of middlewares can have a significant impact on HTTP server performance.

Order of middlewares is also important: right after HTTP server has received request, it will be sent to the first middleware in list, and each middleware will be responsible for passing control to other middlewares. Therefore, when building a list, it would be a good idea to place the request handlers at the end of the list, while keeping the middleware that could block or modify the request at the beginning of the list.

Middleware could also modify HTTP server request, and these changes will be visible to all handlers (either middlewares or the original request handler). This can be done using the following helpers:

  proc updateRequest*(request: HttpRequestRef, scheme: string, meth: HttpMethod,
                      version: HttpVersion, requestUri: string,
                      headers: HttpTable): HttpResultMessage[void]

  proc updateRequest*(request: HttpRequestRef, meth: HttpMethod,
                      requestUri: string,
                      headers: HttpTable): HttpResultMessage[void]

  proc updateRequest*(request: HttpRequestRef, requestUri: string,
                      headers: HttpTable): HttpResultMessage[void]

  proc updateRequest*(request: HttpRequestRef,
                      requestUri: string): HttpResultMessage[void]

  proc updateRequest*(request: HttpRequestRef,
                      headers: HttpTable): HttpResultMessage[void]

As you can see all the HTTP request parameters could be modified: request method, version, request path and request headers.

Middleware could also use helpers to obtain more information about remote and local addresses of request's connection (this could be helpful when you need to do some IP address filtering).

  proc remote*(request: HttpRequestRef): Opt[TransportAddress]
    ## Returns remote address of HTTP request's connection.
  proc local*(request: HttpRequestRef): Opt[TransportAddress] =
    ## Returns local address of HTTP request's connection.

Every middleware is the coroutine which looks like this:

  proc middlewareHandler(
      middleware: HttpServerMiddlewareRef,
      reqfence: RequestFence,
      nextHandler: HttpProcessCallback2
  ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =

Where middleware argument is the object which could hold some specific values, reqfence is HTTP request which is enclosed with HTTP server error information and nextHandler is reference to next request handler, it could be either middleware handler or the original request processing callback handler.

  await nextHandler(reqfence)

You should perform await for the response from the nextHandler(reqfence). Usually you should call next handler when you dont want to handle request or you dont know how to handle it, for example:

  proc middlewareHandler(
      middleware: HttpServerMiddlewareRef,
      reqfence: RequestFence,
      nextHandler: HttpProcessCallback2
  ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
  if reqfence.isErr():
    # We dont know or do not want to handle failed requests, so we call next handler.
    return await nextHandler(reqfence)
  let request = reqfence.get()
  if request.uri.path == "/path/we/able/to/respond":
    try:
      # Sending some response.
      await request.respond(Http200, "TEST")
    except HttpWriteError as exc:
      # We could also return default response for exception or other types of error.
      defaultResponse(exc)
  elif request.uri.path == "/path/for/rewrite":
    # We going to modify request object for this request, next handler will receive it with different request path.
    let res = request.updateRequest("/path/to/new/location")
    if res.isErr():
      return defaultResponse(res.error)
    await nextHandler(reqfence)
  elif request.uri.path == "/restricted/path":
    if request.remote().isNone():
      # We can't obtain remote address, so we force HTTP server to respond with `401 Unauthorized` status code.
      return codeResponse(Http401)
    if $(request.remote().get()).startsWith("127.0.0.1"):
      # Remote peer's address starts with "127.0.0.1", sending proper response.
      await request.respond(Http200, "AUTHORIZED")
    else:
      # Force HTTP server to respond with `403 Forbidden` status code.
      codeResponse(Http403)
  elif request.uri.path == "/blackhole":
    # Force HTTP server to drop connection with remote peer.
    dropResponse()
  else:
    # All other requests should be handled by somebody else.
    await nextHandler(reqfence)

Updating this book