httpclient.nim 47 KB

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