Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve multipart features for httpPost and httpUpload #479

Closed
abelsromero opened this issue Oct 22, 2018 · 8 comments · Fixed by #559
Closed

Improve multipart features for httpPost and httpUpload #479

abelsromero opened this issue Oct 22, 2018 · 8 comments · Fixed by #559
Assignees
Labels
💎 feature New feature
Milestone

Comments

@abelsromero
Copy link

Feature Request

With the current API, some scenarios of multipart interaction are not friendly and require what imho are hacks.

Description

I am working with CMS API and there's a API call with requires uploading a document alongside metadata in JSON.
This is easily done in curl with:

curl "https://api.hostname/v1/documents" \
-X POST \
-H 'Content-Type: multipart/form-data' \
-H "Authorization: Bearer $token" \
-H "Repository-Id: $repo_id" \
-F metadata="{
    \"description\": \"Test document\",
    \"categories\": [{...}]
	};type=application/json" \
-F file=@file_path

I only got this to work with the following code

    val (request, response, result) = url.httpUpload(
//            parameters = listOf("metadata" to """{"description":"test document from Kotlin"};type=application/json""")
    )
            .dataParts { request, url ->
                listOf(
                        DataPart(File("src/main/resources/rust-logo-blk.svg"), "file"),
                        DataPart(File("src/main/resources/doc.json"), "metadata","application/json")
                )
            }
            .header("Authorization" to "Bearer $token")
            .header("Repository-Id" to repoId)
            .responseString()

As you can see this requires storing the JSON in a file. That's because I could not find any way to set the Content-Type for the part other than using DataPart. Using parameters as seen in the commented line sets it as text/plain causing the service to return an error.

I also tried something like

    val (request, response, result) = url.httpPost(listOf(
            "metadata" to """{"description":"test document from Kotlin"}""",
            "file" to DataPart(File("resources/rust-logo-blk.svg"))))
            .header("Content-Type" to "multipart/form-data")
            .header("Authorization" to "Bearer $token")
            .header("Repo-Id" to repoId)
            .responseString()

Proposed Solution

Imho, the naming upload is confusing for this cases but is very useful for simple 1 file uploads. I'd advocate to:

  1. Create a httpMultipart similar to httpPost that offers the ability to pass any kind of object, not only files and set the Content-Type. Or allow htttPost to set Content-Type to multipart so that it manages the format internally (a la curl).
  2. Extend DataPart to support any data (String, Inputstream, ...)

Probably, with only point 2 would be enough to support a cleaner method with httpUpload.

Alternatives I've considered

Current solution does not allow to generate metadata Json on-the fly, which is not valid for a dynamic system.
Also, the dataParts closure element seems cumbersome to me and does not help readability. I prefer simpler/cleaner httpPost syntax. But this is a more personal thing.

Additional context

I noticed that when no Content-Type is set in the DataPart, the header is added empty, maybe this could be just removed.

@SleeplessByte
Copy link
Collaborator

SleeplessByte commented Oct 22, 2018

Hi @abelsromero, thank you for your feature request.

Linking these issues: #147 #162 #169 #259 #348 #399.

I completely agree with this. I think upload should stay and take one or more files, but as you say: take more than just files and allows to set metadata. The upload API should be amended anyway to allow setting filenames, or multiple files in one part.

@abelsromero
Copy link
Author

I am not sure I explained myself well.

I am not talking about uploading more than one file or setting the names, this more about being able to reuse DataPart for"inline" elements to avoid creating temporal files.

@SleeplessByte
Copy link
Collaborator

@abelsromero, currentlý I am reworking the upload/download to use generic streams (even before you posted the issue).

That should resolve your issue.

@abelsromero
Copy link
Author

Cool! Thanks a lot, do not hesitate to mention me if you need a beta tester.

SleeplessByte added a commit that referenced this issue Oct 26, 2018
This switches to use a UploadBody data class that determines its length at most once, and writes directly to the output stream. This is actually prework for #479 #399 and #390 #344.
SleeplessByte added a commit that referenced this issue Oct 28, 2018
Should work for #344
Should work for #479
@SleeplessByte SleeplessByte removed the 🙌 help wanted Looking for new assignee label Oct 29, 2018
@SleeplessByte
Copy link
Collaborator

Hi @abelsromero , this is coming! I have the basics down in #486, but after we merge #485 I am breaking it up into multiple smaller PRs, one specifically to solve your issue. It is, now that I know how to do this, a very trivial issue to solve.

@abelsromero
Copy link
Author

abelsromero commented Oct 30, 2018

Can't promise, but I'll have a look at it asap this week.

SleeplessByte added a commit that referenced this issue Oct 30, 2018
* Fix download to memory ( #414)

* Fix logging a consumed body

Just ensures the request can STILL be logged, even after the body is consumed (written to an output).

* Fix coroutines and remove redundant charset parameter

* Fix empty issues

When there was not last item, this call would fill. LastOrNull fixes that but this should only return on null items.

* Add generic readers and writers for everything that is a body.

* Add a few more headers

* Add KDoc for Body interface

* Add typealias for progress

* Add test for DefaultBody

* Add empty test

* Add Content-Encoding and Transfer-Encoding tests

* Add helper for mocking reflection

* Add HttpClientTest

* Add test to confirm #464 is possible

* Add test for issue (#473)

* Add test for issue (#408)

* Add test for issue (#364)

* Add Body for Response

This actually makes sure response bodies are treated like request bodies and not read multiple times. This also makes the streams completely lazy (as they are wrapped in an anonymous function).

* Add a warning about non-utf8 charset for bodies

* Add type aliases

* Add lazy length for bodies

* Add special "lazy body" and don't call input streams twice

This switches to use a UploadBody data class that determines its length at most once, and writes directly to the output stream. This is actually prework for #479 #399 and #390 #344.

* Add use caches option to client and request

By default, HTTPUrlConnection supports caching, but this was always turned off. This enables it by default, but allows for a "browser refresh" by passing in an argument.

* Add TE/Transfer-Encoding to HttpClient

* Add explanatory comments to HttpClient

* Add request flag to stop decoding content

This can be used if for example you want to download some content as gzipped and store it directly.

* Change (simplify) non-null invokes

* Change classes to data classes for easy copy

* Change condition to have common path first

* Change body to be an interface

This allows us to actually define different type of bodies, as well as extending a body to support #464. The length is now also a lazy call, so that it may be calculated ONLY if necessary.

* Change default name of files to "file"

The empty filename with _1 looks disturbing.

* Change automatic determination of a file's length to only match the encoded length, if it's going to be written with UTF-8.

* Change response method types (cleanup)

* Change argument naming

* Change input stream to be buffered

* Change to return instantly if empty

* Change default bodies to be consumable at most once

This solves multiple issues with input streams being read twice, when logging is turned on. Before, toByteArray would load into memory and that was that. Now it reads directly from an inputstream. However, most streams are only consumable once, thus this solves that.

When the stream is read into memory we might as well keep it around.

* Change upload body to be writable at most once

See the commentary for the DefaultBody

* Change the client for the new body api

* 📖 Change and format README with new API

* Remove redundant braces

* Remove bash dollar sign

The bash dollar sign only indicates that a string is a command and not a result. It should not be part of the actual string returned, as the string returned can not be forwarded to a system call.

* Remove debugging println

* Remove debug logging

* Remove TODO

The logingResponseLogger logged both request and response. Now it correctly only logs the response, and expects you to log the request using a request interceptor. This also makes the logs not show up next to each other, but when they are executed, allowing us to debug async requests.

* Remove default ACCEPT_ENCODING

Accept-Encoding is to accept content with a certain Content-Encoding, for example when request a file, you may ask for it gziped. However, it is not intended to be used as the encoding of the stream, also known as Transfer-Encoding.

* Remove deprecated fields
@SleeplessByte
Copy link
Collaborator

Just to be sure! It's not merged in yet, nor released 😅 but got it working with an experimental change

@abelsromero
Copy link
Author

don't worry, that was clear 👍 I'll check the branches

@SleeplessByte SleeplessByte added this to the Fuel 2.0 milestone Nov 5, 2018
SleeplessByte added a commit that referenced this issue Dec 4, 2018
Based on RFC7578: https://tools.ietf.org/html/rfc7578

This is a breaking change, but mostly deprecates current functions as error.

Removes all the extranous methods and bookkeeping from UploadRequest and UploadBody, and delegates those to the DataPart. This also allows for InlineDataPart that yield a file on the server and removes automatic naming for dataparts (_1, _2), because the correct way to put multiple files under the same name is to give them... the same name!

Fixes #344
Fixes #390
Fixes #399
Fixes #479
Fixes #513
SleeplessByte added a commit that referenced this issue Dec 12, 2018
* Overhaul upload requests, Blob and DataPart

Based on RFC7578: https://tools.ietf.org/html/rfc7578

This is a breaking change, but mostly deprecates current functions as error.

Removes all the extranous methods and bookkeeping from UploadRequest and UploadBody, and delegates those to the DataPart. This also allows for InlineDataPart that yield a file on the server and removes automatic naming for dataparts (_1, _2), because the correct way to put multiple files under the same name is to give them... the same name!

Fixes #344
Fixes #390
Fixes #399
Fixes #479
Fixes #513

* Add kdocs

* Tests, docs and rock & roll

* Remove debug information

* Use new API

* Add convenience methods

* 💅 Clean up warnings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💎 feature New feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants