Skip to content

Commit 5671183

Browse files
committed
Merge branch 'master' into dev/etan/ae-returnstrain
2 parents b0a4008 + 55adad1 commit 5671183

17 files changed

Lines changed: 601 additions & 301 deletions

chronos.nimble

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
mode = ScriptMode.Verbose
22

33
packageName = "chronos"
4-
version = "4.2.2"
4+
# keep in sync: chronos/apps/http/httpagent.nim
5+
version = "4.4.0"
56
author = "Status Research & Development GmbH"
67
description = "Networking framework with async/await support"
78
license = "MIT or Apache License 2.0"
@@ -31,7 +32,7 @@ let testArguments =
3132
[
3233
"-d:debug -d:chronosDebug -d:useSysAssert -d:useGcAssert",
3334
"-d:debug -d:chronosDebug -d:chronosEventEngine=poll -d:useSysAssert -d:useGcAssert",
34-
"-d:release",
35+
"-d:release -d:chronosPreviewV5",
3536
]
3637

3738
let cfg =

chronos/apps/http/httpagent.nim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import strutils
1414
const
1515
ChronosName* = "nim-chronos"
1616
## Project name string
17-
ChronosMajor* {.intdefine.}: int = 3
17+
ChronosMajor* {.intdefine.}: int = 4
1818
## Major number of Chronos' version.
19-
ChronosMinor* {.intdefine.}: int = 0
19+
ChronosMinor* {.intdefine.}: int = 4
2020
## Minor number of Chronos' version.
21-
ChronosPatch* {.intdefine.}: int = 2
21+
ChronosPatch* {.intdefine.}: int = 0
2222
## Patch number of Chronos' version.
2323
ChronosVersion* = $ChronosMajor & "." & $ChronosMinor & "." & $ChronosPatch
2424
## Version of Chronos as a string.

chronos/apps/http/httpclient.nim

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,10 @@ type
188188
reader*: HttpBodyReader
189189
error*: ref HttpError
190190
bodyFlag*: HttpClientBodyFlag
191-
contentEncoding*: set[ContentEncodingFlags]
192-
transferEncoding*: set[TransferEncodingFlags]
191+
contentEncoding*{.deprecated.}: set[ContentEncodingFlags]
192+
transferEncoding*{.deprecated.}: set[TransferEncodingFlags]
193+
contentEncodings*: seq[ContentEncodingFlags]
194+
transferEncodings*: seq[TransferEncodingFlags]
193195
contentLength*: uint64
194196
contentType*: Opt[ContentTypeData]
195197
timestamp*: Moment
@@ -268,8 +270,9 @@ template isIdle(conn: HttpClientConnectionRef, timestamp: Moment,
268270

269271
proc sessionWatcher(session: HttpSessionRef) {.async: (raises: []).}
270272
proc directProvider*(): HttpConnectionProvider
273+
271274
proc new*(t: typedesc[HttpSessionRef],
272-
flags: HttpClientFlags = {NewConnectionAlways},
275+
flags: HttpClientFlags = {},
273276
maxRedirections = HttpMaxRedirections,
274277
connectTimeout = HttpConnectTimeout,
275278
headersTimeout = HttpHeadersTimeout,
@@ -288,6 +291,31 @@ proc new*(t: typedesc[HttpSessionRef],
288291
## ``idleTimeout`` - timeout to consider HTTP connection as idle
289292
## ``idlePeriod`` - period of time to check HTTP connections for inactivity
290293
doAssert(maxRedirections >= 0, "maxRedirections should not be negative")
294+
295+
# TODO chronos v4.2 and earlier enabled persistent connections only
296+
# together with the pipelining flag (persistent connections are
297+
# necessary but not sufficient for pipelining).
298+
#
299+
# Since users of chronos (like presto and json-rpc) didn't include
300+
# pipelining in their defaults, this had the practical effect of disabling
301+
# persistent connections by default which is a good thing because the
302+
# current implementation of connection reuse in the http client is
303+
# practically unusable due to the lack of EOF monitoring (without EOF
304+
# monitoring, connections that get closed are not detected leading to
305+
# frequent request failures in most usage scenarios).
306+
#
307+
# Although connection reuse is useful even without pipelining, we'll
308+
# keep using the deprecated pipelining flag as a gatekeeper so that
309+
# practically, it remains disabled in most cases - this gatekeeper
310+
# will be removed once reuse is feature complete.
311+
{.push warning[Deprecated]: off.}
312+
let flags =
313+
if HttpClientFlag.Http11Pipeline notin flags:
314+
flags + {HttpClientFlag.NewConnectionAlways}
315+
else:
316+
flags
317+
{.pop.}
318+
291319
let res = HttpSessionRef(
292320
flags: flags,
293321
maxRedirections: maxRedirections,
@@ -357,17 +385,20 @@ proc getHttpAddress*(
357385
path: url.path, query: url.query, anchor: url.anchor,
358386
username: url.username, password: url.password))
359387

388+
{.push warning[Deprecated]: off.}
360389
proc getHttpAddress*(
361390
uri: Uri,
362391
flags: HttpClientFlags
363392
): HttpAddressResult {.deprecated: "No DNS resolution in getHttpAddress, no flags needed".} =
364393
getHttpAddress(uri)
394+
{.pop.}
365395

366396
proc getHttpAddress*(
367397
url: string,
368398
): HttpAddressResult =
369399
getHttpAddress(parseUri(url))
370400

401+
{.push warning[Deprecated]: off.}
371402
proc getHttpAddress*(
372403
url: string,
373404
flags: HttpClientFlags
@@ -387,6 +418,8 @@ proc getHttpAddress*(
387418
## Create new HTTP address using URL string ``url`` and .
388419
getHttpAddress(parseUri(url))
389420

421+
{.pop.}
422+
390423
proc getAddress*(session: HttpSessionRef, url: Uri): HttpResult[HttpAddress] {.deprecated: "use getHttpAddress".} =
391424
let res = getHttpAddress(url).valueOr:
392425
return err($error)
@@ -721,7 +754,6 @@ proc reuseOrConnect(
721754
connection.flags.incl(HttpClientConnectionFlag.KeepAlive)
722755
connection.state = HttpClientConnectionState.Acquired
723756
return connection
724-
725757
let connection =
726758
try:
727759
await session.connect(ha).wait(session.connectTimeout)
@@ -906,22 +938,27 @@ proc prepareResponse(
906938
res
907939

908940
# Preprocessing "Content-Encoding" header.
909-
let contentEncoding =
910-
block:
911-
let res = getContentEncoding(headers.getList(ContentEncodingHeader))
912-
if res.isErr():
913-
return err("Invalid headers received, invalid `Content-Encoding`")
941+
let
942+
contentEncodings = getContentEncodings(headers.getList(ContentEncodingHeader)).valueOr:
943+
return err("Invalid `Content-Encoding` in headers")
944+
contentEncoding = {
945+
if contentEncodings.len() > 0:
946+
contentEncodings[^1]
914947
else:
915-
res.get()
948+
ContentEncodingFlags.Identity
949+
}
916950

917951
# Preprocessing "Transfer-Encoding" header.
918-
let transferEncoding =
919-
block:
920-
let res = getTransferEncoding(headers.getList(TransferEncodingHeader))
921-
if res.isErr():
922-
return err("Invalid headers received, invalid `Transfer-Encoding`")
952+
let
953+
transferEncodings = getTransferEncodings(
954+
headers.getList(TransferEncodingHeader)).valueOr:
955+
return err("Invalid `Transfer-Encoding` in headers")
956+
transferEncoding = {
957+
if transferEncodings.len() > 0:
958+
transferEncodings[^1]
923959
else:
924-
res.get()
960+
TransferEncodingFlags.Identity
961+
}
925962

926963
# Preprocessing "Content-Length" header.
927964
let (contentLength, bodyFlag) =
@@ -934,9 +971,9 @@ proc prepareResponse(
934971
# header
935972
let length = headers.getInt(ContentLengthHeader)
936973
(length, HttpClientBodyFlag.NoBody)
937-
elif transferEncoding != {TransferEncodingFlags.Identity}:
974+
elif transferEncodings.len > 0:
938975
# "Transfer-Encoding overrides the Content-Length"
939-
if TransferEncodingFlags.Chunked in transferEncoding:
976+
if transferEncodings[^1] == TransferEncodingFlags.Chunked:
940977
(0'u64, HttpClientBodyFlag.Chunked)
941978
else:
942979
(0'u64, HttpClientBodyFlag.Custom)
@@ -986,6 +1023,7 @@ proc prepareResponse(
9861023
reason: resp.reason(data), version: resp.version, session: request.session,
9871024
connection: connection, headers: headers,
9881025
contentEncoding: contentEncoding, transferEncoding: transferEncoding,
1026+
contentEncodings: contentEncodings, transferEncodings: transferEncodings,
9891027
contentLength: contentLength, contentType: contentType, bodyFlag: bodyFlag
9901028
)
9911029

chronos/apps/http/httpcommon.nim

Lines changed: 95 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@ proc raiseHttpRequestBodyTooLargeError*() {.
150150
raise (ref HttpRequestBodyTooLargeError)(
151151
code: Http413, msg: MaximumBodySizeError)
152152

153+
{.push warning[Deprecated]: off.}
153154
proc raiseHttpCriticalError*(msg: string, code = Http400) {.
154155
noinline, noreturn, raises: [HttpCriticalError], deprecated.} =
155156
raise (ref HttpCriticalError)(code: code, msg: msg)
157+
{.pop.}
156158

157159
proc raiseHttpDisconnectError*() {.
158160
noinline, noreturn, raises: [HttpDisconnectError].} =
@@ -237,69 +239,106 @@ iterator queryParams*(query: string,
237239
else:
238240
yield (decodeUrl(k), decodeUrl(v))
239241

242+
func getTransferEncodings*(
243+
ch: openArray[string]
244+
): HttpResult[seq[TransferEncodingFlags]] =
245+
## Parse value of multiple Transfer-Encoding headers and return the list.
246+
##
247+
## `identity` is a no-op encoding that is accepted and ignored for backwards
248+
## compatibility.
249+
##
250+
## See also:
251+
## * https://www.rfc-editor.org/rfc/rfc9112#section-6.1
252+
## * https://www.rfc-editor.org/rfc/rfc9110#section-8.4
253+
## * https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#transfer-coding
254+
var
255+
res: seq[TransferEncodingFlags]
256+
chunked = 0
257+
258+
for header in ch:
259+
for item in header.split(","):
260+
case strip(item.toLowerAscii())
261+
of "identity": # deprecated in HTTP/1.1+; accepted for compat
262+
discard
263+
of "chunked":
264+
res.add(TransferEncodingFlags.Chunked)
265+
inc chunked
266+
of "compress", "x-compress":
267+
res.add(TransferEncodingFlags.Compress)
268+
of "deflate":
269+
res.add(TransferEncodingFlags.Deflate)
270+
of "gzip", "x-gzip":
271+
res.add(TransferEncodingFlags.Gzip)
272+
else:
273+
return err("Unsupported Transfer-Encoding value")
274+
275+
# Validate RFC 9112 Section 6.1 rules:
276+
# 1) chunked MUST NOT appear more than once
277+
# 2) if chunked is present, it MUST be the final (last) encoding
278+
if chunked > 1:
279+
return err("Transfer-Encoding contains duplicate chunked encoding")
280+
if chunked == 1 and res[^1] != TransferEncodingFlags.Chunked:
281+
return err("chunked transfer coding must be the final encoding")
282+
283+
ok(res)
284+
240285
func getTransferEncoding*(
241286
ch: openArray[string]
242-
): HttpResult[set[TransferEncodingFlags]] =
287+
): HttpResult[set[TransferEncodingFlags]] {.deprecated: "getTransferEncodings".} =
243288
## Parse value of multiple HTTP headers ``Transfer-Encoding`` and return
244-
## it as set of ``TransferEncodingFlags``.
245-
# TODO Transfer-Encoding is ordered, thus using a set here is wrong.
246-
# https://www.rfc-editor.org/info/rfc9112/#section-6.1
247-
# https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#transfer-coding
248-
var res: set[TransferEncodingFlags] = {}
249-
if len(ch) == 0:
250-
res.incl(TransferEncodingFlags.Identity)
251-
ok(res)
252-
else:
253-
for header in ch:
254-
for item in header.split(","):
255-
case strip(item.toLowerAscii())
256-
of "identity":
257-
res.incl(TransferEncodingFlags.Identity)
258-
of "chunked":
259-
res.incl(TransferEncodingFlags.Chunked)
260-
of "compress", "x-compress":
261-
res.incl(TransferEncodingFlags.Compress)
262-
of "deflate":
263-
res.incl(TransferEncodingFlags.Deflate)
264-
of "gzip", "x-gzip":
265-
res.incl(TransferEncodingFlags.Gzip)
266-
of "":
267-
res.incl(TransferEncodingFlags.Identity)
268-
else:
269-
return err("Unsupported Transfer-Encoding value")
270-
ok(res)
289+
## the last entry, ie the first encoding that must be decoded.
290+
let encodings = ?getTransferEncodings(ch)
291+
ok {
292+
if encodings.len() > 0:
293+
encodings[^1]
294+
else:
295+
TransferEncodingFlags.Identity
296+
}
297+
298+
func getContentEncodings*(
299+
ch: openArray[string]
300+
): HttpResult[seq[ContentEncodingFlags]] =
301+
## Parse value of multiple ``Content-Encoding`` headers and return the
302+
## list of ``ContentEncodingFlags``.
303+
##
304+
## If no encodings are given, returns an empty seq.
305+
##
306+
## `identity` is a no-op encoding that is accepted and ignored for backwards
307+
## compatibility.
308+
##
309+
## See also:
310+
## * https://www.rfc-editor.org/rfc/rfc9110#section-8.4
311+
## * https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
312+
var res: seq[ContentEncodingFlags]
313+
for header in ch:
314+
for item in header.split(","):
315+
case strip(item.toLowerAscii()):
316+
of "identity": # valid in Accept-Encoding; accepted here for completeness
317+
discard
318+
of "br":
319+
res.add(ContentEncodingFlags.Br)
320+
of "compress", "x-compress":
321+
res.add(ContentEncodingFlags.Compress)
322+
of "deflate":
323+
res.add(ContentEncodingFlags.Deflate)
324+
of "gzip", "x-gzip":
325+
res.add(ContentEncodingFlags.Gzip)
326+
else:
327+
return err("Unsupported Content-Encoding value")
328+
ok(res)
271329

272330
func getContentEncoding*(
273331
ch: openArray[string]
274-
): HttpResult[set[ContentEncodingFlags]] =
332+
): HttpResult[set[ContentEncodingFlags]] {.deprecated: "getContentEncodings".} =
275333
## Parse value of multiple HTTP headers ``Content-Encoding`` and return
276-
## it as set of ``ContentEncodingFlags``.
277-
# TODO Content-Encoding is ordered, thus using a set here is wrong.
278-
# https://www.rfc-editor.org/info/rfc9110/#section-8.4
279-
# https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
280-
var res: set[ContentEncodingFlags] = {}
281-
if len(ch) == 0:
282-
res.incl(ContentEncodingFlags.Identity)
283-
ok(res)
284-
else:
285-
for header in ch:
286-
for item in header.split(","):
287-
case strip(item.toLowerAscii()):
288-
of "identity":
289-
res.incl(ContentEncodingFlags.Identity)
290-
of "br":
291-
res.incl(ContentEncodingFlags.Br)
292-
of "compress", "x-compress":
293-
res.incl(ContentEncodingFlags.Compress)
294-
of "deflate":
295-
res.incl(ContentEncodingFlags.Deflate)
296-
of "gzip", "x-gzip":
297-
res.incl(ContentEncodingFlags.Gzip)
298-
of "":
299-
res.incl(ContentEncodingFlags.Identity)
300-
else:
301-
return err("Unsupported Content-Encoding value")
302-
ok(res)
334+
## the last entry, ie the first encoding that must be decoded.
335+
let encodings = ?getContentEncodings(ch)
336+
ok {
337+
if encodings.len() > 0:
338+
encodings[^1]
339+
else:
340+
ContentEncodingFlags.Identity
341+
}
303342

304343
func isPersistent*(version: HttpVersion, headers: HttpTable): bool =
305344
if version >= HttpVersion20:

0 commit comments

Comments
 (0)