import scorper
proc myCallback(req: Request) {.async.} =
let headers = {"Content-Type": "text/plain"}
await req.resp("Hello World!", headers.newHttpHeaders())
waitFor serve("127.0.0.1:8888", myCallback)
import scorper
proc helloWorld(req: Request) {.async.} =
let headers = {"Content-Type": "text/plain"}
await req.resp("Hello World!", headers.newHttpHeaders())
proc goodbyeWorld(req: Request) {.async.} =
let headers = {"Content-Type": "text/plain"}
await req.resp("Goodbye World!", headers.newHttpHeaders())
# Create router
let router = newRouter[ScorperCallback]()
router.addRoute(helloWorld, "get", "/hello")
router.addRoute(goodbyeWorld, "get", "/goodbye")
# Create Scorper
let address = "127.0.0.1:8888"
let flags = {ReuseAddr} # flags defined by chronos
var server = newScorper(address, router, flags)
# You can pass a callback to a Scorper instead of a Router as we did before
# Start Scorper
server.start()
waitFor server.join()
Note: The difference between the serve and newScorper proc is that serve cannot handle routers, has no return value and automatically calls the start and join procs; see below
proc serve*(address: string,
callback: ScorperCallback,
flags: set[ServerFlags] = {ReuseAddr},
maxBody = 8.Mb,
isSecurity = false,
privateKey: string = "",
certificate: string = "",
secureFlags: set[TLSFlags] = {},
tlsMinVersion = TLSVersion.TLS11,
tlsMaxVersion = TLSVersion.TLS12,
cache: TLSSessionCache = nil,
) {.async.} =
#...
server.start()
await server.join()
proc newScorper*(address: string, handler: ScorperCallback | Router[ScorperCallback] = default(ScorperCallback),
flags: set[ServerFlags] = {ReuseAddr},
maxBody = 8.Mb,
isSecurity = false,
privateKey: string = "",
certificate: string = "",
secureFlags: set[TLSFlags] = {},
tlsMinVersion = TLSVersion.TLS11,
tlsMaxVersion = TLSVersion.TLS12,
cache: TLSSessionCache = nil,
): Scorper =
#...
The route pragma can be used on handlers to then mounted to a Router in bulk for enhanced development efficiency.
First we define our routes:
# routemount.nim
import chronos
import scorper
proc handlerA*(req: Request) {.route("get", "/a"), async.} =
# do things here if `get` request comes to {SERVERADDRESS}/a
let headers = {"Content-type": "text/plain"}
await req.resp("There's no helping you here", headers.newHttpHeaders())
proc handlerB*(req: Request) {.route(["get", "post"], "/b"), async.} =
# do things here if a `get` or `post` request comes to {SERVERADDRESS}/b
let headers = {"Content-type": "text/plain"}
await req.resp("There's no help here either", headers.newHttpHeaders())
And then we mount them.
# main.nim
import chronos
import scorper
import ./routemount # file defined above
let router = newRouter[ScorperCallback]()
router.mount(routemount) # Mounting will bulk add the routes; you can use addRoute()
doAssert(router.len == 2) # and pass the handler names to add them manually instead
# eg: router.addRoute(handlerA)
# Can pass the router loaded with the routes specified in the routemount
# file to a Scorper as normal
let flags = {ReuseAddr}
var server = newScorper("127.0.0.1:8888", router, flags)
server.start()
waitFor server.join()
Content-length etc will be autofilled in the response by the server if empty headers are passed
proc handler(request: Request) {.async.} =
await request.resp("Hello World, 200", newHttpHeaders())
# The client response headers will include Content-Length of 16
Content-Length will be enforced in the response by the server if headers with the key are passed
proc handler(request: Request) {.async.} =
let headers = {"Content-Length": "0"}
await request.resp("Hello World, 200", headers.newHttpHeaders())
# The client response body will be empty
proc handler(request: Request) {.async.} =
await request.resp("Oops!, 404", code = Http404)
Incomplete API. This is subject to change. See src/scorper/http/httpbasicauth
A request can have authorization basic headers automatically decoded and then passed to a validator for verification.
# Create validator
# Must take a Request and two string params and return a Future[bool]
proc basicValidation(request: Request, user, pass: string): Future[bool] {.async.} =
# Check the decoded user and pass against your database/whatever
# return true for authentication to be accepted and for the calling callback to continue
# return false for automatic Http401 response
return true
# Create your callback
proc handler(request: Request) {.async.} =
# Create validation object and pass to request method basicAuth
var validator = HttpBasicAuthValidator(basicValidation) # our validator proc
if await request.basicAuth(validator):
# Do things because request is validified
else:
# Do things here because request was invalid
# RESPONSE UNNECESSARY; see note below
# basicAuth proc returns a bool; however any failure to validate the auth header
# will result in an automatic response of either Http400 or Http401. An if/else
# statement can be used however responses for failure are unnecessary.
# Using the statement below is therefore perfectly valid
#
# if not await request.basicAuth(validator): return
# Can create router here or set server handler directly
let address, flags = "127.0.0.1:8888", {ReuseAddr}
let server = newScorper(address, handler, flags)
server.start()
waitFor server.join()
A variety of procs can be used to generate responses to requests. The procedures and their parameters are listed below; please see src/scorper/http/streamserver for their implementations.
Listed below are some common response procs.
proc respBasicAuth*(req: ImpRequest, scheme = "Basic", realm = "Scorper", params: seq[tuple[key: string,
value: string]] = @[], code = Http401): Future[void] {.async.}
## Responds to the req with the specified ``HttpCode``; defaults to Http401
proc respError*(req: ImpRequest, code: HttpCode, content: sink string, headers = newHttpHeaders()): Future[
void] {.async.}
proc respError*(req: ImpRequest, code: HttpCode, headers = newHttpHeaders()): Future[void] {.async.}
## Responds to the req with the specified ``HttpCode``.
proc respStatus*(req: ImpRequest, code: HttpCode, ver = HttpVer11): Future[void] {.async.}
proc respStatus*(req: ImpRequest, code: HttpCode, msg: string, ver = HttpVer11): Future[void] {.async.}
## Chore: include responses related to files/downloads etc
proc handler(request: Request) {.async.} =
let j = await request.json() # If the request does not include valid
await request.resp($j) # JSON then a Http400 is returned with
# "Error" body
Route variables and queries can be accessed by the request params
and query
table fields.
import scorper
proc handler(request: Request) {.async.} =
doAssert request.params["author"] == "bung87" # Access parsed param in route
doAssert request.params["module"] == "scorper"
doAssert request.query["q"] == "is_amazing" # Access parsed query
await request.resp("")
# Create router
let address, flags = "127.0.0.1:8888", {ReuseAddr}
let router = newRouter[ScorperCallback]()
# Add route with params
router.addRoute(handler, "get", "/nim/{author}/{module}")
# Start server
let server = newScorper(address, router, flags)
server.start()
waitFor server.join()
# curl 127.0.0.1:8888/nim/bung87/scorper?q=is_amazing
Encoded uri requests are decoded automatically.
When handling Request Route params etc; the decoding occurs before the tables are populated. This means when accessing the route variable, you will receive the decoded input.
proc handler(request: Request) {.async.} =
doAssert request.params["decoded"] == "ß" # Decoded from %C3%9F
await request.resp("")
# Create router
let address, flags = "127.0.0.1:8888", {ReuseAddr}
let router = newRouter[ScorperCallback]()
#Add route with params
router.addRoute(handler, "get", "/scorper/{decoded}")
# Start server
let server = newScorper(address, router, flags)
server.start()
waitFor server.join()
# curl 127.0.0.1:8888/scorper/%C3%9F
Requests with content-types of url encoded forms will automatically be decoded. Lets look at the following example where a client posts the following message:
UserName=test&UserNameKana=%E3%83%86%E3%82%B9%E3%83%88&MailAddress=test%40example.com
With headers Content-Type
of application/x-www-form-urlencoded
.
The content of the form is accessible from the request as follows:
proc handler(request: Request) {.async.} =
let form = await request.form
doAssert $form is string
doAssert form.data["UserName"] == "test"
doAssert form.data["UserNameKana"] == "テスト"
doAssert form.data["MailAddress"] == "[email protected]"
await request.resp("")
from chronos
type
ServerFlags* = enum
## Server's flags
ReuseAddr, ReusePort, TcpNoDelay, NoAutoRead, GCUserData, FirstPipe,
NoPipeFlash, Broadcast
type Request* = ref object
meth*: HttpMethod
headers*: HttpHeaders
protocol*: tuple[major, minor: int]
url*: Url # from urlly
path*: string # http req path
hostname*: string
ip*: string
params*: Table[string, string]
query*: seq[(string, string)]
type
HttpHeaders* = ref object
table*: TableRef[string, seq[string]]
HttpHeaderValues* = distinct seq[string]
type
HttpCode* = distinct range[0 .. 599]
Predefined for the following:
const Http100* = HttpCode(100) #... 101, 200, 201, 202, 203, 204, 205, 206, 300, 301, 302, 303, 304, 305, 307, 308, 400, 401, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504 Http505* = HttpCode(505)
type ScorperCallback* = proc (req: Request): Future[void] {.closure, gcsafe.}
type Scorper* = ref object of StreamServer
# inherited (partial)
sock*: AsyncFD # Socket
local*: TransportAddress # Address
status*: ServerStatus # Current server status
flags*: set[ServerFlags] # Flags
errorCode*: OSErrorCode
type MiddlewareProc* = proc (req: Request): Future[bool]
type
HttpVersion* = enum
HttpVer11 = "HTTP/1.1",
HttpVer10 = "HTTP/1.0"
HttpVer20 = "HTTP/2.0"
type
HttpMethod* = enum ## the requested HttpMethod
HttpHead, ## Asks for the response identical to the one that would
## correspond to a GET request, but without the response
## body.
HttpGet, ## Retrieves the specified resource.
HttpPost, ## Submits data to be processed to the identified
## resource. The data is included in the body of the
## request.
HttpPut, ## Uploads a representation of the specified resource.
HttpDelete, ## Deletes the specified resource.
HttpTrace, ## Echoes back the received request, so that a client
## can see what intermediate servers are adding or
## changing in the request.
HttpOptions, ## Returns the HTTP methods that the server supports
## for specified address.
HttpConnect, ## Converts the request connection to a transparent
## TCP/IP tunnel, usually used for proxies.
HttpPatch ## Applies partial modifications to a resource.
type HttpBasicAuthValidator* = ref object
validate: proc (request: Request, user, pass: string): Future[bool] {.closure, gcsafe.}
type
HttpErrorCode* = range[400 .. 599]
HttpError* = ref object of CatchableError # See chronos error handling
code*: HttpErrorCode
see router
type
HttpVerb* = enum ## Available methods to associate a mapped handler with
GET = "get"
HEAD = "head"
OPTIONS = "options"
PUT = "put"
POST = "post"
DELETE = "delete"
Internal implementation
type Router*[H] = ref object
verbTrees: CritBitTree[PatternNode[H]]
type
Route* = object ## Arguments extracted from a request while routing it
params*: TableRef[string, string]
prefix*: string
see HttpCode type
const CRLF* = "\c\L"