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:
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
- cancellation - Cancellation primer
- timeoutsimple - Simple timeouts
- timeoutcomposed - Shared timeout of multiple tasks
Threads
- signalling - Cross-thread signalling
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:
-
old-style functions annotated with plain
async
will behave as if they had been annotated withasync: (handleException: true)
.This is functionally equivalent to
async: (handleException: true, raises: [CatchableError])
and will, as before, remap anyException
that is notDefect
intoAsyncExceptionError
, while also allowing anyCatchableError
(includingAsyncExceptionError
) to get through without compilation errors. -
New-style functions with
async: (raises: [...])
annotations or their ownhandleException
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 - useAsyncEventQueue
insteadFuture.value
andFuture.error
panic when accessed in the wrong stateFuture.read
andFuture.readError
raiseFutureError
instead ofValueError
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 mixchronos
andasyncdispatch
in the same threadimport chronos
instead ofimport 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, useawaitne
. asyncSpawn
is used instead ofasyncCheck
- note that exceptions raised in tasks that areasyncSpawn
: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 libraryasyncdispatch
module (-d:asyncBackend=asyncdispatch
)none
--d:asyncBackend=none
- disableasync
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)