Implement file streaming for httpclient's MultipartData (#12982)

* Add `uploadFile` to POST files by streaming them

* Use constant for \c\L

* Formatting

* Remove uploadFile

* Implement MultipartData file streaming

* Remove unnecessary var annotations

* Call string on TaintedStrings

Fixes #12789

* Move cl constant to httpcore

* Fix `request` inconsistencies

* Update documentaion

* Clean up

* Skip multipart formatting when there's 0 entries

* Remove extraneous `cl` from multipart formatting

* Update MultipartData `$` to match old behaviour

* Update comment

* Address comments
This commit is contained in:
Zed 2020-03-06 19:55:45 +01:00 • committed by GitHub
commit e056298ceb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 207 additions and 142 deletions

View file

@ -52,6 +52,19 @@
## ##
## echo client.postContent("http://validator.w3.org/check", multipart=data) ## echo client.postContent("http://validator.w3.org/check", multipart=data)
## ##
## To stream files from disk when performing the request, use ``addFiles``.
##
## **Note:** This will allocate a new ``Mimetypes`` database every time you call
## it, you can pass your own via the ``mimeDb`` parameter to avoid this.
##
## .. code-block:: Nim
## let mimes = newMimetypes()
## var client = newHttpClient()
## var data = newMultipartData()
## data.addFiles({"uploaded_file": "test.html"}, mimeDb = mimes)
##
## echo client.postContent("http://validator.w3.org/check", multipart=data)
##
## You can also make post requests with custom headers. ## You can also make post requests with custom headers.
## This example sets ``Content-Type`` to ``application/json`` ## This example sets ``Content-Type`` to ``application/json``
## and uses a json object for the body ## and uses a json object for the body
@ -177,7 +190,7 @@
include "system/inclrtl" include "system/inclrtl"
import net, strutils, uri, parseutils, base64, os, mimetypes, import net, strutils, uri, parseutils, base64, os, mimetypes, streams,
math, random, httpcore, times, tables, streams, std/monotimes math, random, httpcore, times, tables, streams, std/monotimes
import asyncnet, asyncdispatch, asyncfile import asyncnet, asyncdispatch, asyncfile
import nativesockets import nativesockets
@ -252,9 +265,18 @@ type
url*: Uri url*: Uri
auth*: string auth*: string
MultipartEntry = object
name, content: string
case isFile: bool
of true:
filename, contentType: string
fileSize: int64
isStream: bool
else: discard
MultipartEntries* = openArray[tuple[name, content: string]] MultipartEntries* = openArray[tuple[name, content: string]]
MultipartData* = ref object MultipartData* = ref object
content: seq[string] content: seq[MultipartEntry]
ProtocolError* = object of IOError ## exception that is raised when server ProtocolError* = object of IOError ## exception that is raised when server
## does not conform to the implemented ## does not conform to the implemented
@ -278,7 +300,6 @@ proc fileError(msg: string) =
e.msg = msg e.msg = msg
raise e raise e
when not defined(ssl): when not defined(ssl):
type SslContext = ref object type SslContext = ref object
var defaultSslContext {.threadvar.}: SslContext var defaultSslContext {.threadvar.}: SslContext
@ -297,24 +318,28 @@ proc newProxy*(url: string, auth = ""): Proxy =
proc newMultipartData*: MultipartData = proc newMultipartData*: MultipartData =
## Constructs a new ``MultipartData`` object. ## Constructs a new ``MultipartData`` object.
MultipartData(content: @[]) MultipartData()
proc `$`*(data: MultipartData): string {.since: (1, 1).} = proc `$`*(data: MultipartData): string {.since: (1, 1).} =
## convert MultipartData to string so it's human readable when echo ## convert MultipartData to string so it's human readable when echo
## see https://github.com/nim-lang/Nim/issues/11863 ## see https://github.com/nim-lang/Nim/issues/11863
const prefixLen = "Content-Disposition: form-data; ".len const sep = "-".repeat(30)
for pos, item in data.content: for pos, entry in data.content:
result &= "------------------------------ " result.add(sep & center($pos, 3) & sep)
result.addInt pos result.add("\nname=\"" & entry.name & "\"")
result &= " ------------------------------\n" if entry.isFile:
result &= item[prefixLen .. item.high] result.add("; filename=\"" & entry.filename & "\"\n")
result.add("Content-Type: " & entry.contentType)
proc add*(p: var MultipartData, name, content: string, filename: string = "", result.add("\n\n" & entry.content & "\n")
contentType: string = "") =
## Add a value to the multipart data. Raises a `ValueError` exception if
## `name`, `filename` or `contentType` contain newline characters.
proc add*(p: MultipartData, name, content: string, filename: string = "",
contentType: string = "", useStream = true) =
## Add a value to the multipart data.
##
## When ``useStream`` is ``false``, the file will be read into memory.
##
## Raises a ``ValueError`` exception if
## ``name``, ``filename`` or ``contentType`` contain newline characters.
if {'\c', '\L'} in name: if {'\c', '\L'} in name:
raise newException(ValueError, "name contains a newline character") raise newException(ValueError, "name contains a newline character")
if {'\c', '\L'} in filename: if {'\c', '\L'} in filename:
@ -322,19 +347,22 @@ proc add*(p: var MultipartData, name, content: string, filename: string = "",
if {'\c', '\L'} in contentType: if {'\c', '\L'} in contentType:
raise newException(ValueError, "contentType contains a newline character") raise newException(ValueError, "contentType contains a newline character")
var str = "Content-Disposition: form-data; name=\"" & name & "\"" var entry = MultipartEntry(
if filename.len > 0: name: name,
str.add("; filename=\"" & filename & "\"") content: content,
str.add("\c\L") isFile: filename.len > 0
if contentType.len > 0: )
str.add("Content-Type: " & contentType & "\c\L")
str.add("\c\L" & content & "\c\L")
p.content.add(str) if entry.isFile:
entry.isStream = useStream
entry.filename = filename
entry.contentType = contentType
proc add*(p: var MultipartData, xs: MultipartEntries): MultipartData p.content.add(entry)
proc add*(p: MultipartData, xs: MultipartEntries): MultipartData
{.discardable.} = {.discardable.} =
## Add a list of multipart entries to the multipart data `p`. All values are ## Add a list of multipart entries to the multipart data ``p``. All values are
## added without a filename and without a content type. ## added without a filename and without a content type.
## ##
## .. code-block:: Nim ## .. code-block:: Nim
@ -344,70 +372,77 @@ proc add*(p: var MultipartData, xs: MultipartEntries): MultipartData
result = p result = p
proc newMultipartData*(xs: MultipartEntries): MultipartData = proc newMultipartData*(xs: MultipartEntries): MultipartData =
## Create a new multipart data object and fill it with the entries `xs` ## Create a new multipart data object and fill it with the entries ``xs``
## directly. ## directly.
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## var data = newMultipartData({"action": "login", "format": "json"}) ## var data = newMultipartData({"action": "login", "format": "json"})
result = MultipartData(content: @[]) result = MultipartData()
result.add(xs) for entry in xs:
result.add(entry.name, entry.content)
proc addFiles*(p: var MultipartData, xs: openArray[tuple[name, file: string]]): proc addFiles*(p: MultipartData, xs: openArray[tuple[name, file: string]],
mimeDb = newMimetypes(), useStream = true):
MultipartData {.discardable.} = MultipartData {.discardable.} =
## Add files to a multipart data object. The file will be opened from your ## Add files to a multipart data object. The files will be streamed from disk
## disk, read and sent with the automatically determined MIME type. Raises an ## when the request is being made. When ``stream`` is ``false``, the files are
## `IOError` if the file cannot be opened or reading fails. To manually ## instead read into memory, but beware this is very memory ineffecient even
## specify file content, filename and MIME type, use `[]=` instead. ## for small files. The MIME types will automatically be determined.
## Raises an ``IOError`` if the file cannot be opened or reading fails. To
## manually specify file content, filename and MIME type, use ``[]=`` instead.
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## data.addFiles({"uploaded_file": "public/test.html"}) ## data.addFiles({"uploaded_file": "public/test.html"})
var m = newMimetypes()
for name, file in xs.items: for name, file in xs.items:
var contentType: string var contentType: string
let (_, fName, ext) = splitFile(file) let (_, fName, ext) = splitFile(file)
if ext.len > 0: if ext.len > 0:
contentType = m.getMimetype(ext[1..ext.high], "") contentType = mimeDb.getMimetype(ext[1..ext.high], "")
p.add(name, readFile(file).string, fName & ext, contentType) let content = if useStream: file else: readFile(file).string
p.add(name, content, fName & ext, contentType, useStream = useStream)
result = p result = p
proc `[]=`*(p: var MultipartData, name, content: string) = proc `[]=`*(p: MultipartData, name, content: string) =
## Add a multipart entry to the multipart data `p`. The value is added ## Add a multipart entry to the multipart data ``p``. The value is added
## without a filename and without a content type. ## without a filename and without a content type.
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## data["username"] = "NimUser" ## data["username"] = "NimUser"
p.add(name, content) p.add(name, content)
proc `[]=`*(p: var MultipartData, name: string, proc `[]=`*(p: MultipartData, name: string,
file: tuple[name, contentType, content: string]) = file: tuple[name, contentType, content: string]) =
## Add a file to the multipart data `p`, specifying filename, contentType and ## Add a file to the multipart data ``p``, specifying filename, contentType
## content manually. ## and content manually.
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## data["uploaded_file"] = ("test.html", "text/html", ## data["uploaded_file"] = ("test.html", "text/html",
## "<html><head></head><body><p>test</p></body></html>") ## "<html><head></head><body><p>test</p></body></html>")
p.add(name, file.content, file.name, file.contentType) p.add(name, file.content, file.name, file.contentType, useStream = false)
proc format(p: MultipartData): tuple[contentType, body: string] = proc getBoundary(p: MultipartData): string =
if p == nil or p.content.len == 0: if p == nil or p.content.len == 0: return
return ("", "")
# Create boundary that is not in the data to be formatted
var bound: string
while true: while true:
bound = $random(int.high) result = $random(int.high)
var found = false for i, entry in p.content:
for s in p.content: if result in entry.content: break
if bound in s: elif i == p.content.high: return
found = true
if not found:
break
result.contentType = "multipart/form-data; boundary=" & bound proc sendFile(socket: Socket | AsyncSocket,
result.body = "" entry: MultipartEntry) {.multisync.} =
for s in p.content: const chunkSize = 2^18
result.body.add("--" & bound & "\c\L" & s) let file =
result.body.add("--" & bound & "--\c\L") when socket is AsyncSocket: openAsync(entry.content)
else: newFileStream(entry.content, fmRead)
var buffer: string
while true:
buffer =
when socket is AsyncSocket: (await read(file, chunkSize)).string
else: readStr(file, chunkSize).string
if buffer.len == 0: break
await socket.send(buffer)
file.close()
proc redirection(status: string): bool = proc redirection(status: string): bool =
const redirectionNRs = ["301", "302", "303", "307"] const redirectionNRs = ["301", "302", "303", "307"]
@ -427,8 +462,8 @@ proc getNewLocation(lastURL: string, headers: HttpHeaders): string =
parsed.anchor = r.anchor parsed.anchor = r.anchor
result = $parsed result = $parsed
proc generateHeaders(requestUrl: Uri, httpMethod: string, proc generateHeaders(requestUrl: Uri, httpMethod: string, headers: HttpHeaders,
headers: HttpHeaders, body: string, proxy: Proxy): string = proxy: Proxy): string =
# GET # GET
let upperMethod = httpMethod.toUpperAscii() let upperMethod = httpMethod.toUpperAscii()
result = upperMethod result = upperMethod
@ -447,34 +482,28 @@ proc generateHeaders(requestUrl: Uri, httpMethod: string,
result.add($modifiedUrl) result.add($modifiedUrl)
# HTTP/1.1\c\l # HTTP/1.1\c\l
result.add(" HTTP/1.1\c\L") result.add(" HTTP/1.1" & httpNewLine)
# Host header. # Host header.
if not headers.hasKey("Host"): if not headers.hasKey("Host"):
if requestUrl.port == "": if requestUrl.port == "":
add(result, "Host: " & requestUrl.hostname & "\c\L") add(result, "Host: " & requestUrl.hostname & httpNewLine)
else: else:
add(result, "Host: " & requestUrl.hostname & ":" & requestUrl.port & "\c\L") add(result, "Host: " & requestUrl.hostname & ":" & requestUrl.port & httpNewLine)
# Connection header. # Connection header.
if not headers.hasKey("Connection"): if not headers.hasKey("Connection"):
add(result, "Connection: Keep-Alive\c\L") add(result, "Connection: Keep-Alive" & httpNewLine)
# Content length header.
const requiresBody = ["POST", "PUT", "PATCH"]
let needsContentLength = body.len > 0 or upperMethod in requiresBody
if needsContentLength and not headers.hasKey("Content-Length"):
add(result, "Content-Length: " & $body.len & "\c\L")
# Proxy auth header. # Proxy auth header.
if not proxy.isNil and proxy.auth != "": if not proxy.isNil and proxy.auth != "":
let auth = base64.encode(proxy.auth) let auth = base64.encode(proxy.auth)
add(result, "Proxy-Authorization: basic " & auth & "\c\L") add(result, "Proxy-Authorization: basic " & auth & httpNewLine)
for key, val in headers: for key, val in headers:
add(result, key & ": " & val & "\c\L") add(result, key & ": " & val & httpNewLine)
add(result, "\c\L") add(result, httpNewLine)
type type
ProgressChangedProc*[ReturnType] = ProgressChangedProc*[ReturnType] =
@ -511,8 +540,8 @@ type
type type
HttpClient* = HttpClientBase[Socket] HttpClient* = HttpClientBase[Socket]
proc newHttpClient*(userAgent = defUserAgent, proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
maxRedirects = 5, sslContext = getDefaultSSL(), proxy: Proxy = nil, sslContext = getDefaultSSL(), proxy: Proxy = nil,
timeout = -1, headers = newHttpHeaders()): HttpClient = timeout = -1, headers = newHttpHeaders()): HttpClient =
## Creates a new HttpClient instance. ## Creates a new HttpClient instance.
## ##
@ -546,9 +575,9 @@ proc newHttpClient*(userAgent = defUserAgent,
type type
AsyncHttpClient* = HttpClientBase[AsyncSocket] AsyncHttpClient* = HttpClientBase[AsyncSocket]
proc newAsyncHttpClient*(userAgent = defUserAgent, proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
maxRedirects = 5, sslContext = getDefaultSSL(), sslContext = getDefaultSSL(), proxy: Proxy = nil,
proxy: Proxy = nil, headers = newHttpHeaders()): AsyncHttpClient = headers = newHttpHeaders()): AsyncHttpClient =
## Creates a new AsyncHttpClient instance. ## Creates a new AsyncHttpClient instance.
## ##
## ``userAgent`` specifies the user agent that will be used when making ## ``userAgent`` specifies the user agent that will be used when making
@ -671,8 +700,7 @@ proc parseChunks(client: HttpClient | AsyncHttpClient): Future[void]
# Trailer headers will only be sent if the request specifies that we want # Trailer headers will only be sent if the request specifies that we want
# them: http://tools.ietf.org/html/rfc2616#section-3.6.1 # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
proc parseBody(client: HttpClient | AsyncHttpClient, proc parseBody(client: HttpClient | AsyncHttpClient, headers: HttpHeaders,
headers: HttpHeaders,
httpVersion: string): Future[void] {.multisync.} = httpVersion: string): Future[void] {.multisync.} =
# Reset progress from previous requests. # Reset progress from previous requests.
client.contentTotal = 0 client.contentTotal = 0
@ -745,7 +773,7 @@ proc parseResponse(client: HttpClient | AsyncHttpClient,
# We've been disconnected. # We've been disconnected.
client.close() client.close()
break break
if line == "\c\L": if line == httpNewLine:
fullyRead = true fullyRead = true
break break
if not parsedStatus: if not parsedStatus:
@ -846,7 +874,7 @@ proc newConnection(client: HttpClient | AsyncHttpClient,
connectUrl.port = if url.port != "": url.port else: "443" connectUrl.port = if url.port != "": url.port else: "443"
let proxyHeaderString = generateHeaders(connectUrl, $HttpConnect, let proxyHeaderString = generateHeaders(connectUrl, $HttpConnect,
newHttpHeaders(), "", client.proxy) newHttpHeaders(), client.proxy)
await client.socket.send(proxyHeaderString) await client.socket.send(proxyHeaderString)
let proxyResp = await parseResponse(client, false) let proxyResp = await parseResponse(client, false)
@ -864,6 +892,45 @@ proc newConnection(client: HttpClient | AsyncHttpClient,
client.currentURL = url client.currentURL = url
client.connected = true client.connected = true
proc readFileSizes(client: HttpClient | AsyncHttpClient,
multipart: MultipartData) {.multisync.} =
for entry in multipart.content.mitems():
if not entry.isFile: continue
if not entry.isStream:
entry.fileSize = entry.content.len
continue
# TODO: look into making getFileSize work with async
let fileSize = getFileSize(entry.content)
entry.fileSize = fileSize
proc format(entry: MultipartEntry, boundary: string): string =
result = "--" & boundary & httpNewLine
result.add("Content-Disposition: form-data; name=\"" & entry.name & "\"")
if entry.isFile:
result.add("; filename=\"" & entry.filename & "\"" & httpNewLine)
result.add("Content-Type: " & entry.contentType & httpNewLine)
else:
result.add(httpNewLine & httpNewLine & entry.content)
proc format(client: HttpClient | AsyncHttpClient,
multipart: MultipartData): Future[seq[string]] {.multisync.} =
let bound = getBoundary(multipart)
client.headers["Content-Type"] = "multipart/form-data; boundary=" & bound
await client.readFileSizes(multipart)
var length: int64
for entry in multipart.content:
result.add(format(entry, bound) & httpNewLine)
if entry.isFile:
length += entry.fileSize + httpNewLine.len
result.add "--" & bound & "--"
for s in result: length += s.len
client.headers["Content-Length"] = $length
proc override(fallback, override: HttpHeaders): HttpHeaders = proc override(fallback, override: HttpHeaders): HttpHeaders =
# Right-biased map union for `HttpHeaders` # Right-biased map union for `HttpHeaders`
if override.isNil: if override.isNil:
@ -875,9 +942,9 @@ proc override(fallback, override: HttpHeaders): HttpHeaders =
for k, vs in override.table: for k, vs in override.table:
result[k] = vs result[k] = vs
proc requestAux(client: HttpClient | AsyncHttpClient, url: string, proc requestAux(client: HttpClient | AsyncHttpClient, url, httpMethod: string,
httpMethod: string, body = "", body = "", headers: HttpHeaders = nil,
headers: HttpHeaders = nil): Future[Response | AsyncResponse] multipart: MultipartData = nil): Future[Response | AsyncResponse]
{.multisync.} = {.multisync.} =
# Helper that actually makes the request. Does not handle redirects. # Helper that actually makes the request. Does not handle redirects.
let requestUrl = parseUri(url) let requestUrl = parseUri(url)
@ -885,6 +952,12 @@ proc requestAux(client: HttpClient | AsyncHttpClient, url: string,
if requestUrl.scheme == "": if requestUrl.scheme == "":
raise newException(ValueError, "No uri scheme supplied.") raise newException(ValueError, "No uri scheme supplied.")
var data: seq[string]
if multipart != nil and multipart.content.len > 0:
data = await client.format(multipart)
else:
client.headers["Content-Length"] = $body.len
when client is AsyncHttpClient: when client is AsyncHttpClient:
if not client.parseBodyFut.isNil: if not client.parseBodyFut.isNil:
# let the current operation finish before making another request # let the current operation finish before making another request
@ -893,16 +966,30 @@ proc requestAux(client: HttpClient | AsyncHttpClient, url: string,
await newConnection(client, requestUrl) await newConnection(client, requestUrl)
let effectiveHeaders = client.headers.override(headers) let newHeaders = client.headers.override(headers)
if not newHeaders.hasKey("user-agent") and client.userAgent.len > 0:
newHeaders["User-Agent"] = client.userAgent
if not effectiveHeaders.hasKey("user-agent") and client.userAgent != "": let headerString = generateHeaders(requestUrl, httpMethod, newHeaders,
effectiveHeaders["User-Agent"] = client.userAgent client.proxy)
await client.socket.send(headerString)
var headersString = generateHeaders(requestUrl, httpMethod, if data.len > 0:
effectiveHeaders, body, client.proxy) var buffer: string
for i, entry in multipart.content:
await client.socket.send(headersString) buffer.add data[i]
if body != "": if not entry.isFile: continue
if buffer.len > 0:
await client.socket.send(buffer)
buffer.setLen(0)
if entry.isStream:
await client.socket.sendFile(entry)
else:
await client.socket.send(entry.content)
buffer.add httpNewLine
# send the rest and the last boundary
await client.socket.send(buffer & data[^1])
elif body.len > 0:
await client.socket.send(body) await client.socket.send(body)
let getBody = httpMethod.toLowerAscii() notin ["head", "connect"] and let getBody = httpMethod.toLowerAscii() notin ["head", "connect"] and
@ -910,8 +997,8 @@ proc requestAux(client: HttpClient | AsyncHttpClient, url: string,
result = await parseResponse(client, getBody) result = await parseResponse(client, getBody)
proc request*(client: HttpClient | AsyncHttpClient, url: string, proc request*(client: HttpClient | AsyncHttpClient, url: string,
httpMethod: string, body = "", httpMethod: string, body = "", headers: HttpHeaders = nil,
headers: HttpHeaders = nil): Future[Response | AsyncResponse] multipart: MultipartData = nil): Future[Response | AsyncResponse]
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and performs a request ## Connects to the hostname specified by the URL and performs a request
## using the custom method string specified by ``httpMethod``. ## using the custom method string specified by ``httpMethod``.
@ -922,7 +1009,7 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string,
## ##
## This procedure will follow redirects up to a maximum number of redirects ## This procedure will follow redirects up to a maximum number of redirects
## specified in ``client.maxRedirects``. ## specified in ``client.maxRedirects``.
result = await client.requestAux(url, httpMethod, body, headers) result = await client.requestAux(url, httpMethod, body, headers, multipart)
var lastURL = url var lastURL = url
for i in 1..client.maxRedirects: for i in 1..client.maxRedirects:
@ -930,13 +1017,12 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string,
let redirectTo = getNewLocation(lastURL, result.headers) let redirectTo = getNewLocation(lastURL, result.headers)
# Guarantee method for HTTP 307: see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307 # Guarantee method for HTTP 307: see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
var meth = if result.status == "307": httpMethod else: "GET" var meth = if result.status == "307": httpMethod else: "GET"
result = await client.requestAux(redirectTo, meth, body, headers) result = await client.requestAux(redirectTo, meth, body, headers, multipart)
lastURL = redirectTo lastURL = redirectTo
proc request*(client: HttpClient | AsyncHttpClient, url: string, proc request*(client: HttpClient | AsyncHttpClient, url: string,
httpMethod = HttpGet, body = "", httpMethod = HttpGet, body = "", headers: HttpHeaders = nil,
headers: HttpHeaders = nil): Future[Response | AsyncResponse] multipart: MultipartData = nil): Future[Response | AsyncResponse]
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and performs a request ## Connects to the hostname specified by the URL and performs a request
## using the method specified. ## using the method specified.
@ -947,7 +1033,7 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string,
## ##
## When a request is made to a different hostname, the current connection will ## When a request is made to a different hostname, the current connection will
## be closed. ## be closed.
result = await request(client, url, $httpMethod, body, headers) result = await request(client, url, $httpMethod, body, headers, multipart)
proc responseContent(resp: Response | AsyncResponse): Future[string] {.multisync.} = proc responseContent(resp: Response | AsyncResponse): Future[string] {.multisync.} =
## Returns the content of a response as a string. ## Returns the content of a response as a string.
@ -991,31 +1077,14 @@ proc deleteContent*(client: HttpClient | AsyncHttpClient,
let resp = await delete(client, url) let resp = await delete(client, url)
return await responseContent(resp) return await responseContent(resp)
proc makeRequestContent(body = "", multipart: MultipartData = nil): (string, HttpHeaders) =
let (mpContentType, mpBody) = format(multipart)
# TODO: Support FutureStream for `body` parameter.
template withNewLine(x): untyped =
if x.len > 0 and not x.endsWith("\c\L"):
x & "\c\L"
else:
x
var xb = mpBody.withNewLine() & body
var headers = newHttpHeaders()
if multipart != nil:
headers["Content-Type"] = mpContentType
headers["Content-Length"] = $len(xb)
return (xb, headers)
proc post*(client: HttpClient | AsyncHttpClient, url: string, body = "", proc post*(client: HttpClient | AsyncHttpClient, url: string, body = "",
multipart: MultipartData = nil): Future[Response | AsyncResponse] multipart: MultipartData = nil): Future[Response | AsyncResponse]
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and performs a POST request. ## Connects to the hostname specified by the URL and performs a POST request.
## This procedure uses httpClient values such as ``client.maxRedirects``. ## This procedure uses httpClient values such as ``client.maxRedirects``.
var (xb, headers) = makeRequestContent(body, multipart) result = await client.request(url, $HttpPost, body, multipart=multipart)
result = await client.request(url, $HttpPost, xb, headers)
proc postContent*(client: HttpClient | AsyncHttpClient, url: string, proc postContent*(client: HttpClient | AsyncHttpClient, url: string, body = "",
body = "",
multipart: MultipartData = nil): Future[string] multipart: MultipartData = nil): Future[string]
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and returns the content of a POST request. ## Connects to the hostname specified by the URL and returns the content of a POST request.
@ -1027,13 +1096,10 @@ proc put*(client: HttpClient | AsyncHttpClient, url: string, body = "",
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and performs a PUT request. ## Connects to the hostname specified by the URL and performs a PUT request.
## This procedure uses httpClient values such as ``client.maxRedirects``. ## This procedure uses httpClient values such as ``client.maxRedirects``.
var (xb, headers) = makeRequestContent(body, multipart) result = await client.request(url, $HttpPut, body, multipart=multipart)
result = await client.request(url, $HttpPut, xb, headers)
proc putContent*(client: HttpClient | AsyncHttpClient, url: string, proc putContent*(client: HttpClient | AsyncHttpClient, url: string, body = "",
body = "", multipart: MultipartData = nil): Future[string] {.multisync.} =
multipart: MultipartData = nil): Future[string]
{.multisync.} =
## Connects to the hostname specified by the URL andreturns the content of a PUT request. ## Connects to the hostname specified by the URL andreturns the content of a PUT request.
let resp = await put(client, url, body, multipart) let resp = await put(client, url, body, multipart)
return await responseContent(resp) return await responseContent(resp)
@ -1043,11 +1109,9 @@ proc patch*(client: HttpClient | AsyncHttpClient, url: string, body = "",
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and performs a PATCH request. ## Connects to the hostname specified by the URL and performs a PATCH request.
## This procedure uses httpClient values such as ``client.maxRedirects``. ## This procedure uses httpClient values such as ``client.maxRedirects``.
var (xb, headers) = makeRequestContent(body, multipart) result = await client.request(url, $HttpPatch, body, multipart=multipart)
result = await client.request(url, $HttpPatch, xb, headers)
proc patchContent*(client: HttpClient | AsyncHttpClient, url: string, proc patchContent*(client: HttpClient | AsyncHttpClient, url: string, body = "",
body = "",
multipart: MultipartData = nil): Future[string] multipart: MultipartData = nil): Future[string]
{.multisync.} = {.multisync.} =
## Connects to the hostname specified by the URL and returns the content of a PATCH request. ## Connects to the hostname specified by the URL and returns the content of a PATCH request.

View file

@ -97,6 +97,7 @@ const
Http504* = HttpCode(504) Http504* = HttpCode(504)
Http505* = HttpCode(505) Http505* = HttpCode(505)
const httpNewLine* = "\c\L"
const headerLimit* = 10_000 const headerLimit* = 10_000
proc newHttpHeaders*(): HttpHeaders = proc newHttpHeaders*(): HttpHeaders =