diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4395085..477beb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-latest, macos-latest] nim-version: ['1.2.2', '1.2.x', '1.4.x', 'stable'] include: - nim-version: '1.4.x' @@ -30,5 +30,6 @@ jobs: - run: nimble test -y --gc:refc - run: nimble test -y --gc:arc - run: nimble test -y --gc:orc + - run: nimble test -d:release -y --gc:orc - run: nimble test -y --gc:arc --threads:on diff --git a/README.md b/README.md index 02010a4..df8e516 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,33 @@ echo response.headers echo response.body.len ``` +## Examples + +Using multipart/form-data: + +```nim +var entries: seq[MultipartEntry] +entries.add MultipartEntry( + name: "input_text", + fileName: "input.txt", + contentType: "text/plain", + payload: "foobar" +) +entries.add MultipartEntry( + name: "options", + payload: "{\"utf8\":true}" +) + +let (contentType, body) = encodeMultipart(entries) + +var headers: HttpHeaders +headers["Content-Type"] = contentType + +let response = post("Your API endpoint here", headers, body) +``` + +See the [examples/](https://github.com/treeform/puppy) folder for more examples. + ## Always use Libcurl You can pass `-d:puppyLibcurl` to force use of `libcurl` even on windows and macOS. This is useful to debug, if the some reason native OS API does not work. Libcurl is usually installed on macOS but requires a `curl.dll` on windows. diff --git a/examples/multipart.nim b/examples/multipart.nim new file mode 100644 index 0000000..67483bd --- /dev/null +++ b/examples/multipart.nim @@ -0,0 +1,22 @@ +import puppy + +var entries: seq[MultipartEntry] +entries.add MultipartEntry( + name: "input_text", + fileName: "input.txt", + contentType: "text/plain", + payload: "foobar" +) +entries.add MultipartEntry( + name: "options", + payload: "{\"utf8\":true}" +) + +let (contentType, body) = encodeMultipart(entries) + +var headers: HttpHeaders +headers["Content-Type"] = contentType + +let response = post("http://localhost:8080", headers, body) + +echo response.code diff --git a/puppy.nimble b/puppy.nimble index a40ef79..5f2a0b1 100644 --- a/puppy.nimble +++ b/puppy.nimble @@ -1,4 +1,4 @@ -version = "2.0.2" +version = "2.0.3" author = "Andre von Houck" description = "Puppy fetches resources via HTTP and HTTPS." license = "MIT" @@ -8,4 +8,4 @@ srcDir = "src" requires "nim >= 1.2.2" requires "libcurl >= 1.0.0" requires "zippy >= 0.10.0" -requires "webby >= 0.1.3" +requires "webby >= 0.1.6" diff --git a/src/puppy/platforms/linux/platform.nim b/src/puppy/platforms/linux/platform.nim index c7dbe5a..ea16abc 100644 --- a/src/puppy/platforms/linux/platform.nim +++ b/src/puppy/platforms/linux/platform.nim @@ -42,77 +42,77 @@ proc fetch*(req: Request): Response {.raises: [PuppyError].} = strings.add k & ": " & v let curl = easy_init() - defer: - curl.easy_cleanup() + try: + discard curl.easy_setopt(OPT_URL, strings[0].cstring) + discard curl.easy_setopt(OPT_CUSTOMREQUEST, strings[1].cstring) + discard curl.easy_setopt(OPT_TIMEOUT, req.timeout.int) + + # Create the Pslist for passing headers to curl manually. This is to + # avoid needing to call slist_free_all which creates problems + var slists: seq[Slist] + for i, header in req.headers: + slists.add Slist(data: strings[2 + i].cstring, next: nil) + # Do this in two passes so the slists index addresses are stable + var headerList: Pslist + for i, header in req.headers: + if i == 0: + headerList = slists[0].addr + else: + var tail = headerList + while tail.next != nil: + tail = tail.next + tail.next = slists[i].addr + + discard curl.easy_setopt(OPT_HTTPHEADER, headerList) + + if req.verb.toUpperAscii() == "POST" or req.body.len > 0: + discard curl.easy_setopt(OPT_POSTFIELDSIZE, req.body.len) + discard curl.easy_setopt(OPT_POSTFIELDS, req.body.cstring) + + # Setup writers. + var headerWrap, bodyWrap: StringWrap + discard curl.easy_setopt(OPT_WRITEDATA, bodyWrap.addr) + discard curl.easy_setopt(OPT_WRITEFUNCTION, curlWriteFn) + discard curl.easy_setopt(OPT_HEADERDATA, headerWrap.addr) + discard curl.easy_setopt(OPT_HEADERFUNCTION, curlWriteFn) + + # On Windows look for cacert.pem. + when defined(windows): + discard curl.easy_setopt(OPT_CAINFO, "cacert.pem".cstring) + + # Follow up to 10 redirects by default. + discard curl.easy_setopt(OPT_FOLLOWLOCATION, 1) + discard curl.easy_setopt(OPT_MAXREDIRS, 10) + + if req.allowAnyHttpsCertificate: + discard curl.easy_setopt(OPT_SSL_VERIFYPEER, 0) + discard curl.easy_setopt(OPT_SSL_VERIFYHOST, 0) - discard curl.easy_setopt(OPT_URL, strings[0].cstring) - discard curl.easy_setopt(OPT_CUSTOMREQUEST, strings[1].cstring) - discard curl.easy_setopt(OPT_TIMEOUT, req.timeout.int) - - # Create the Pslist for passing headers to curl manually. This is to - # avoid needing to call slist_free_all which creates problems - var slists: seq[Slist] - for i, header in req.headers: - slists.add Slist(data: strings[2 + i].cstring, next: nil) - # Do this in two passes so the slists index addresses are stable - var headerList: Pslist - for i, header in req.headers: - if i == 0: - headerList = slists[0].addr + let + ret = curl.easy_perform() + headerData = headerWrap.str + + if ret == E_OK: + var httpCode: uint32 + discard curl.easy_getinfo(INFO_RESPONSE_CODE, httpCode.addr) + result.code = httpCode.int + + var responseUrl: cstring + discard curl.easy_getinfo(INFO_EFFECTIVE_URL, responseUrl.addr) + result.url = $responseUrl + + for headerLine in headerData.split(CRLF): + let arr = headerLine.split(":", 1) + if arr.len == 2: + result.headers.add((arr[0].strip(), arr[1].strip())) + + result.body = bodyWrap.str + if result.headers["Content-Encoding"] == "gzip": + try: + result.body = uncompress(result.body, dfGzip) + except ZippyError as e: + raise newException(PuppyError, "Error uncompressing response", e) else: - var tail = headerList - while tail.next != nil: - tail = tail.next - tail.next = slists[i].addr - - discard curl.easy_setopt(OPT_HTTPHEADER, headerList) - - if req.verb.toUpperAscii() == "POST" or req.body.len > 0: - discard curl.easy_setopt(OPT_POSTFIELDSIZE, req.body.len) - discard curl.easy_setopt(OPT_POSTFIELDS, req.body.cstring) - - # Setup writers. - var headerWrap, bodyWrap: StringWrap - discard curl.easy_setopt(OPT_WRITEDATA, bodyWrap.addr) - discard curl.easy_setopt(OPT_WRITEFUNCTION, curlWriteFn) - discard curl.easy_setopt(OPT_HEADERDATA, headerWrap.addr) - discard curl.easy_setopt(OPT_HEADERFUNCTION, curlWriteFn) - - # On Windows look for cacert.pem. - when defined(windows): - discard curl.easy_setopt(OPT_CAINFO, "cacert.pem".cstring) - - # Follow up to 10 redirects by default. - discard curl.easy_setopt(OPT_FOLLOWLOCATION, 1) - discard curl.easy_setopt(OPT_MAXREDIRS, 10) - - if req.allowAnyHttpsCertificate: - discard curl.easy_setopt(OPT_SSL_VERIFYPEER, 0) - discard curl.easy_setopt(OPT_SSL_VERIFYHOST, 0) - - let - ret = curl.easy_perform() - headerData = headerWrap.str - - if ret == E_OK: - var httpCode: uint32 - discard curl.easy_getinfo(INFO_RESPONSE_CODE, httpCode.addr) - result.code = httpCode.int - - var responseUrl: cstring - discard curl.easy_getinfo(INFO_EFFECTIVE_URL, responseUrl.addr) - result.url = $responseUrl - - for headerLine in headerData.split(CRLF): - let arr = headerLine.split(":", 1) - if arr.len == 2: - result.headers.add((arr[0].strip(), arr[1].strip())) - - result.body = bodyWrap.str - if result.headers["Content-Encoding"] == "gzip": - try: - result.body = uncompress(result.body, dfGzip) - except ZippyError as e: - raise newException(PuppyError, "Error uncompressing response", e) - else: - raise newException(PuppyError, $easy_strerror(ret)) + raise newException(PuppyError, $easy_strerror(ret)) + finally: + curl.easy_cleanup()