Goal: Learn how to send POST HTTP requests and set request headers.
Source code: chapter6/src/uptimemon.nim
How cool would it be to get notified about a service being down to your phone? This way, you can launch the program and just go on with your business and not constantly monitor the terminal window.
ntfy is a service that allows to send push notifications with POST requests. Let's use it to send notifications when our program detects a [NOK] or [ERR].
Set Up ntfy
- Go to ntfy.sh/app.
- Click on Subscribe to topic in the sidebar, click GENERATE NAME in the popup, copy the generated name, and SUBSCRIBE. We'll use this unique topic name to send the notifications to.
- Click on GRANT NOW to allow push notifications from your browser.
- Keep the browser open.
Add Alerts
Here's the version of the program with alerting capabilities:
import std/sequtils import chronos/apps/http/httpclient #ANCHOR: ntfy_topic const ntfyTopic = "<YOUR_NTFY_TOPIC_NAME>" #ANCHOR_END: ntfy_topic uris = @[ "https://duckduckgo.com/?q=chronos", "https://mock.codes/403", "http://10.255.255.1", "https://html.spec.whatwg.org/", "https://mock.codes/200", ] #ANCHOR: sendAlert proc sendAlert( session: HttpSessionRef, message: string, priority = 3 ) {.async: (raises: [CancelledError]).} = let #ANCHOR_END: sendAlert #ANCHOR: headers headers = {"Title": "Chronos Uptime Monitor", "Priority": $priority} #ANCHOR_END: headers #ANCHOR: body body = message.stringToBytes() #ANCHOR_END: body #ANCHOR: request request = HttpClientRequestRef.new( session, "https://ntfy.sh/" & ntfyTopic, meth = MethodPost, headers = headers, body = body, ).valueOr: echo "[WRN] Failed to send alert: " & error return #ANCHOR_END: request #ANCHOR: response try: let response = await request.send().wait(5.seconds) await response.closeWait() except HttpError, FuturePendingError, AsyncTimeoutError: echo "[WRN] Failed to send alert: " & getCurrentExceptionMsg() finally: await request.closeWait() #ANCHOR_END: response proc findMarker( bodyReader: HttpBodyReader ): Future[bool] {.async: (raises: [AsyncStreamError, CancelledError]).} = const marker = "<html" readLimit = 10 * 1024 var totalRead = 0 sample = newString(len(marker) - 1) found = false proc findMarkerInSample(data: openArray[byte]): (int, bool) = if len(data) == 0: (0, false) else: sample = sample[^(len(marker) - 1) .. high(sample)] sample &= bytesToString(data) found = marker in sample totalRead += len(data) (len(data), found and totalRead <= readLimit) await bodyReader.readMessage(findMarkerInSample) found #ANCHOR: check proc check(session: HttpSessionRef, uri: string) {.async: (raises: [CancelledError]).} = let request = HttpClientRequestRef.new(session, uri).valueOr: echo "[ERR] " & uri & ": " & error return response = try: await request.send().wait(5.seconds) except HttpError, AsyncTimeoutError: echo "[ERR] " & uri & ": " & getCurrentExceptionMsg() finally: await request.closeWait() try: if response.status == 200: let bodyReader = response.getBodyReader() markerFound = try: await bodyReader.findMarker() finally: await bodyReader.closeWait() if markerFound: echo "[OK] " & uri else: let message = "[NOK] " & uri & ": Not valid HTML" echo message await session.sendAlert(message) else: let message = "[NOK] " & uri & ": " & $response.status echo message await session.sendAlert(message) except HttpError, AsyncStreamError: let message = "[ERR] " & uri & ": " & getCurrentExceptionMsg() echo message await session.sendAlert(message, 4) finally: await response.closeWait() #ANCHOR_END: check 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)
As usual, let's examine the changes part by part.
const ntfyTopic = "<YOUR_NTFY_TOPIC_NAME>"
Define a new constant for the ntfy topic name you copied Set Up ntfy. Replace YOUR_NTFY_TOPIC_NAME with the actual value you copied from ntfy.
proc sendAlert( session: HttpSessionRef, message: string, priority = 3 ) {.async: (raises: [CancelledError]).} = let
Define a new async function that will do the request sending to ntfy. We'll send those requests in the same session so we pass it to the function as session.
message is the text we want to send in the notification.
priority is a number that defines the style of the notification in ntfy. ntfy recognizes five priority levels from 1 to 5: the higher the number, the "scarier" the message.
headers = {"Title": "Chronos Uptime Monitor", "Priority": $priority}
ntfy uses headers to customize notifications, e.g. `Title` and `Priority`.
Here we set the headers as an arrays of tuples using Nim's shortcut syntax.
body = message.stringToBytes()
Requests body must be a sequence of bytes so we convert our text message using httpcommon: stringToBytes(openArray[char]).
request = HttpClientRequestRef.new(
session,
"https://ntfy.sh/" & ntfyTopic,
meth = MethodPost,
headers = headers,
body = body,
).valueOr:
echo "[WRN] Failed to send alert: " & error
return
Create the request with the necessary properties. meth is the request's HTTP method.
try: let response = await request.send().wait(5.seconds) await response.closeWait() except HttpError, FuturePendingError, AsyncTimeoutError: echo "[WRN] Failed to send alert: " & getCurrentExceptionMsg() finally: await request.closeWait()
If the request was successfully created (request.isOk), we try to send it with send() and discard it (with closeWait).
If the request couldn't be sent (e.g. ntfy is unavailable), we print a warning.
proc check(session: HttpSessionRef, uri: string) {.async: (raises: [CancelledError]).} = let request = HttpClientRequestRef.new(session, uri).valueOr: echo "[ERR] " & uri & ": " & error return response = try: await request.send().wait(5.seconds) except HttpError, AsyncTimeoutError: echo "[ERR] " & uri & ": " & getCurrentExceptionMsg() finally: await request.closeWait() try: if response.status == 200: let bodyReader = response.getBodyReader() markerFound = try: await bodyReader.findMarker() finally: await bodyReader.closeWait() if markerFound: echo "[OK] " & uri else: let message = "[NOK] " & uri & ": Not valid HTML" echo message await session.sendAlert(message) else: let message = "[NOK] " & uri & ": " & $response.status echo message await session.sendAlert(message) except HttpError, AsyncStreamError: let message = "[ERR] " & uri & ": " & getCurrentExceptionMsg() echo message await session.sendAlert(message, 4) finally: await response.closeWait()
Finally, we add calls to sendAlert in the check branches for [NOK] and [ERR]. Run the code and observe alerts appearing in your browser accompanied by push notifications:

To receive the notifications on your phone, install ntfy mobile app and subscribe to the same topic.
In the final chapter, we'll see how to scale our application and add some finishing touches!