Fix to asynchttpserver form data/body broken with #13147 (#13394)

* Fix to asynchttpserver form data/body broken with #13147
* New implementation that use a interator instance of future streams
* asynchttpserver now can handle chunks of data.
This commit is contained in:
Henrique Dias 2020-02-14 20:59:37 +00:00 • committed by GitHub
commit 5bf571f061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 75 additions and 44 deletions

View file

@ -65,7 +65,8 @@
## Library changes ## Library changes
- `asynchttpserver` now the request body is a FutureStream. - `asynchttpserver` added an iterator that allows the request body to be read in
chunks of data when new server "stream" option is set to true.
- `asyncdispatch.drain` now properly takes into account `selector.hasPendingOperations` - `asyncdispatch.drain` now properly takes into account `selector.hasPendingOperations`
and only returns once all pending async operations are guaranteed to have completed. and only returns once all pending async operations are guaranteed to have completed.
- `asyncdispatch.drain` now consistently uses the passed timeout value for all - `asyncdispatch.drain` now consistently uses the passed timeout value for all

View file

@ -41,13 +41,13 @@
## import asynchttpserver, asyncdispatch ## import asynchttpserver, asyncdispatch
## import strutils, strformat ## import strutils, strformat
## ##
## const stream = true # for test purposes switch from true to false
##
## proc htmlpage(contentLength, bodyLength: int): string = ## proc htmlpage(contentLength, bodyLength: int): string =
## return &""" ## return &"""
## <!Doctype html> ## <!Doctype html>
## <html lang="en"> ## <html lang="en">
## <head> ## <head><meta charset="utf-8"/></head>
## <meta charset="utf-8"/>
## </head>
## <body> ## <body>
## <form action="/" method="post" enctype="multipart/form-data"> ## <form action="/" method="post" enctype="multipart/form-data">
## File: <input type="file" name="testfile" accept="text/*"><br /> ## File: <input type="file" name="testfile" accept="text/*"><br />
@ -65,17 +65,23 @@
## bodyLength = 0 ## bodyLength = 0
## if req.reqMethod == HttpPost: ## if req.reqMethod == HttpPost:
## contentLength = req.headers["Content-length"].parseInt ## contentLength = req.headers["Content-length"].parseInt
## if contentLength < 8*1024: # the default chunkSize ## if stream:
## # read the request body at once ## # Read 8*1024 bytes at a time
## let body = await req.bodyStream.readAll(); ## # optional chunkSize parameter. The default is 8*1024
## bodyLength = body.len ## for length, data in req.bodyStream(8*1024):
## let content = await data
## if length == content.len:
## bodyLength += content.len
## else: ## else:
## # read 8*1024 bytes at a time ## # Handle exception
## while (let data = await req.bodyStream.read(); data[0]): ## await req.respond(Http400,
## bodyLength += data[1].len ## "Bad Request. Data read has a different length than the expected.")
## return
## else:
## bodyLength += req.body.len
## await req.respond(Http200, htmlpage(contentLength, bodyLength)) ## await req.respond(Http200, htmlpage(contentLength, bodyLength))
## ##
## let server = newAsyncHttpServer(maxBody = 10485760) # 10 MB ## let server = newAsyncHttpServer(maxBody = 10485760, stream = stream) # 10 MB
## waitFor server.serve(Port(8080), cb) ## waitFor server.serve(Port(8080), cb)
import tables, asyncnet, asyncdispatch, parseutils, uri, strutils import tables, asyncnet, asyncdispatch, parseutils, uri, strutils
@ -93,9 +99,6 @@ const
maxLine = 8*1024 maxLine = 8*1024
when (NimMajor, NimMinor) >= (1, 1): when (NimMajor, NimMinor) >= (1, 1):
const
chunkSize = 8*1024 ## This seems perfectly reasonable for default chunkSize.
type type
Request* = object Request* = object
client*: AsyncSocket # TODO: Separate this into a Response object? client*: AsyncSocket # TODO: Separate this into a Response object?
@ -104,8 +107,16 @@ when (NimMajor, NimMinor) >= (1, 1):
protocol*: tuple[orig: string, major, minor: int] protocol*: tuple[orig: string, major, minor: int]
url*: Uri url*: Uri
hostname*: string ## The hostname of the client that made the request. hostname*: string ## The hostname of the client that made the request.
body*: string # For future removal body*: string
bodyStream*: FutureStream[string] contentLength*: int
type
AsyncHttpServer* = ref object
socket: AsyncSocket
reuseAddr: bool
reusePort: bool
maxBody: int ## The maximum content-length that will be read for the body.
stream: bool ## By default (stream = false), the body of the request is read immediately
else: else:
type type
Request* = object Request* = object
@ -117,14 +128,24 @@ else:
hostname*: string ## The hostname of the client that made the request. hostname*: string ## The hostname of the client that made the request.
body*: string body*: string
type type
AsyncHttpServer* = ref object AsyncHttpServer* = ref object
socket: AsyncSocket socket: AsyncSocket
reuseAddr: bool reuseAddr: bool
reusePort: bool reusePort: bool
maxBody: int ## The maximum content-length that will be read for the body. maxBody: int ## The maximum content-length that will be read for the body.
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false, when (NimMajor, NimMinor) >= (1, 1):
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
maxBody = 8388608, stream = false): AsyncHttpServer =
## Creates a new ``AsyncHttpServer`` instance.
new result
result.reuseAddr = reuseAddr
result.reusePort = reusePort
result.maxBody = maxBody
result.stream = stream
else:
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
maxBody = 8388608): AsyncHttpServer = maxBody = 8388608): AsyncHttpServer =
## Creates a new ``AsyncHttpServer`` instance. ## Creates a new ``AsyncHttpServer`` instance.
new result new result
@ -193,13 +214,30 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
proc sendStatus(client: AsyncSocket, status: string): Future[void] = proc sendStatus(client: AsyncSocket, status: string): Future[void] =
client.send("HTTP/1.1 " & status & "\c\L\c\L") client.send("HTTP/1.1 " & status & "\c\L\c\L")
when (NimMajor, NimMinor) >= (1, 1):
iterator bodyStream*(
request: Request,
chunkSize: int = 8*1024): (int, Future[string]) =
## The chunkSize parameter is optional and default value is 8*1024 bytes.
##
## This iterator return a tuple with the length of the data that was read
## and a future.
var remainder = request.contentLength
while remainder > 0:
let readSize = min(remainder, chunkSize)
let data = request.client.recv(readSize)
if data.failed:
raise newException(ValueError, "Error reading POST data from client.")
yield (readSize, data)
remainder -= readSize
proc processRequest( proc processRequest(
server: AsyncHttpServer, server: AsyncHttpServer,
req: FutureVar[Request], req: FutureVar[Request],
client: AsyncSocket, client: AsyncSocket,
address: string, address: string,
lineFut: FutureVar[string], lineFut: FutureVar[string],
callback: proc (request: Request): Future[void] {.closure, gcsafe.}, callback: proc (request: Request): Future[void] {.closure, gcsafe.}
): Future[bool] {.async.} = ): Future[bool] {.async.} =
# Alias `request` to `req.mget()` so we don't have to write `mget` everywhere. # Alias `request` to `req.mget()` so we don't have to write `mget` everywhere.
@ -213,13 +251,9 @@ proc processRequest(
request.hostname.shallowCopy(address) request.hostname.shallowCopy(address)
assert client != nil assert client != nil
request.client = client request.client = client
request.body = ""
when (NimMajor, NimMinor) >= (1, 1): when (NimMajor, NimMinor) >= (1, 1):
request.bodyStream = newFutureStream[string]() request.contentLength = 0
# To uncomment in the future after compatibility issues
# with third parties are solved
# else:
# request.body = ""
request.body = "" # Temporary fix for future removal
# We should skip at least one empty line before the request # We should skip at least one empty line before the request
# https://tools.ietf.org/html/rfc7230#section-3.5 # https://tools.ietf.org/html/rfc7230#section-3.5
@ -316,16 +350,12 @@ proc processRequest(
return false return false
when (NimMajor, NimMinor) >= (1, 1): when (NimMajor, NimMinor) >= (1, 1):
var remainder = contentLength request.contentLength = contentLength
while remainder > 0: if not server.stream:
let readSize = min(remainder, chunkSize) request.body = await client.recv(contentLength)
let data = await client.recv(read_size) if request.body.len != contentLength:
if data.len != read_size:
await request.respond(Http400, "Bad Request. Content-Length does not match actual.") await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
return true return true
await request.bodyStream.write(data)
remainder -= data.len
request.bodyStream.complete()
else: else:
request.body = await client.recv(contentLength) request.body = await client.recv(contentLength)
if request.body.len != contentLength: if request.body.len != contentLength: