httpclient.nim 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2019 Nim Contributors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module implements a simple HTTP client that can be used to retrieve
  10. ## webpages and other data.
  11. ##
  12. ## Retrieving a website
  13. ## ====================
  14. ##
  15. ## This example uses HTTP GET to retrieve
  16. ## ``http://google.com``:
  17. ##
  18. ## .. code-block:: Nim
  19. ## import httpclient
  20. ## var client = newHttpClient()
  21. ## echo client.getContent("http://google.com")
  22. ##
  23. ## The same action can also be performed asynchronously, simply use the
  24. ## ``AsyncHttpClient``:
  25. ##
  26. ## .. code-block:: Nim
  27. ## import asyncdispatch, httpclient
  28. ##
  29. ## proc asyncProc(): Future[string] {.async.} =
  30. ## var client = newAsyncHttpClient()
  31. ## return await client.getContent("http://example.com")
  32. ##
  33. ## echo waitFor asyncProc()
  34. ##
  35. ## The functionality implemented by ``HttpClient`` and ``AsyncHttpClient``
  36. ## is the same, so you can use whichever one suits you best in the examples
  37. ## shown here.
  38. ##
  39. ## **Note:** You need to run asynchronous examples in an async proc
  40. ## otherwise you will get an ``Undeclared identifier: 'await'`` error.
  41. ##
  42. ## Using HTTP POST
  43. ## ===============
  44. ##
  45. ## This example demonstrates the usage of the W3 HTML Validator, it
  46. ## uses ``multipart/form-data`` as the ``Content-Type`` to send the HTML to be
  47. ## validated to the server.
  48. ##
  49. ## .. code-block:: Nim
  50. ## var client = newHttpClient()
  51. ## var data = newMultipartData()
  52. ## data["output"] = "soap12"
  53. ## data["uploaded_file"] = ("test.html", "text/html",
  54. ## "<html><head></head><body><p>test</p></body></html>")
  55. ##
  56. ## echo client.postContent("http://validator.w3.org/check", multipart=data)
  57. ##
  58. ## To stream files from disk when performing the request, use ``addFiles``.
  59. ##
  60. ## **Note:** This will allocate a new ``Mimetypes`` database every time you call
  61. ## it, you can pass your own via the ``mimeDb`` parameter to avoid this.
  62. ##
  63. ## .. code-block:: Nim
  64. ## let mimes = newMimetypes()
  65. ## var client = newHttpClient()
  66. ## var data = newMultipartData()
  67. ## data.addFiles({"uploaded_file": "test.html"}, mimeDb = mimes)
  68. ##
  69. ## echo client.postContent("http://validator.w3.org/check", multipart=data)
  70. ##
  71. ## You can also make post requests with custom headers.
  72. ## This example sets ``Content-Type`` to ``application/json``
  73. ## and uses a json object for the body
  74. ##
  75. ## .. code-block:: Nim
  76. ## import httpclient, json
  77. ##
  78. ## let client = newHttpClient()
  79. ## client.headers = newHttpHeaders({ "Content-Type": "application/json" })
  80. ## let body = %*{
  81. ## "data": "some text"
  82. ## }
  83. ## let response = client.request("http://some.api", httpMethod = HttpPost, body = $body)
  84. ## echo response.status
  85. ##
  86. ## Progress reporting
  87. ## ==================
  88. ##
  89. ## You may specify a callback procedure to be called during an HTTP request.
  90. ## This callback will be executed every second with information about the
  91. ## progress of the HTTP request.
  92. ##
  93. ## .. code-block:: Nim
  94. ## import asyncdispatch, httpclient
  95. ##
  96. ## proc onProgressChanged(total, progress, speed: BiggestInt) {.async.} =
  97. ## echo("Downloaded ", progress, " of ", total)
  98. ## echo("Current rate: ", speed div 1000, "kb/s")
  99. ##
  100. ## proc asyncProc() {.async.} =
  101. ## var client = newAsyncHttpClient()
  102. ## client.onProgressChanged = onProgressChanged
  103. ## discard await client.getContent("http://speedtest-ams2.digitalocean.com/100mb.test")
  104. ##
  105. ## waitFor asyncProc()
  106. ##
  107. ## If you would like to remove the callback simply set it to ``nil``.
  108. ##
  109. ## .. code-block:: Nim
  110. ## client.onProgressChanged = nil
  111. ##
  112. ## **Warning:** The ``total`` reported by httpclient may be 0 in some cases.
  113. ##
  114. ##
  115. ## SSL/TLS support
  116. ## ===============
  117. ## This requires the OpenSSL library, fortunately it's widely used and installed
  118. ## on many operating systems. httpclient will use SSL automatically if you give
  119. ## any of the functions a url with the ``https`` schema, for example:
  120. ## ``https://github.com/``.
  121. ##
  122. ## You will also have to compile with ``ssl`` defined like so:
  123. ## ``nim c -d:ssl ...``.
  124. ##
  125. ## Certificate validation is NOT performed by default.
  126. ## This will change in future.
  127. ##
  128. ## A set of directories and files from the `ssl_certs <ssl_certs.html>`_
  129. ## module are scanned to locate CA certificates.
  130. ##
  131. ## See `newContext <net.html#newContext>`_ to tweak or disable certificate validation.
  132. ##
  133. ## Timeouts
  134. ## ========
  135. ##
  136. ## Currently only the synchronous functions support a timeout.
  137. ## The timeout is
  138. ## measured in milliseconds, once it is set any call on a socket which may
  139. ## block will be susceptible to this timeout.
  140. ##
  141. ## It may be surprising but the
  142. ## function as a whole can take longer than the specified timeout, only
  143. ## individual internal calls on the socket are affected. In practice this means
  144. ## that as long as the server is sending data an exception will not be raised,
  145. ## if however data does not reach the client within the specified timeout a
  146. ## ``TimeoutError`` exception will be raised.
  147. ##
  148. ## Here is how to set a timeout when creating an ``HttpClient`` instance:
  149. ##
  150. ## .. code-block:: Nim
  151. ## import httpclient
  152. ##
  153. ## let client = newHttpClient(timeout = 42)
  154. ##
  155. ## Proxy
  156. ## =====
  157. ##
  158. ## A proxy can be specified as a param to any of the procedures defined in
  159. ## this module. To do this, use the ``newProxy`` constructor. Unfortunately,
  160. ## only basic authentication is supported at the moment.
  161. ##
  162. ## Some examples on how to configure a Proxy for ``HttpClient``:
  163. ##
  164. ## .. code-block:: Nim
  165. ## import httpclient
  166. ##
  167. ## let myProxy = newProxy("http://myproxy.network")
  168. ## let client = newHttpClient(proxy = myProxy)
  169. ##
  170. ## Get Proxy URL from environment variables:
  171. ##
  172. ## .. code-block:: Nim
  173. ## import httpclient
  174. ##
  175. ## var url = ""
  176. ## try:
  177. ## if existsEnv("http_proxy"):
  178. ## url = getEnv("http_proxy")
  179. ## elif existsEnv("https_proxy"):
  180. ## url = getEnv("https_proxy")
  181. ## except ValueError:
  182. ## echo "Unable to parse proxy from environment variables."
  183. ##
  184. ## let myProxy = newProxy(url = url)
  185. ## let client = newHttpClient(proxy = myProxy)
  186. ##
  187. ## Redirects
  188. ## =========
  189. ##
  190. ## The maximum redirects can be set with the ``maxRedirects`` of ``int`` type,
  191. ## it specifies the maximum amount of redirects to follow,
  192. ## it defaults to ``5``, you can set it to ``0`` to disable redirects.
  193. ##
  194. ## Here you can see an example about how to set the ``maxRedirects`` of ``HttpClient``:
  195. ##
  196. ## .. code-block:: Nim
  197. ## import httpclient
  198. ##
  199. ## let client = newHttpClient(maxRedirects = 0)
  200. ##
  201. import std/private/since
  202. import net, strutils, uri, parseutils, base64, os, mimetypes, streams,
  203. math, random, httpcore, times, tables, streams, std/monotimes
  204. import asyncnet, asyncdispatch, asyncfile
  205. import nativesockets
  206. export httpcore except parseHeader # TODO: The ``except`` doesn't work
  207. type
  208. Response* = ref object
  209. version*: string
  210. status*: string
  211. headers*: HttpHeaders
  212. body: string
  213. bodyStream*: Stream
  214. AsyncResponse* = ref object
  215. version*: string
  216. status*: string
  217. headers*: HttpHeaders
  218. body: string
  219. bodyStream*: FutureStream[string]
  220. proc code*(response: Response | AsyncResponse): HttpCode
  221. {.raises: [ValueError, OverflowDefect].} =
  222. ## Retrieves the specified response's ``HttpCode``.
  223. ##
  224. ## Raises a ``ValueError`` if the response's ``status`` does not have a
  225. ## corresponding ``HttpCode``.
  226. return response.status[0 .. 2].parseInt.HttpCode
  227. proc contentType*(response: Response | AsyncResponse): string {.inline.} =
  228. ## Retrieves the specified response's content type.
  229. ##
  230. ## This is effectively the value of the "Content-Type" header.
  231. response.headers.getOrDefault("content-type")
  232. proc contentLength*(response: Response | AsyncResponse): int =
  233. ## Retrieves the specified response's content length.
  234. ##
  235. ## This is effectively the value of the "Content-Length" header.
  236. ##
  237. ## A ``ValueError`` exception will be raised if the value is not an integer.
  238. var contentLengthHeader = response.headers.getOrDefault("Content-Length")
  239. result = contentLengthHeader.parseInt()
  240. doAssert(result >= 0 and result <= high(int32))
  241. proc lastModified*(response: Response | AsyncResponse): DateTime =
  242. ## Retrieves the specified response's last modified time.
  243. ##
  244. ## This is effectively the value of the "Last-Modified" header.
  245. ##
  246. ## Raises a ``ValueError`` if the parsing fails or the value is not a correctly
  247. ## formatted time.
  248. var lastModifiedHeader = response.headers.getOrDefault("last-modified")
  249. result = parse(lastModifiedHeader, "ddd, dd MMM yyyy HH:mm:ss 'GMT'", utc())
  250. proc body*(response: Response): string =
  251. ## Retrieves the specified response's body.
  252. ##
  253. ## The response's body stream is read synchronously.
  254. if response.body.len == 0:
  255. response.body = response.bodyStream.readAll()
  256. return response.body
  257. proc body*(response: AsyncResponse): Future[string] {.async.} =
  258. ## Reads the response's body and caches it. The read is performed only
  259. ## once.
  260. if response.body.len == 0:
  261. response.body = await readAll(response.bodyStream)
  262. return response.body
  263. type
  264. Proxy* = ref object
  265. url*: Uri
  266. auth*: string
  267. MultipartEntry = object
  268. name, content: string
  269. case isFile: bool
  270. of true:
  271. filename, contentType: string
  272. fileSize: int64
  273. isStream: bool
  274. else: discard
  275. MultipartEntries* = openArray[tuple[name, content: string]]
  276. MultipartData* = ref object
  277. content: seq[MultipartEntry]
  278. ProtocolError* = object of IOError ## exception that is raised when server
  279. ## does not conform to the implemented
  280. ## protocol
  281. HttpRequestError* = object of IOError ## Thrown in the ``getContent`` proc
  282. ## and ``postContent`` proc,
  283. ## when the server returns an error
  284. const defUserAgent* = "Nim httpclient/" & NimVersion
  285. proc httpError(msg: string) =
  286. var e: ref ProtocolError
  287. new(e)
  288. e.msg = msg
  289. raise e
  290. proc fileError(msg: string) =
  291. var e: ref IOError
  292. new(e)
  293. e.msg = msg
  294. raise e
  295. when not defined(ssl):
  296. type SslContext = ref object
  297. var defaultSslContext {.threadvar.}: SslContext
  298. proc getDefaultSSL(): SslContext =
  299. result = defaultSslContext
  300. when defined(ssl):
  301. if result == nil:
  302. defaultSslContext = newContext(verifyMode = CVerifyNone)
  303. result = defaultSslContext
  304. doAssert result != nil, "failure to initialize the SSL context"
  305. proc newProxy*(url: string, auth = ""): Proxy =
  306. ## Constructs a new ``TProxy`` object.
  307. result = Proxy(url: parseUri(url), auth: auth)
  308. proc newMultipartData*: MultipartData {.inline.} =
  309. ## Constructs a new ``MultipartData`` object.
  310. MultipartData()
  311. proc `$`*(data: MultipartData): string {.since: (1, 1).} =
  312. ## convert MultipartData to string so it's human readable when echo
  313. ## see https://github.com/nim-lang/Nim/issues/11863
  314. const sep = "-".repeat(30)
  315. for pos, entry in data.content:
  316. result.add(sep & center($pos, 3) & sep)
  317. result.add("\nname=\"" & entry.name & "\"")
  318. if entry.isFile:
  319. result.add("; filename=\"" & entry.filename & "\"\n")
  320. result.add("Content-Type: " & entry.contentType)
  321. result.add("\n\n" & entry.content & "\n")
  322. proc add*(p: MultipartData, name, content: string, filename: string = "",
  323. contentType: string = "", useStream = true) =
  324. ## Add a value to the multipart data.
  325. ##
  326. ## When ``useStream`` is ``false``, the file will be read into memory.
  327. ##
  328. ## Raises a ``ValueError`` exception if
  329. ## ``name``, ``filename`` or ``contentType`` contain newline characters.
  330. if {'\c', '\L'} in name:
  331. raise newException(ValueError, "name contains a newline character")
  332. if {'\c', '\L'} in filename:
  333. raise newException(ValueError, "filename contains a newline character")
  334. if {'\c', '\L'} in contentType:
  335. raise newException(ValueError, "contentType contains a newline character")
  336. var entry = MultipartEntry(
  337. name: name,
  338. content: content,
  339. isFile: filename.len > 0
  340. )
  341. if entry.isFile:
  342. entry.isStream = useStream
  343. entry.filename = filename
  344. entry.contentType = contentType
  345. p.content.add(entry)
  346. proc add*(p: MultipartData, xs: MultipartEntries): MultipartData
  347. {.discardable.} =
  348. ## Add a list of multipart entries to the multipart data ``p``. All values are
  349. ## added without a filename and without a content type.
  350. ##
  351. ## .. code-block:: Nim
  352. ## data.add({"action": "login", "format": "json"})
  353. for name, content in xs.items:
  354. p.add(name, content)
  355. result = p
  356. proc newMultipartData*(xs: MultipartEntries): MultipartData =
  357. ## Create a new multipart data object and fill it with the entries ``xs``
  358. ## directly.
  359. ##
  360. ## .. code-block:: Nim
  361. ## var data = newMultipartData({"action": "login", "format": "json"})
  362. result = MultipartData()
  363. for entry in xs:
  364. result.add(entry.name, entry.content)
  365. proc addFiles*(p: MultipartData, xs: openArray[tuple[name, file: string]],
  366. mimeDb = newMimetypes(), useStream = true):
  367. MultipartData {.discardable.} =
  368. ## Add files to a multipart data object. The files will be streamed from disk
  369. ## when the request is being made. When ``stream`` is ``false``, the files are
  370. ## instead read into memory, but beware this is very memory ineffecient even
  371. ## for small files. The MIME types will automatically be determined.
  372. ## Raises an ``IOError`` if the file cannot be opened or reading fails. To
  373. ## manually specify file content, filename and MIME type, use ``[]=`` instead.
  374. ##
  375. ## .. code-block:: Nim
  376. ## data.addFiles({"uploaded_file": "public/test.html"})
  377. for name, file in xs.items:
  378. var contentType: string
  379. let (_, fName, ext) = splitFile(file)
  380. if ext.len > 0:
  381. contentType = mimeDb.getMimetype(ext[1..ext.high], "")
  382. let content = if useStream: file else: readFile(file).string
  383. p.add(name, content, fName & ext, contentType, useStream = useStream)
  384. result = p
  385. proc `[]=`*(p: MultipartData, name, content: string) {.inline.} =
  386. ## Add a multipart entry to the multipart data ``p``. The value is added
  387. ## without a filename and without a content type.
  388. ##
  389. ## .. code-block:: Nim
  390. ## data["username"] = "NimUser"
  391. p.add(name, content)
  392. proc `[]=`*(p: MultipartData, name: string,
  393. file: tuple[name, contentType, content: string]) {.inline.} =
  394. ## Add a file to the multipart data ``p``, specifying filename, contentType
  395. ## and content manually.
  396. ##
  397. ## .. code-block:: Nim
  398. ## data["uploaded_file"] = ("test.html", "text/html",
  399. ## "<html><head></head><body><p>test</p></body></html>")
  400. p.add(name, file.content, file.name, file.contentType, useStream = false)
  401. proc getBoundary(p: MultipartData): string =
  402. if p == nil or p.content.len == 0: return
  403. while true:
  404. result = $rand(int.high)
  405. for i, entry in p.content:
  406. if result in entry.content: break
  407. elif i == p.content.high: return
  408. proc sendFile(socket: Socket | AsyncSocket,
  409. entry: MultipartEntry) {.multisync.} =
  410. const chunkSize = 2^18
  411. let file =
  412. when socket is AsyncSocket: openAsync(entry.content)
  413. else: newFileStream(entry.content, fmRead)
  414. var buffer: string
  415. while true:
  416. buffer =
  417. when socket is AsyncSocket: (await read(file, chunkSize)).string
  418. else: readStr(file, chunkSize).string
  419. if buffer.len == 0: break
  420. await socket.send(buffer)
  421. file.close()
  422. proc redirection(status: string): bool =
  423. const redirectionNRs = ["301", "302", "303", "307", "308"]
  424. for i in items(redirectionNRs):
  425. if status.startsWith(i):
  426. return true
  427. proc getNewLocation(lastURL: string, headers: HttpHeaders): string =
  428. result = headers.getOrDefault"Location"
  429. if result == "": httpError("location header expected")
  430. # Relative URLs. (Not part of the spec, but soon will be.)
  431. let r = parseUri(result)
  432. if r.hostname == "" and r.path != "":
  433. var parsed = parseUri(lastURL)
  434. parsed.path = r.path
  435. parsed.query = r.query
  436. parsed.anchor = r.anchor
  437. result = $parsed
  438. proc generateHeaders(requestUrl: Uri, httpMethod: string, headers: HttpHeaders,
  439. proxy: Proxy): string =
  440. # GET
  441. let upperMethod = httpMethod.toUpperAscii()
  442. result = upperMethod
  443. result.add ' '
  444. if proxy.isNil or requestUrl.scheme == "https":
  445. # /path?query
  446. if not requestUrl.path.startsWith("/"): result.add '/'
  447. result.add(requestUrl.path)
  448. if requestUrl.query.len > 0:
  449. result.add("?" & requestUrl.query)
  450. else:
  451. # Remove the 'http://' from the URL for CONNECT requests for TLS connections.
  452. var modifiedUrl = requestUrl
  453. if requestUrl.scheme == "https": modifiedUrl.scheme = ""
  454. result.add($modifiedUrl)
  455. # HTTP/1.1\c\l
  456. result.add(" HTTP/1.1" & httpNewLine)
  457. # Host header.
  458. if not headers.hasKey("Host"):
  459. if requestUrl.port == "":
  460. add(result, "Host: " & requestUrl.hostname & httpNewLine)
  461. else:
  462. add(result, "Host: " & requestUrl.hostname & ":" & requestUrl.port & httpNewLine)
  463. # Connection header.
  464. if not headers.hasKey("Connection"):
  465. add(result, "Connection: Keep-Alive" & httpNewLine)
  466. # Proxy auth header.
  467. if not proxy.isNil and proxy.auth != "":
  468. let auth = base64.encode(proxy.auth)
  469. add(result, "Proxy-Authorization: basic " & auth & httpNewLine)
  470. for key, val in headers:
  471. add(result, key & ": " & val & httpNewLine)
  472. add(result, httpNewLine)
  473. type
  474. ProgressChangedProc*[ReturnType] =
  475. proc (total, progress, speed: BiggestInt):
  476. ReturnType {.closure, gcsafe.}
  477. HttpClientBase*[SocketType] = ref object
  478. socket: SocketType
  479. connected: bool
  480. currentURL: Uri ## Where we are currently connected.
  481. headers*: HttpHeaders ## Headers to send in requests.
  482. maxRedirects: Natural ## Maximum redirects, set to ``0`` to disable.
  483. userAgent: string
  484. timeout*: int ## Only used for blocking HttpClient for now.
  485. proxy: Proxy
  486. ## ``nil`` or the callback to call when request progress changes.
  487. when SocketType is Socket:
  488. onProgressChanged*: ProgressChangedProc[void]
  489. else:
  490. onProgressChanged*: ProgressChangedProc[Future[void]]
  491. when defined(ssl):
  492. sslContext: net.SslContext
  493. contentTotal: BiggestInt
  494. contentProgress: BiggestInt
  495. oneSecondProgress: BiggestInt
  496. lastProgressReport: MonoTime
  497. when SocketType is AsyncSocket:
  498. bodyStream: FutureStream[string]
  499. parseBodyFut: Future[void]
  500. else:
  501. bodyStream: Stream
  502. getBody: bool ## When `false`, the body is never read in requestAux.
  503. type
  504. HttpClient* = HttpClientBase[Socket]
  505. proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
  506. sslContext = getDefaultSSL(), proxy: Proxy = nil,
  507. timeout = -1, headers = newHttpHeaders()): HttpClient =
  508. ## Creates a new HttpClient instance.
  509. ##
  510. ## ``userAgent`` specifies the user agent that will be used when making
  511. ## requests.
  512. ##
  513. ## ``maxRedirects`` specifies the maximum amount of redirects to follow,
  514. ## default is 5.
  515. ##
  516. ## ``sslContext`` specifies the SSL context to use for HTTPS requests.
  517. ## See `SSL/TLS support <##ssl-tls-support>`_
  518. ##
  519. ## ``proxy`` specifies an HTTP proxy to use for this HTTP client's
  520. ## connections.
  521. ##
  522. ## ``timeout`` specifies the number of milliseconds to allow before a
  523. ## ``TimeoutError`` is raised.
  524. ##
  525. ## ``headers`` specifies the HTTP Headers.
  526. runnableExamples:
  527. import asyncdispatch, httpclient, strutils
  528. proc asyncProc(): Future[string] {.async.} =
  529. var client = newAsyncHttpClient()
  530. return await client.getContent("http://example.com")
  531. let exampleHtml = waitFor asyncProc()
  532. assert "Example Domain" in exampleHtml
  533. assert not ("Pizza" in exampleHtml)
  534. new result
  535. result.headers = headers
  536. result.userAgent = userAgent
  537. result.maxRedirects = maxRedirects
  538. result.proxy = proxy
  539. result.timeout = timeout
  540. result.onProgressChanged = nil
  541. result.bodyStream = newStringStream()
  542. result.getBody = true
  543. when defined(ssl):
  544. result.sslContext = sslContext
  545. type
  546. AsyncHttpClient* = HttpClientBase[AsyncSocket]
  547. proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
  548. sslContext = getDefaultSSL(), proxy: Proxy = nil,
  549. headers = newHttpHeaders()): AsyncHttpClient =
  550. ## Creates a new AsyncHttpClient instance.
  551. ##
  552. ## ``userAgent`` specifies the user agent that will be used when making
  553. ## requests.
  554. ##
  555. ## ``maxRedirects`` specifies the maximum amount of redirects to follow,
  556. ## default is 5.
  557. ##
  558. ## ``sslContext`` specifies the SSL context to use for HTTPS requests.
  559. ##
  560. ## ``proxy`` specifies an HTTP proxy to use for this HTTP client's
  561. ## connections.
  562. ##
  563. ## ``headers`` specifies the HTTP Headers.
  564. new result
  565. result.headers = headers
  566. result.userAgent = userAgent
  567. result.maxRedirects = maxRedirects
  568. result.proxy = proxy
  569. result.timeout = -1 # TODO
  570. result.onProgressChanged = nil
  571. result.bodyStream = newFutureStream[string]("newAsyncHttpClient")
  572. result.getBody = true
  573. when defined(ssl):
  574. result.sslContext = sslContext
  575. proc close*(client: HttpClient | AsyncHttpClient) =
  576. ## Closes any connections held by the HTTP client.
  577. if client.connected:
  578. client.socket.close()
  579. client.connected = false
  580. proc getSocket*(client: HttpClient): Socket {.inline.} =
  581. ## Get network socket, useful if you want to find out more details about the connection
  582. ##
  583. ## this example shows info about local and remote endpoints
  584. ##
  585. ## .. code-block:: Nim
  586. ## if client.connected:
  587. ## echo client.getSocket.getLocalAddr
  588. ## echo client.getSocket.getPeerAddr
  589. ##
  590. return client.socket
  591. proc getSocket*(client: AsyncHttpClient): AsyncSocket {.inline.} =
  592. return client.socket
  593. proc reportProgress(client: HttpClient | AsyncHttpClient,
  594. progress: BiggestInt) {.multisync.} =
  595. client.contentProgress += progress
  596. client.oneSecondProgress += progress
  597. if (getMonoTime() - client.lastProgressReport).inSeconds > 1:
  598. if not client.onProgressChanged.isNil:
  599. await client.onProgressChanged(client.contentTotal,
  600. client.contentProgress,
  601. client.oneSecondProgress)
  602. client.oneSecondProgress = 0
  603. client.lastProgressReport = getMonoTime()
  604. proc recvFull(client: HttpClient | AsyncHttpClient, size: int, timeout: int,
  605. keep: bool): Future[int] {.multisync.} =
  606. ## Ensures that all the data requested is read and returned.
  607. var readLen = 0
  608. while true:
  609. if size == readLen: break
  610. let remainingSize = size - readLen
  611. let sizeToRecv = min(remainingSize, net.BufferSize)
  612. when client.socket is Socket:
  613. let data = client.socket.recv(sizeToRecv, timeout)
  614. else:
  615. let data = await client.socket.recv(sizeToRecv)
  616. if data == "":
  617. client.close()
  618. break # We've been disconnected.
  619. readLen.inc(data.len)
  620. if keep:
  621. await client.bodyStream.write(data)
  622. await reportProgress(client, data.len)
  623. return readLen
  624. proc parseChunks(client: HttpClient | AsyncHttpClient): Future[void]
  625. {.multisync.} =
  626. while true:
  627. var chunkSize = 0
  628. var chunkSizeStr = (await client.socket.recvLine()).string
  629. var i = 0
  630. if chunkSizeStr == "":
  631. httpError("Server terminated connection prematurely")
  632. while i < chunkSizeStr.len:
  633. case chunkSizeStr[i]
  634. of '0'..'9':
  635. chunkSize = chunkSize shl 4 or (ord(chunkSizeStr[i]) - ord('0'))
  636. of 'a'..'f':
  637. chunkSize = chunkSize shl 4 or (ord(chunkSizeStr[i]) - ord('a') + 10)
  638. of 'A'..'F':
  639. chunkSize = chunkSize shl 4 or (ord(chunkSizeStr[i]) - ord('A') + 10)
  640. of ';':
  641. # http://tools.ietf.org/html/rfc2616#section-3.6.1
  642. # We don't care about chunk-extensions.
  643. break
  644. else:
  645. httpError("Invalid chunk size: " & chunkSizeStr)
  646. inc(i)
  647. if chunkSize <= 0:
  648. discard await recvFull(client, 2, client.timeout, false) # Skip \c\L
  649. break
  650. var bytesRead = await recvFull(client, chunkSize, client.timeout, true)
  651. if bytesRead != chunkSize:
  652. httpError("Server terminated connection prematurely")
  653. bytesRead = await recvFull(client, 2, client.timeout, false) # Skip \c\L
  654. if bytesRead != 2:
  655. httpError("Server terminated connection prematurely")
  656. # Trailer headers will only be sent if the request specifies that we want
  657. # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
  658. proc parseBody(client: HttpClient | AsyncHttpClient, headers: HttpHeaders,
  659. httpVersion: string): Future[void] {.multisync.} =
  660. # Reset progress from previous requests.
  661. client.contentTotal = 0
  662. client.contentProgress = 0
  663. client.oneSecondProgress = 0
  664. client.lastProgressReport = MonoTime()
  665. when client is AsyncHttpClient:
  666. assert(not client.bodyStream.finished)
  667. if headers.getOrDefault"Transfer-Encoding" == "chunked":
  668. await parseChunks(client)
  669. else:
  670. # -REGION- Content-Length
  671. # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.3
  672. var contentLengthHeader = headers.getOrDefault"Content-Length"
  673. if contentLengthHeader != "":
  674. var length = contentLengthHeader.parseInt()
  675. client.contentTotal = length
  676. if length > 0:
  677. let recvLen = await client.recvFull(length, client.timeout, true)
  678. if recvLen == 0:
  679. client.close()
  680. httpError("Got disconnected while trying to read body.")
  681. if recvLen != length:
  682. httpError("Received length doesn't match expected length. Wanted " &
  683. $length & " got " & $recvLen)
  684. else:
  685. # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.4 TODO
  686. # -REGION- Connection: Close
  687. # (http://tools.ietf.org/html/rfc2616#section-4.4) NR.5
  688. let implicitConnectionClose =
  689. httpVersion == "1.0" or
  690. # This doesn't match the HTTP spec, but it fixes issues for non-conforming servers.
  691. (httpVersion == "1.1" and headers.getOrDefault"Connection" == "")
  692. if headers.getOrDefault"Connection" == "close" or implicitConnectionClose:
  693. while true:
  694. let recvLen = await client.recvFull(4000, client.timeout, true)
  695. if recvLen != 4000:
  696. client.close()
  697. break
  698. when client is AsyncHttpClient:
  699. client.bodyStream.complete()
  700. else:
  701. client.bodyStream.setPosition(0)
  702. # If the server will close our connection, then no matter the method of
  703. # reading the body, we need to close our socket.
  704. if headers.getOrDefault"Connection" == "close":
  705. client.close()
  706. proc parseResponse(client: HttpClient | AsyncHttpClient,
  707. getBody: bool): Future[Response | AsyncResponse]
  708. {.multisync.} =
  709. new result
  710. var parsedStatus = false
  711. var linei = 0
  712. var fullyRead = false
  713. var line = ""
  714. result.headers = newHttpHeaders()
  715. while true:
  716. linei = 0
  717. when client is HttpClient:
  718. line = (await client.socket.recvLine(client.timeout)).string
  719. else:
  720. line = (await client.socket.recvLine()).string
  721. if line == "":
  722. # We've been disconnected.
  723. client.close()
  724. break
  725. if line == httpNewLine:
  726. fullyRead = true
  727. break
  728. if not parsedStatus:
  729. # Parse HTTP version info and status code.
  730. var le = skipIgnoreCase(line, "HTTP/", linei)
  731. if le <= 0:
  732. httpError("invalid http version, `" & line & "`")
  733. inc(linei, le)
  734. le = skipIgnoreCase(line, "1.1", linei)
  735. if le > 0: result.version = "1.1"
  736. else:
  737. le = skipIgnoreCase(line, "1.0", linei)
  738. if le <= 0: httpError("unsupported http version")
  739. result.version = "1.0"
  740. inc(linei, le)
  741. # Status code
  742. linei.inc skipWhitespace(line, linei)
  743. result.status = line[linei .. ^1]
  744. parsedStatus = true
  745. else:
  746. # Parse headers
  747. var name = ""
  748. var le = parseUntil(line, name, ':', linei)
  749. if le <= 0: httpError("invalid headers")
  750. inc(linei, le)
  751. if line[linei] != ':': httpError("invalid headers")
  752. inc(linei) # Skip :
  753. result.headers.add(name, line[linei .. ^1].strip())
  754. if result.headers.len > headerLimit:
  755. httpError("too many headers")
  756. if not fullyRead:
  757. httpError("Connection was closed before full request has been made")
  758. if getBody and result.code != Http204:
  759. when client is HttpClient:
  760. client.bodyStream = newStringStream()
  761. result.bodyStream = client.bodyStream
  762. parseBody(client, result.headers, result.version)
  763. else:
  764. client.bodyStream = newFutureStream[string]("parseResponse")
  765. result.bodyStream = client.bodyStream
  766. assert(client.parseBodyFut.isNil or client.parseBodyFut.finished)
  767. client.parseBodyFut = parseBody(client, result.headers, result.version)
  768. # do not wait here for the body request to complete
  769. proc newConnection(client: HttpClient | AsyncHttpClient,
  770. url: Uri) {.multisync.} =
  771. if client.currentURL.hostname != url.hostname or
  772. client.currentURL.scheme != url.scheme or
  773. client.currentURL.port != url.port or
  774. (not client.connected):
  775. # Connect to proxy if specified
  776. let connectionUrl =
  777. if client.proxy.isNil: url else: client.proxy.url
  778. let isSsl = connectionUrl.scheme.toLowerAscii() == "https"
  779. if isSsl and not defined(ssl):
  780. raise newException(HttpRequestError,
  781. "SSL support is not available. Cannot connect over SSL. Compile with -d:ssl to enable.")
  782. if client.connected:
  783. client.close()
  784. client.connected = false
  785. # TODO: I should be able to write 'net.Port' here...
  786. let port =
  787. if connectionUrl.port == "":
  788. if isSsl:
  789. nativesockets.Port(443)
  790. else:
  791. nativesockets.Port(80)
  792. else: nativesockets.Port(connectionUrl.port.parseInt)
  793. when client is HttpClient:
  794. client.socket = await net.dial(connectionUrl.hostname, port)
  795. elif client is AsyncHttpClient:
  796. client.socket = await asyncnet.dial(connectionUrl.hostname, port)
  797. else: {.fatal: "Unsupported client type".}
  798. when defined(ssl):
  799. if isSsl:
  800. try:
  801. client.sslContext.wrapConnectedSocket(
  802. client.socket, handshakeAsClient, connectionUrl.hostname)
  803. except:
  804. client.socket.close()
  805. raise getCurrentException()
  806. # If need to CONNECT through proxy
  807. if url.scheme == "https" and not client.proxy.isNil:
  808. when defined(ssl):
  809. # Pass only host:port for CONNECT
  810. var connectUrl = initUri()
  811. connectUrl.hostname = url.hostname
  812. connectUrl.port = if url.port != "": url.port else: "443"
  813. let proxyHeaderString = generateHeaders(connectUrl, $HttpConnect,
  814. newHttpHeaders(), client.proxy)
  815. await client.socket.send(proxyHeaderString)
  816. let proxyResp = await parseResponse(client, false)
  817. if not proxyResp.status.startsWith("200"):
  818. raise newException(HttpRequestError,
  819. "The proxy server rejected a CONNECT request, " &
  820. "so a secure connection could not be established.")
  821. client.sslContext.wrapConnectedSocket(
  822. client.socket, handshakeAsClient, url.hostname)
  823. else:
  824. raise newException(HttpRequestError,
  825. "SSL support is not available. Cannot connect over SSL. Compile with -d:ssl to enable.")
  826. # May be connected through proxy but remember actual URL being accessed
  827. client.currentURL = url
  828. client.connected = true
  829. proc readFileSizes(client: HttpClient | AsyncHttpClient,
  830. multipart: MultipartData) {.multisync.} =
  831. for entry in multipart.content.mitems():
  832. if not entry.isFile: continue
  833. if not entry.isStream:
  834. entry.fileSize = entry.content.len
  835. continue
  836. # TODO: look into making getFileSize work with async
  837. let fileSize = getFileSize(entry.content)
  838. entry.fileSize = fileSize
  839. proc format(entry: MultipartEntry, boundary: string): string =
  840. result = "--" & boundary & httpNewLine
  841. result.add("Content-Disposition: form-data; name=\"" & entry.name & "\"")
  842. if entry.isFile:
  843. result.add("; filename=\"" & entry.filename & "\"" & httpNewLine)
  844. result.add("Content-Type: " & entry.contentType & httpNewLine)
  845. else:
  846. result.add(httpNewLine & httpNewLine & entry.content)
  847. proc format(client: HttpClient | AsyncHttpClient,
  848. multipart: MultipartData): Future[seq[string]] {.multisync.} =
  849. let bound = getBoundary(multipart)
  850. client.headers["Content-Type"] = "multipart/form-data; boundary=" & bound
  851. await client.readFileSizes(multipart)
  852. var length: int64
  853. for entry in multipart.content:
  854. result.add(format(entry, bound) & httpNewLine)
  855. if entry.isFile:
  856. length += entry.fileSize + httpNewLine.len
  857. result.add "--" & bound & "--"
  858. for s in result: length += s.len
  859. client.headers["Content-Length"] = $length
  860. proc override(fallback, override: HttpHeaders): HttpHeaders =
  861. # Right-biased map union for `HttpHeaders`
  862. if override.isNil:
  863. return fallback
  864. result = newHttpHeaders()
  865. # Copy by value
  866. result.table[] = fallback.table[]
  867. for k, vs in override.table:
  868. result[k] = vs
  869. proc requestAux(client: HttpClient | AsyncHttpClient, url, httpMethod: string,
  870. body = "", headers: HttpHeaders = nil,
  871. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  872. {.multisync.} =
  873. # Helper that actually makes the request. Does not handle redirects.
  874. let requestUrl = parseUri(url)
  875. if requestUrl.scheme == "":
  876. raise newException(ValueError, "No uri scheme supplied.")
  877. var data: seq[string]
  878. if multipart != nil and multipart.content.len > 0:
  879. data = await client.format(multipart)
  880. else:
  881. client.headers["Content-Length"] = $body.len
  882. when client is AsyncHttpClient:
  883. if not client.parseBodyFut.isNil:
  884. # let the current operation finish before making another request
  885. await client.parseBodyFut
  886. client.parseBodyFut = nil
  887. await newConnection(client, requestUrl)
  888. let newHeaders = client.headers.override(headers)
  889. if not newHeaders.hasKey("user-agent") and client.userAgent.len > 0:
  890. newHeaders["User-Agent"] = client.userAgent
  891. let headerString = generateHeaders(requestUrl, httpMethod, newHeaders,
  892. client.proxy)
  893. await client.socket.send(headerString)
  894. if data.len > 0:
  895. var buffer: string
  896. for i, entry in multipart.content:
  897. buffer.add data[i]
  898. if not entry.isFile: continue
  899. if buffer.len > 0:
  900. await client.socket.send(buffer)
  901. buffer.setLen(0)
  902. if entry.isStream:
  903. await client.socket.sendFile(entry)
  904. else:
  905. await client.socket.send(entry.content)
  906. buffer.add httpNewLine
  907. # send the rest and the last boundary
  908. await client.socket.send(buffer & data[^1])
  909. elif body.len > 0:
  910. await client.socket.send(body)
  911. let getBody = httpMethod.toLowerAscii() notin ["head", "connect"] and
  912. client.getBody
  913. result = await parseResponse(client, getBody)
  914. proc request*(client: HttpClient | AsyncHttpClient, url: string,
  915. httpMethod: string, body = "", headers: HttpHeaders = nil,
  916. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  917. {.multisync.} =
  918. ## Connects to the hostname specified by the URL and performs a request
  919. ## using the custom method string specified by ``httpMethod``.
  920. ##
  921. ## Connection will be kept alive. Further requests on the same ``client`` to
  922. ## the same hostname will not require a new connection to be made. The
  923. ## connection can be closed by using the ``close`` procedure.
  924. ##
  925. ## This procedure will follow redirects up to a maximum number of redirects
  926. ## specified in ``client.maxRedirects``.
  927. ##
  928. ## You need to make sure that the ``url`` doesn't contain any newline
  929. ## characters. Failing to do so will raise ``AssertionDefect``.
  930. doAssert(not url.contains({'\c', '\L'}), "url shouldn't contain any newline characters")
  931. result = await client.requestAux(url, httpMethod, body, headers, multipart)
  932. var lastURL = url
  933. for i in 1..client.maxRedirects:
  934. if result.status.redirection():
  935. let redirectTo = getNewLocation(lastURL, result.headers)
  936. # Guarantee method for HTTP 307: see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
  937. var meth = if result.status == "307": httpMethod else: "GET"
  938. result = await client.requestAux(redirectTo, meth, body, headers, multipart)
  939. lastURL = redirectTo
  940. proc request*(client: HttpClient | AsyncHttpClient, url: string,
  941. httpMethod = HttpGet, body = "", headers: HttpHeaders = nil,
  942. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  943. {.multisync.} =
  944. ## Connects to the hostname specified by the URL and performs a request
  945. ## using the method specified.
  946. ##
  947. ## Connection will be kept alive. Further requests on the same ``client`` to
  948. ## the same hostname will not require a new connection to be made. The
  949. ## connection can be closed by using the ``close`` procedure.
  950. ##
  951. ## When a request is made to a different hostname, the current connection will
  952. ## be closed.
  953. result = await request(client, url, $httpMethod, body, headers, multipart)
  954. proc responseContent(resp: Response | AsyncResponse): Future[string] {.multisync.} =
  955. ## Returns the content of a response as a string.
  956. ##
  957. ## A ``HttpRequestError`` will be raised if the server responds with a
  958. ## client error (status code 4xx) or a server error (status code 5xx).
  959. if resp.code.is4xx or resp.code.is5xx:
  960. raise newException(HttpRequestError, resp.status)
  961. else:
  962. return await resp.bodyStream.readAll()
  963. proc head*(client: HttpClient | AsyncHttpClient,
  964. url: string): Future[Response | AsyncResponse] {.multisync.} =
  965. ## Connects to the hostname specified by the URL and performs a HEAD request.
  966. ##
  967. ## This procedure uses httpClient values such as ``client.maxRedirects``.
  968. result = await client.request(url, HttpHead)
  969. proc get*(client: HttpClient | AsyncHttpClient,
  970. url: string): Future[Response | AsyncResponse] {.multisync.} =
  971. ## Connects to the hostname specified by the URL and performs a GET request.
  972. ##
  973. ## This procedure uses httpClient values such as ``client.maxRedirects``.
  974. result = await client.request(url, HttpGet)
  975. proc getContent*(client: HttpClient | AsyncHttpClient,
  976. url: string): Future[string] {.multisync.} =
  977. ## Connects to the hostname specified by the URL and returns the content of a GET request.
  978. let resp = await get(client, url)
  979. return await responseContent(resp)
  980. proc delete*(client: HttpClient | AsyncHttpClient,
  981. url: string): Future[Response | AsyncResponse] {.multisync.} =
  982. ## Connects to the hostname specified by the URL and performs a DELETE request.
  983. ## This procedure uses httpClient values such as ``client.maxRedirects``.
  984. result = await client.request(url, HttpDelete)
  985. proc deleteContent*(client: HttpClient | AsyncHttpClient,
  986. url: string): Future[string] {.multisync.} =
  987. ## Connects to the hostname specified by the URL and returns the content of a DELETE request.
  988. let resp = await delete(client, url)
  989. return await responseContent(resp)
  990. proc post*(client: HttpClient | AsyncHttpClient, url: string, body = "",
  991. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  992. {.multisync.} =
  993. ## Connects to the hostname specified by the URL and performs a POST request.
  994. ## This procedure uses httpClient values such as ``client.maxRedirects``.
  995. result = await client.request(url, $HttpPost, body, multipart=multipart)
  996. proc postContent*(client: HttpClient | AsyncHttpClient, url: string, body = "",
  997. multipart: MultipartData = nil): Future[string]
  998. {.multisync.} =
  999. ## Connects to the hostname specified by the URL and returns the content of a POST request.
  1000. let resp = await post(client, url, body, multipart)
  1001. return await responseContent(resp)
  1002. proc put*(client: HttpClient | AsyncHttpClient, url: string, body = "",
  1003. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  1004. {.multisync.} =
  1005. ## Connects to the hostname specified by the URL and performs a PUT request.
  1006. ## This procedure uses httpClient values such as ``client.maxRedirects``.
  1007. result = await client.request(url, $HttpPut, body, multipart=multipart)
  1008. proc putContent*(client: HttpClient | AsyncHttpClient, url: string, body = "",
  1009. multipart: MultipartData = nil): Future[string] {.multisync.} =
  1010. ## Connects to the hostname specified by the URL andreturns the content of a PUT request.
  1011. let resp = await put(client, url, body, multipart)
  1012. return await responseContent(resp)
  1013. proc patch*(client: HttpClient | AsyncHttpClient, url: string, body = "",
  1014. multipart: MultipartData = nil): Future[Response | AsyncResponse]
  1015. {.multisync.} =
  1016. ## Connects to the hostname specified by the URL and performs a PATCH request.
  1017. ## This procedure uses httpClient values such as ``client.maxRedirects``.
  1018. result = await client.request(url, $HttpPatch, body, multipart=multipart)
  1019. proc patchContent*(client: HttpClient | AsyncHttpClient, url: string, body = "",
  1020. multipart: MultipartData = nil): Future[string]
  1021. {.multisync.} =
  1022. ## Connects to the hostname specified by the URL and returns the content of a PATCH request.
  1023. let resp = await patch(client, url, body, multipart)
  1024. return await responseContent(resp)
  1025. proc downloadFile*(client: HttpClient, url: string, filename: string) =
  1026. ## Downloads ``url`` and saves it to ``filename``.
  1027. client.getBody = false
  1028. defer:
  1029. client.getBody = true
  1030. let resp = client.get(url)
  1031. client.bodyStream = newFileStream(filename, fmWrite)
  1032. if client.bodyStream.isNil:
  1033. fileError("Unable to open file")
  1034. parseBody(client, resp.headers, resp.version)
  1035. client.bodyStream.close()
  1036. if resp.code.is4xx or resp.code.is5xx:
  1037. raise newException(HttpRequestError, resp.status)
  1038. proc downloadFile*(client: AsyncHttpClient, url: string,
  1039. filename: string): Future[void] =
  1040. proc downloadFileEx(client: AsyncHttpClient,
  1041. url, filename: string): Future[void] {.async.} =
  1042. ## Downloads ``url`` and saves it to ``filename``.
  1043. client.getBody = false
  1044. let resp = await client.get(url)
  1045. client.bodyStream = newFutureStream[string]("downloadFile")
  1046. var file = openAsync(filename, fmWrite)
  1047. # Let `parseBody` write response data into client.bodyStream in the
  1048. # background.
  1049. asyncCheck parseBody(client, resp.headers, resp.version)
  1050. # The `writeFromStream` proc will complete once all the data in the
  1051. # `bodyStream` has been written to the file.
  1052. await file.writeFromStream(client.bodyStream)
  1053. file.close()
  1054. if resp.code.is4xx or resp.code.is5xx:
  1055. raise newException(HttpRequestError, resp.status)
  1056. result = newFuture[void]("downloadFile")
  1057. try:
  1058. result = downloadFileEx(client, url, filename)
  1059. except Exception as exc:
  1060. result.fail(exc)
  1061. finally:
  1062. result.addCallback(
  1063. proc () = client.getBody = true
  1064. )