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

Option to allow the request body to be processed outside the asynchttpserver library. #13147

Merged
merged 12 commits into from
Feb 4, 2020
56 changes: 42 additions & 14 deletions lib/pure/asynchttpserver.nim
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export httpcore except parseHeader

const
maxLine = 8*1024
chunkSize = 1048

# TODO: If it turns out that the decisions that asynchttpserver makes
# explicitly, about whether to close the client sockets or upgrade them are
Expand All @@ -52,6 +53,7 @@ type
url*: Uri
hostname*: string ## The hostname of the client that made the request.
body*: string
bodyStream*: FutureStream[string]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when (NimMajor, NimMinor) >= (1, 1):
  bodyStream*: FutureStream[string]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when (NimMajor, NimMinor) >= (1, 1):
  bodyStream*: FutureStream[string]

When I put the annotation I get this error:
Error: constant expression expected

type
  Request* = object
    client*: AsyncSocket # TODO: Separate this into a Response object?
    reqMethod*: HttpMethod
    headers*: HttpHeaders
    protocol*: tuple[orig: string, major, minor: int]
    url*: Uri
    hostname*: string    ## The hostname of the client that made the request.
    body*: string
    when (NimMajor, NimMinor) >= (1, 1):
      bodyStream*: FutureStream[string]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah, what's up with that...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works if outside of type declaration

when (NimMajor, NimMinor) >= (1, 1):
  const
    maxLine = 8*1024
    chunkSize = 1048

  type
    Request* = object
      client*: AsyncSocket # TODO: Separate this into a Response object?
      reqMethod*: HttpMethod
      headers*: HttpHeaders
      protocol*: tuple[orig: string, major, minor: int]
      url*: Uri
      hostname*: string    ## The hostname of the client that made the request.
      body*: string
      bodyStream*: FutureStream[string] # <<<<<
else:
  const
    maxLine = 8*1024

  type
    Request* = object
      client*: AsyncSocket # TODO: Separate this into a Response object?
      reqMethod*: HttpMethod
      headers*: HttpHeaders
      protocol*: tuple[orig: string, major, minor: int]
      url*: Uri
      hostname*: string    ## The hostname of the client that made the request.
      body*: string

type
  AsyncHttpServer* = ref object
    socket: AsyncSocket
    reuseAddr: bool
    reusePort: bool
    maxBody: int ## The maximum content-length that will be read for the body.


AsyncHttpServer* = ref object
socket: AsyncSocket
Expand Down Expand Up @@ -128,6 +130,20 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
proc sendStatus(client: AsyncSocket, status: string): Future[void] =
client.send("HTTP/1.1 " & status & "\c\L\c\L")

proc parseUppercaseMethod(name: string): HttpMethod =
result =
case name
of "GET": HttpGet
of "POST": HttpPost
of "HEAD": HttpHead
of "PUT": HttpPut
of "DELETE": HttpDelete
of "PATCH": HttpPatch
of "OPTIONS": HttpOptions
of "CONNECT": HttpConnect
of "TRACE": HttpTrace
else: raise newException(ValueError, "Invalid HTTP method " & name)

proc processRequest(
server: AsyncHttpServer,
req: FutureVar[Request],
Expand All @@ -149,6 +165,7 @@ proc processRequest(
request.hostname.shallowCopy(address)
assert client != nil
request.client = client
request.bodyStream = newFutureStream[string]()

# We should skip at least one empty line before the request
# https://tools.ietf.org/html/rfc7230#section-3.5
Expand All @@ -173,17 +190,9 @@ proc processRequest(
for linePart in lineFut.mget.split(' '):
case i
of 0:
case linePart
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the inline case statement better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't touch this part of the library. Maybe Dominik Picheta can see this part.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you based your patch on something else but nim devel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I based my patch on nim devel.

of "GET": request.reqMethod = HttpGet
of "POST": request.reqMethod = HttpPost
of "HEAD": request.reqMethod = HttpHead
of "PUT": request.reqMethod = HttpPut
of "DELETE": request.reqMethod = HttpDelete
of "PATCH": request.reqMethod = HttpPatch
of "OPTIONS": request.reqMethod = HttpOptions
of "CONNECT": request.reqMethod = HttpConnect
of "TRACE": request.reqMethod = HttpTrace
else:
try:
request.reqMethod = parseUppercaseMethod(linePart)
except ValueError:
asyncCheck request.respondError(Http400)
return true # Retry processing of request
of 1:
Expand Down Expand Up @@ -236,17 +245,36 @@ proc processRequest(
# - Check for Content-length header
if request.headers.hasKey("Content-Length"):
var contentLength = 0
if parseSaturatedNatural(request.headers["Content-Length"], contentLength) == 0:
if parseSaturatedNatural(request.headers["Content-Length"],
contentLength) == 0:
await request.respond(Http400, "Bad Request. Invalid Content-Length.")
return true
else:
if contentLength > server.maxBody:
await request.respondError(Http413)
return false
request.body = await client.recv(contentLength)
if request.body.len != contentLength:

var remainder = contentLength
while remainder > 0:
let read_size = if remainder < chunkSize: remainder else: chunkSize
mrhdias marked this conversation as resolved.
Show resolved Hide resolved
let data = await client.recv(read_size)
if data.len != read_size:
await request.respond(Http500, "Internal Server Error. An error occurred while reading the request body.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you know it's not

await request.respond(Http400, "Bad Request. Content-Length does not match actual.")

return true
await request.bodyStream.write(data)
remainder -= data.len

if remainder > 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By construction we know that remainder <= 0 after the while loop.

await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
return true

request.bodyStream.complete()

## request.body = await client.recv(contentLength)
## if request.body.len != contentLength:
## await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
## return true

elif request.reqMethod == HttpPost:
await request.respond(Http411, "Content-Length required.")
return true
Expand Down