Skip to content

Commit 5bf571f

Browse files
authored
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.
1 parent 3300e31 commit 5bf571f

File tree

2 files changed

+75
-44
lines changed

2 files changed

+75
-44
lines changed

changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565

6666
## Library changes
6767

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

lib/pure/asynchttpserver.nim

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@
4141
## import asynchttpserver, asyncdispatch
4242
## import strutils, strformat
4343
##
44+
## const stream = true # for test purposes switch from true to false
45+
##
4446
## proc htmlpage(contentLength, bodyLength: int): string =
4547
## return &"""
4648
## <!Doctype html>
4749
## <html lang="en">
48-
## <head>
49-
## <meta charset="utf-8"/>
50-
## </head>
50+
## <head><meta charset="utf-8"/></head>
5151
## <body>
5252
## <form action="/" method="post" enctype="multipart/form-data">
5353
## File: <input type="file" name="testfile" accept="text/*"><br />
@@ -65,17 +65,23 @@
6565
## bodyLength = 0
6666
## if req.reqMethod == HttpPost:
6767
## contentLength = req.headers["Content-length"].parseInt
68-
## if contentLength < 8*1024: # the default chunkSize
69-
## # read the request body at once
70-
## let body = await req.bodyStream.readAll();
71-
## bodyLength = body.len
68+
## if stream:
69+
## # Read 8*1024 bytes at a time
70+
## # optional chunkSize parameter. The default is 8*1024
71+
## for length, data in req.bodyStream(8*1024):
72+
## let content = await data
73+
## if length == content.len:
74+
## bodyLength += content.len
75+
## else:
76+
## # Handle exception
77+
## await req.respond(Http400,
78+
## "Bad Request. Data read has a different length than the expected.")
79+
## return
7280
## else:
73-
## # read 8*1024 bytes at a time
74-
## while (let data = await req.bodyStream.read(); data[0]):
75-
## bodyLength += data[1].len
81+
## bodyLength += req.body.len
7682
## await req.respond(Http200, htmlpage(contentLength, bodyLength))
7783
##
78-
## let server = newAsyncHttpServer(maxBody = 10485760) # 10 MB
84+
## let server = newAsyncHttpServer(maxBody = 10485760, stream = stream) # 10 MB
7985
## waitFor server.serve(Port(8080), cb)
8086

8187
import tables, asyncnet, asyncdispatch, parseutils, uri, strutils
@@ -93,9 +99,6 @@ const
9399
maxLine = 8*1024
94100

95101
when (NimMajor, NimMinor) >= (1, 1):
96-
const
97-
chunkSize = 8*1024 ## This seems perfectly reasonable for default chunkSize.
98-
99102
type
100103
Request* = object
101104
client*: AsyncSocket # TODO: Separate this into a Response object?
@@ -104,8 +107,16 @@ when (NimMajor, NimMinor) >= (1, 1):
104107
protocol*: tuple[orig: string, major, minor: int]
105108
url*: Uri
106109
hostname*: string ## The hostname of the client that made the request.
107-
body*: string # For future removal
108-
bodyStream*: FutureStream[string]
110+
body*: string
111+
contentLength*: int
112+
113+
type
114+
AsyncHttpServer* = ref object
115+
socket: AsyncSocket
116+
reuseAddr: bool
117+
reusePort: bool
118+
maxBody: int ## The maximum content-length that will be read for the body.
119+
stream: bool ## By default (stream = false), the body of the request is read immediately
109120
else:
110121
type
111122
Request* = object
@@ -117,20 +128,30 @@ else:
117128
hostname*: string ## The hostname of the client that made the request.
118129
body*: string
119130

120-
type
121-
AsyncHttpServer* = ref object
122-
socket: AsyncSocket
123-
reuseAddr: bool
124-
reusePort: bool
125-
maxBody: int ## The maximum content-length that will be read for the body.
131+
type
132+
AsyncHttpServer* = ref object
133+
socket: AsyncSocket
134+
reuseAddr: bool
135+
reusePort: bool
136+
maxBody: int ## The maximum content-length that will be read for the body.
126137

127-
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
138+
when (NimMajor, NimMinor) >= (1, 1):
139+
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
140+
maxBody = 8388608, stream = false): AsyncHttpServer =
141+
## Creates a new ``AsyncHttpServer`` instance.
142+
new result
143+
result.reuseAddr = reuseAddr
144+
result.reusePort = reusePort
145+
result.maxBody = maxBody
146+
result.stream = stream
147+
else:
148+
proc newAsyncHttpServer*(reuseAddr = true, reusePort = false,
128149
maxBody = 8388608): AsyncHttpServer =
129-
## Creates a new ``AsyncHttpServer`` instance.
130-
new result
131-
result.reuseAddr = reuseAddr
132-
result.reusePort = reusePort
133-
result.maxBody = maxBody
150+
## Creates a new ``AsyncHttpServer`` instance.
151+
new result
152+
result.reuseAddr = reuseAddr
153+
result.reusePort = reusePort
154+
result.maxBody = maxBody
134155

135156
proc addHeaders(msg: var string, headers: HttpHeaders) =
136157
for k, v in headers:
@@ -193,13 +214,30 @@ proc parseProtocol(protocol: string): tuple[orig: string, major, minor: int] =
193214
proc sendStatus(client: AsyncSocket, status: string): Future[void] =
194215
client.send("HTTP/1.1 " & status & "\c\L\c\L")
195216

217+
when (NimMajor, NimMinor) >= (1, 1):
218+
iterator bodyStream*(
219+
request: Request,
220+
chunkSize: int = 8*1024): (int, Future[string]) =
221+
## The chunkSize parameter is optional and default value is 8*1024 bytes.
222+
##
223+
## This iterator return a tuple with the length of the data that was read
224+
## and a future.
225+
var remainder = request.contentLength
226+
while remainder > 0:
227+
let readSize = min(remainder, chunkSize)
228+
let data = request.client.recv(readSize)
229+
if data.failed:
230+
raise newException(ValueError, "Error reading POST data from client.")
231+
yield (readSize, data)
232+
remainder -= readSize
233+
196234
proc processRequest(
197235
server: AsyncHttpServer,
198236
req: FutureVar[Request],
199237
client: AsyncSocket,
200238
address: string,
201239
lineFut: FutureVar[string],
202-
callback: proc (request: Request): Future[void] {.closure, gcsafe.},
240+
callback: proc (request: Request): Future[void] {.closure, gcsafe.}
203241
): Future[bool] {.async.} =
204242

205243
# Alias `request` to `req.mget()` so we don't have to write `mget` everywhere.
@@ -213,13 +251,9 @@ proc processRequest(
213251
request.hostname.shallowCopy(address)
214252
assert client != nil
215253
request.client = client
254+
request.body = ""
216255
when (NimMajor, NimMinor) >= (1, 1):
217-
request.bodyStream = newFutureStream[string]()
218-
# To uncomment in the future after compatibility issues
219-
# with third parties are solved
220-
# else:
221-
# request.body = ""
222-
request.body = "" # Temporary fix for future removal
256+
request.contentLength = 0
223257

224258
# We should skip at least one empty line before the request
225259
# https://tools.ietf.org/html/rfc7230#section-3.5
@@ -316,16 +350,12 @@ proc processRequest(
316350
return false
317351

318352
when (NimMajor, NimMinor) >= (1, 1):
319-
var remainder = contentLength
320-
while remainder > 0:
321-
let readSize = min(remainder, chunkSize)
322-
let data = await client.recv(read_size)
323-
if data.len != read_size:
353+
request.contentLength = contentLength
354+
if not server.stream:
355+
request.body = await client.recv(contentLength)
356+
if request.body.len != contentLength:
324357
await request.respond(Http400, "Bad Request. Content-Length does not match actual.")
325358
return true
326-
await request.bodyStream.write(data)
327-
remainder -= data.len
328-
request.bodyStream.complete()
329359
else:
330360
request.body = await client.recv(contentLength)
331361
if request.body.len != contentLength:

0 commit comments

Comments
 (0)