httpclient.nim 46 KB

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