Skip to content

Commit

Permalink
Merge pull request #22 from termermc/master
Browse files Browse the repository at this point in the history
Expose more options, avoid descriptor exhaustion crashes, improve documentation
  • Loading branch information
ringabout authored Mar 22, 2023
2 parents 5b85d64 + 0fc888e commit 4bf9e59
Showing 1 changed file with 97 additions and 33 deletions.
130 changes: 97 additions & 33 deletions src/httpx.nim
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,82 @@ type

type
Request* = object
## An HTTP request

selector: Selector[Data]

client*: SocketHandle
## The underlying operating system socket handle associated with the request client connection.
## May be closed; you should check by calling "closed" on this Request object before interacting with its client socket.

# Identifier used to distinguish requests.
requestID: uint

OnRequest* = proc (req: Request): Future[void] {.gcsafe, gcsafe.}
## Callback used to handle HTTP requests

Startup = proc () {.closure, gcsafe.}

Settings* = object
## HTTP server settings

port*: Port
## The port to bind to

bindAddr*: string
## The address to bind to

numThreads: int
## The number of threads to serve on

startup: Startup

HttpxDefect* = ref object of Defect
## Defect raised when something HTTPX-specific fails

const httpxDefaultServerName* = "Nim-HTTPX"
## The default server name sent in the Server header in responses.
## A custom name can be set by defining httpxServerName at compile time.

const
serverInfo {.strdefine.} = "Nim-HTTPX"
clientBufSzie = 256
const serverInfo {.strdefine.}: string = httpxDefaultServerName
## Alias to httpxServerName, use that instead

const httpxServerName* {.strdefine.} =
when serverInfo != httpxDefaultServerName:
{.warning: "Setting the server name with serverInfo is deprecated. You should use httpxServerName instead.".}
serverInfo
else:
httpxDefaultServerName
## The server name sent in the Server header in responses.
## If not defined, the value of httpxDefaultServerName will be used.
## If the value is empty, no Server header will be sent.

var serverDate {.threadvar.}: string
const httpxClientBufDefaultSize* = 256
## The default size of the client read buffer.
## A custom size can be set by defining httpxClientBufSize.

const httpxClientBufSize* {.intdefine.} = httpxClientBufDefaultSize
## The size of the client read buffer.
## Defaults to httpxClientBufDefaultSize.

when httpxClientBufSize < 3:
{.fatal: "Client buffer size must be at least 3, and ideally at least 256.".}
elif httpxClientBufSize < httpxClientBufDefaultSize:
{.warning: "You should set your client read buffer size to at least 256 bytes. Smaller buffers will harm performance.".}

const httpxSendServerDate* {.booldefine.} = true
## Whether to send the current server date along with requests.
## Defaults to true.

when httpxSendServerDate:
# We store the current server date here as a thread var and update it periodically to avoid checking the date each time we respond to a request.
# The date is updated every second from within the event loop.
var serverDate {.threadvar.}: string


when usePosixVersion:
let osMaxFdCount = selectors.maxDescriptors()
## The maximum number of file descriptors allowed at one time by the OS

proc doNothing(): Startup {.gcsafe.} =
result = proc () {.closure, gcsafe.} =
Expand All @@ -95,24 +149,15 @@ proc doNothing(): Startup {.gcsafe.} =
func initSettings*(port = Port(8080),
bindAddr = "",
numThreads = 0,
startup: Startup,
startup: Startup = doNothing(),
): Settings =
result = Settings(
port: port,
bindAddr: bindAddr,
numThreads: numThreads,
startup: startup
)
## Creates a new HTTP server Settings object with the provided options.

func initSettings*(port = Port(8080),
bindAddr = "",
numThreads = 0
): Settings =
result = Settings(
port: port,
bindAddr: bindAddr,
numThreads: numThreads,
startup: doNothing()
startup: startup
)

func initData(fdKind: FdKind, ip = ""): Data =
Expand Down Expand Up @@ -175,9 +220,13 @@ proc send*(req: Request, code: HttpCode, body: string, contentLength: Option[int
if contentLength.isSome:
text &= "\c\LContent-Length: "
text.addInt contentLength.unsafeGet()
text &= "\c\LServer: " & serverInfo
text &= "\c\LDate: "
text &= serverDate

when httpxServerName != "":
text &= "\c\LServer: " & httpxServerName

when httpxSendServerDate:
text &= "\c\LDate: " & serverDate

text &= otherHeaders
text &= "\c\L\c\L"
text &= body
Expand Down Expand Up @@ -208,7 +257,9 @@ proc send*(req: Request, body: string, code = Http200) {.inline.} =
## **Warning:** This can only be called once in the OnRequest callback.
req.send(code, body)

template acceptClient() =
template tryAcceptClient() =
## Tries to accept a client, but does nothing if one cannot be accepted (due to file descriptor exhaustion, etc)

let (client, address) = fd.SocketHandle.accept
if client == osInvalidSocket:
let lastError = osLastError()
Expand All @@ -219,9 +270,19 @@ template acceptClient() =
return

raiseOSError(lastError)

setBlocking(client, false)
selector.registerHandle(client, {Event.Read},
initData(Client, ip = address))

template regHandle() =
selector.registerHandle(client, {Event.Read}, initData(Client, ip = address))

when usePosixVersion:
# Only register the handle if the file descriptor count has not been reached
if likely(client.int < osMaxFdCount):
regHandle()
else:
regHandle()


template closeClient(selector: Selector[Data],
fd: SocketHandle|int,
Expand Down Expand Up @@ -330,7 +391,7 @@ proc processEvents(selector: Selector[Data],
case data.fdKind
of Server:
if Event.Read in events[i].events:
acceptClient()
tryAcceptClient()
else:
doAssert false, "Only Read events are expected for the server"
of Dispatcher:
Expand All @@ -342,13 +403,13 @@ proc processEvents(selector: Selector[Data],
discard
of Client:
if Event.Read in events[i].events:
var buf: array[clientBufSzie, char]
var buf: array[httpxClientBufSize, char]
# Read until EAGAIN. We take advantage of the fact that the client
# will wait for a response after they send a request. So we can
# comfortably continue reading until the message ends with \c\l
# \c\l.
while true:
let ret = recv(fd.SocketHandle, addr buf[0], clientBufSzie, 0.cint)
let ret = recv(fd.SocketHandle, addr buf[0], httpxClientBufSize, 0.cint)
if ret == 0:
closeClient(selector, fd)

Expand Down Expand Up @@ -410,7 +471,7 @@ proc processEvents(selector: Selector[Data],
else:
validateResponse(data)

if ret != clientBufSzie:
if ret != httpxClientBufSize:
# Assume there is nothing else for us right now and break.
break
elif Event.Write in events[i].events:
Expand Down Expand Up @@ -451,9 +512,10 @@ proc processEvents(selector: Selector[Data],
else:
assert false

proc updateDate(fd: AsyncFD): bool =
result = false # Returning true signifies we want timer to stop.
serverDate = now().utc().format("ddd, dd MMM yyyy HH:mm:ss 'GMT'")
when httpxSendServerDate:
proc updateDate(fd: AsyncFD): bool =
result = false # Returning true signifies we want timer to stop.
serverDate = now().utc().format("ddd, dd MMM yyyy HH:mm:ss 'GMT'")

proc eventLoop(params: (OnRequest, Settings)) =
let
Expand All @@ -471,9 +533,11 @@ proc eventLoop(params: (OnRequest, Settings)) =
server.getFd.setBlocking(false)
selector.registerHandle(server.getFd, {Event.Read}, initData(Server))

# Set up timer to get current date/time.
discard updateDate(0.AsyncFD)
asyncdispatch.addTimer(1000, false, updateDate)
when httpxSendServerDate:
# Set up timer to get current date/time.
discard updateDate(0.AsyncFD)
asyncdispatch.addTimer(1000, false, updateDate)

let disp = getGlobalDispatcher()

when usePosixVersion:
Expand All @@ -489,7 +553,7 @@ proc eventLoop(params: (OnRequest, Settings)) =
# See https://github.com/nim-lang/Nim/issues/7532.
# Not processing callbacks can also lead to exceptions being silently
# lost!
if unlikely(asyncdispatch.getGlobalDispatcher().callbacks.len > 0):
if unlikely(disp.callbacks.len > 0):
asyncdispatch.poll(0)
else:
var events: array[64, ReadyKey]
Expand Down

0 comments on commit 4bf9e59

Please sign in to comment.