4 Commits

Author SHA1 Message Date
06ac861c20 Optionally include additional data to return in ApiErrors. 2025-09-02 00:33:14 -05:00
061b0a44fc Make %(ApiResponse) a proc as conversion of wrapped types can have side-effects. 2025-08-02 12:40:14 -05:00
be60254227 Add utilities for handling the traceparent header (see Trace Context W3C standard). 2025-08-02 11:57:38 -05:00
c6d02d7db7 Update default allowed headers in makeCorsHeaders
- Allow the caller to optionally provide a list
- Add `traceparent`, `tracestate`, `X-Request-ID` and `X-Correlation-Id`
  by default.
2025-04-18 09:40:16 -05:00
3 changed files with 80 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
# Package # Package
version = "0.4.7" version = "0.4.11"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Jonathan's opinionated extensions and auth layer for Jester." description = "Jonathan's opinionated extensions and auth layer for Jester."
license = "MIT" license = "MIT"

View File

@@ -1,36 +1,56 @@
from strutils import isEmptyOrWhitespace import std/[httpcore, json, options, strutils]
from httpcore import HttpCode
type ApiError* = object of CatchableError type ApiError* = object of CatchableError
respMsg*: string respMsg*: string
respCode*: HttpCode respCode*: HttpCode
respData*: Option[JsonNode]
proc newApiError*(parent: ref Exception = nil, respCode: HttpCode, respMsg: string, msg = ""): ref ApiError = proc newApiError*(
parent: ref Exception = nil,
respCode: HttpCode,
respMsg: string,
respData = none[JsonNode](),
msg = ""): ref ApiError =
result = newException(ApiError, msg, parent) result = newException(ApiError, msg, parent)
result.respCode = respCode result.respCode = respCode
result.respMsg = respMsg result.respMsg = respMsg
result.respData = respData
if not parent.isNil: if not parent.isNil:
result.trace &= parent.trace result.trace &= parent.trace
proc raiseApiError*(respCode: HttpCode, respMsg: string, msg = "") = proc raiseApiError*(
respCode: HttpCode,
respMsg: string,
respData = none[JsonNode](),
msg = "") =
var apiError = newApiError( var apiError = newApiError(
parent = nil, parent = nil,
respCode = respCode, respCode = respCode,
respMsg = respMsg, respMsg = respMsg,
respData = respData,
msg = if msg.isEmptyOrWhitespace: respMsg msg = if msg.isEmptyOrWhitespace: respMsg
else: msg) else: msg)
raise apiError raise apiError
proc raiseApiError*(respCode: HttpCode, parent: ref Exception, respMsg: string = "", msg = "") = proc raiseApiError*(
respCode: HttpCode,
parent: ref Exception,
respMsg: string = "",
respData = none[JsonNode](),
msg = "") =
var apiError = newApiError( var apiError = newApiError(
parent = parent, parent = parent,
respCode = respCode, respCode = respCode,
respMsg = respMsg =
if respMsg.isEmptyOrWhitespace: parent.msg if respMsg.isEmptyOrWhitespace: parent.msg
else: respMsg, else: respMsg,
msg = msg) respData = respData,
msg =
if msg.isEmptyOrWhitespace: parent.msg
else: msg)
raise apiError raise apiError

View File

@@ -1,5 +1,5 @@
import std/[json, jsonutils, options, sequtils, strtabs, strutils] import std/[json, jsonutils, options, sequtils, strtabs, strutils]
import mummy, webby import mummy, webby, uuids
import std/httpcore except HttpHeaders import std/httpcore except HttpHeaders
@@ -31,7 +31,7 @@ func initApiResponse*[T](
totalItems: totalItems, nextLink: nextLink, prevLink: prevLink) totalItems: totalItems, nextLink: nextLink, prevLink: prevLink)
func `%`*(r: ApiResponse): JsonNode = proc `%`*(r: ApiResponse): JsonNode =
result = newJObject() result = newJObject()
if r.details.isSome: result["details"] = %r.details if r.details.isSome: result["details"] = %r.details
if r.data.isSome: result["data"] = %r.data if r.data.isSome: result["data"] = %r.data
@@ -47,6 +47,7 @@ func `$`*(r: ApiResponse): string = $(%r)
proc makeCorsHeaders*( proc makeCorsHeaders*(
allowedMethods: seq[string], allowedMethods: seq[string],
allowedOrigins: seq[string], allowedOrigins: seq[string],
allowedHeaders: Option[seq[string]],
reqOrigin = none[string]()): HttpHeaders = reqOrigin = none[string]()): HttpHeaders =
result = result =
@@ -54,8 +55,13 @@ proc makeCorsHeaders*(
@{ @{
"Access-Control-Allow-Origin": reqOrigin.get, "Access-Control-Allow-Origin": reqOrigin.get,
"Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": allowedMethods.join(", "), "Access-Control-Allow-Methods": allowedMethods.join(","),
"Access-Control-Allow-Headers": "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-CSRF-TOKEN" "Access-Control-Allow-Headers":
if allowedHeaders.isSome: allowedHeaders.get.join(",")
else:
"DNT,User-Agent,X-Requested-With,If-Modified-Since," &
"Cache-Control,Content-Type,Range,Authorization,X-CSRF-TOKEN," &
"traceparent,tracestate,X-Request-ID,X-Correlation-ID",
} }
else: else:
if reqOrigin.isSome: if reqOrigin.isSome:
@@ -67,8 +73,16 @@ proc makeCorsHeaders*(
proc makeCorsHeaders*( proc makeCorsHeaders*(
allowedMethods: seq[HttpMethod], allowedMethods: seq[HttpMethod],
allowedOrigins: seq[string], allowedOrigins: seq[string],
allowedHeaders: Option[seq[string]],
reqOrigin = none[string]()): HttpHeaders = reqOrigin = none[string]()): HttpHeaders =
makeCorsHeaders(allowedMethods.mapIt($it), allowedOrigins, reqOrigin ) makeCorsHeaders(allowedMethods.mapIt($it), allowedOrigins, allowedHeaders, reqOrigin )
proc makeCorsHeaders*[T: HttpMethod or string](
allowedMethods: seq[T],
allowedOrigins: seq[string],
reqOrigin = none[string]()): HttpHeaders =
makeCorsHeaders(allowedMethods, allowedOrigins, none[seq[string]](), reqOrigin )
func origin*(req: Request): Option[string] = func origin*(req: Request): Option[string] =
@@ -76,6 +90,40 @@ func origin*(req: Request): Option[string] =
else: none[string]() else: none[string]()
func traceparent*(req: Request): Option[string] =
## Extract the traceparent from the request headers, if present.
if req.headers.contains("traceparent"):
return some(req.headers["traceparent"])
else:
return none[string]()
proc makeTraceContextHeaders*(req: Request, traceParentId: string): HttpHeaders =
var headers = HttpHeaders(@[])
if req.headers.contains("traceparent"):
# If the traceparent header is present, we should update it with our
# parent-id.
let traceparentParts = req.headers["traceparent"].split("-")
if traceparentParts.len != 4:
headers["traceparent"] = "00-$#-$#-00" % [
replace($genUUID(), "-", ""), # trace-id
traceParentId, # parent-id
]
else:
headers["traceparent"] = "00-$#-$#-$#" % [
traceparentParts[1], # trace-id
traceParentId, # parent-id
traceparentParts[3], # flags
]
if req.headers.contains("tracestate"):
headers["tracestate"] = req.headers["tracestate"]
return headers
proc respondWithRawJson*( proc respondWithRawJson*(
req: Request, req: Request,
body: JsonNode, body: JsonNode,