Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose more options, avoid descriptor exhaustion crashes, improve documentation #22

Merged
merged 12 commits into from
Mar 22, 2023
128 changes: 95 additions & 33 deletions src/httpx.nim
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,80 @@ 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.}: string = httpxDefaultServerName
## Alias to httpxServerName, use that instead

const
serverInfo {.strdefine.} = "Nim-HTTPX"
clientBufSzie = 256
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.

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.

var serverDate {.threadvar.}: string
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

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 +147,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 +218,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 +255,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 +268,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()
ringabout marked this conversation as resolved.
Show resolved Hide resolved
else:
regHandle()


template closeClient(selector: Selector[Data],
fd: SocketHandle|int,
Expand Down Expand Up @@ -330,7 +389,7 @@ proc processEvents(selector: Selector[Data],
case data.fdKind
of Server:
if Event.Read in events[i].events:
acceptClient()
tryAcceptClient()
ringabout marked this conversation as resolved.
Show resolved Hide resolved
else:
doAssert false, "Only Read events are expected for the server"
of Dispatcher:
Expand All @@ -342,13 +401,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 +469,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 +510,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 +531,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 +551,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