From 61aba4a86b4cb9607f325b175f3a878eabbbb5a6 Mon Sep 17 00:00:00 2001 From: termer Date: Mon, 20 Mar 2023 23:57:35 -0400 Subject: [PATCH 01/11] omit Server header if serverInfo is blank --- src/httpx.nim | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index 1bf0b83..ae0a238 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -175,9 +175,11 @@ 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 serverInfo != "": + text &= "\c\LServer: " & serverInfo + + text &= "\c\LDate: " & serverDate text &= otherHeaders text &= "\c\L\c\L" text &= body @@ -219,6 +221,7 @@ template acceptClient() = return raiseOSError(lastError) + setBlocking(client, false) selector.registerHandle(client, {Event.Read}, initData(Client, ip = address)) @@ -330,7 +333,7 @@ proc processEvents(selector: Selector[Data], case data.fdKind of Server: if Event.Read in events[i].events: - acceptClient() + acceptClient() else: doAssert false, "Only Read events are expected for the server" of Dispatcher: From 73096a793fb954fb43409a3278caa45cd0d38e5f Mon Sep 17 00:00:00 2001 From: termer Date: Mon, 20 Mar 2023 23:59:01 -0400 Subject: [PATCH 02/11] eat descriptor exhaustion errors instead of crashing --- src/httpx.nim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/httpx.nim b/src/httpx.nim index ae0a238..81c00f3 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -333,7 +333,13 @@ proc processEvents(selector: Selector[Data], case data.fdKind of Server: if Event.Read in events[i].events: - acceptClient() + try: + acceptClient() + except IOSelectorsException: + # Carry on without doing anything if the maximum number of descriptors is exhausted; hopefully there will be some available next tick + # termer 2023/03/20: There is no better way to check this error at the moment. The only way to differentiate descriptor exhaustion from other selector errors is based on its error message. + if getCurrentExceptionMsg() != "Maximum number of descriptors is exhausted!": + raise else: doAssert false, "Only Read events are expected for the server" of Dispatcher: From 6abcc593c8043edd8b8472bb460d568a32740ddb Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 00:29:25 -0400 Subject: [PATCH 03/11] expose more constants, allow disabling name const --- src/httpx.nim | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index 81c00f3..a563a2e 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -82,9 +82,34 @@ type HttpxDefect* = ref object of Defect -const - serverInfo {.strdefine.} = "Nim-HTTPX" - clientBufSzie = 256 +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 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. + +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.".} var serverDate {.threadvar.}: string @@ -176,8 +201,8 @@ proc send*(req: Request, code: HttpCode, body: string, contentLength: Option[int text &= "\c\LContent-Length: " text.addInt contentLength.unsafeGet() - when serverInfo != "": - text &= "\c\LServer: " & serverInfo + when httpxServerName != "": + text &= "\c\LServer: " & httpxServerName text &= "\c\LDate: " & serverDate text &= otherHeaders @@ -351,13 +376,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) @@ -391,7 +416,8 @@ proc processEvents(selector: Selector[Data], if data.bytesSent != 0: logging.warn("bytesSent isn't empty.") - let waitingForBody = methodNeedsBody(data) and bodyInTransit(data) + #let waitingForBody = methodNeedsBody(data) and bodyInTransit(data) + let waitingForBody = false if likely(not waitingForBody): # For pipelined requests, we need to reset this flag. data.headersFinished = true @@ -419,7 +445,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: From 5369e4526ef347d7a2579d98038e7fae9935a0c6 Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 00:44:17 -0400 Subject: [PATCH 04/11] improve documentation, make server date optional --- src/httpx.nim | 59 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index a563a2e..506aab7 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -65,22 +65,38 @@ 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. @@ -111,7 +127,14 @@ when httpxClientBufSize < 3: elif httpxClientBufSize < httpxClientBufDefaultSize: {.warning: "You should set your client read buffer size to at least 256 bytes. Smaller buffers will harm performance.".} -var serverDate {.threadvar.}: string +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 proc doNothing(): Startup {.gcsafe.} = result = proc () {.closure, gcsafe.} = @@ -120,24 +143,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 = @@ -204,7 +218,9 @@ proc send*(req: Request, code: HttpCode, body: string, contentLength: Option[int when httpxServerName != "": text &= "\c\LServer: " & httpxServerName - text &= "\c\LDate: " & serverDate + when httpxSendServerDate: + text &= "\c\LDate: " & serverDate + text &= otherHeaders text &= "\c\L\c\L" text &= body @@ -486,9 +502,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 @@ -506,9 +523,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: From 973b152ed5a6aacac8505ad166c5259a30db89d7 Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 00:49:19 -0400 Subject: [PATCH 05/11] document behavior for empty httpxServerName const --- src/httpx.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/httpx.nim b/src/httpx.nim index 506aab7..ef60206 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -113,6 +113,7 @@ const httpxServerName* {.strdefine.} = 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. From 178aa532a4bef85176265dababe02a132383d031 Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 01:38:16 -0400 Subject: [PATCH 06/11] fix debug line being uncommented --- src/httpx.nim | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index ef60206..affab7c 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -16,6 +16,26 @@ # limitations under the License. + + + + + + + + +# TODO Set max headers size via constant that can be defined at compile time aas well + + + + + + + + + + + import net, nativesockets, os, httpcore, asyncdispatch, strutils import options, logging, times, heapqueue, std/monotimes import std/sugar @@ -433,8 +453,7 @@ proc processEvents(selector: Selector[Data], if data.bytesSent != 0: logging.warn("bytesSent isn't empty.") - #let waitingForBody = methodNeedsBody(data) and bodyInTransit(data) - let waitingForBody = false + let waitingForBody = methodNeedsBody(data) and bodyInTransit(data) if likely(not waitingForBody): # For pipelined requests, we need to reset this flag. data.headersFinished = true From 5d8fc7fd560b0d08375c7d8e9d7c1094513e834e Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 01:38:37 -0400 Subject: [PATCH 07/11] remove TODO comment --- src/httpx.nim | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index affab7c..4a82560 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -16,26 +16,6 @@ # limitations under the License. - - - - - - - - -# TODO Set max headers size via constant that can be defined at compile time aas well - - - - - - - - - - - import net, nativesockets, os, httpcore, asyncdispatch, strutils import options, logging, times, heapqueue, std/monotimes import std/sugar From b80035c4a4a7ba66e43e7cf5659cba9c93646a34 Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 03:04:14 -0400 Subject: [PATCH 08/11] check for file descriptor limit before accepting clients --- src/httpx.nim | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index 4a82560..a429092 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -137,6 +137,9 @@ when httpxSendServerDate: # 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.} = discard @@ -252,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() @@ -265,8 +270,10 @@ template acceptClient() = raiseOSError(lastError) setBlocking(client, false) - selector.registerHandle(client, {Event.Read}, - initData(Client, ip = address)) + + # Only register the handle if the file descriptor count has not been reached + if likely(client.int < osMaxFdCount): + selector.registerHandle(client, {Event.Read}, initData(Client, ip = address)) template closeClient(selector: Selector[Data], fd: SocketHandle|int, @@ -375,13 +382,7 @@ proc processEvents(selector: Selector[Data], case data.fdKind of Server: if Event.Read in events[i].events: - try: - acceptClient() - except IOSelectorsException: - # Carry on without doing anything if the maximum number of descriptors is exhausted; hopefully there will be some available next tick - # termer 2023/03/20: There is no better way to check this error at the moment. The only way to differentiate descriptor exhaustion from other selector errors is based on its error message. - if getCurrentExceptionMsg() != "Maximum number of descriptors is exhausted!": - raise + tryAcceptClient() else: doAssert false, "Only Read events are expected for the server" of Dispatcher: @@ -543,7 +544,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] From 512277b0e0e4809f87d4fb139c6a5c3b123e1f15 Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 03:08:51 -0400 Subject: [PATCH 09/11] only do FD check on posix --- src/httpx.nim | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index a429092..f464698 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -271,10 +271,17 @@ template tryAcceptClient() = setBlocking(client, false) - # Only register the handle if the file descriptor count has not been reached - if likely(client.int < osMaxFdCount): + 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, inLoop = true) = From f71ee33f8684245c1816c8989af9457f391af8c1 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 21 Mar 2023 15:15:43 +0800 Subject: [PATCH 10/11] Update src/httpx.nim --- src/httpx.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/httpx.nim b/src/httpx.nim index f464698..7f73ea9 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -389,7 +389,7 @@ proc processEvents(selector: Selector[Data], case data.fdKind of Server: if Event.Read in events[i].events: - tryAcceptClient() + tryAcceptClient() else: doAssert false, "Only Read events are expected for the server" of Dispatcher: From 0d155fa9c8c3bebd0467046ad415674fccf35d70 Mon Sep 17 00:00:00 2001 From: termer Date: Tue, 21 Mar 2023 03:16:17 -0400 Subject: [PATCH 11/11] fix trying to access selectors module on windows --- src/httpx.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/httpx.nim b/src/httpx.nim index f464698..db05676 100644 --- a/src/httpx.nim +++ b/src/httpx.nim @@ -137,8 +137,10 @@ when httpxSendServerDate: # 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 + +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.} =