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

Add support for multipart requests. #87

Merged
merged 6 commits into from
Sep 11, 2017
Merged

Conversation

ijcd
Copy link
Contributor

@ijcd ijcd commented Jun 18, 2017

I needed multipart support so I took a stab at it. Feedback welcome.

You can pass a Multipart struct as the body.

Example:
```ex
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this example is missing alias Tesla.Multipart. It could be misleading for someone who read only a readme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll make the change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks!

@teamon
Copy link
Member

teamon commented Jun 23, 2017

Whoo this is really cool!
I will give it a try after weekend, but the API looks pretty well-thought 👍

@ijcd
Copy link
Contributor Author

ijcd commented Jul 6, 2017

I updated the docs with the suggestion. Are there any other changes you would like to see?

@ijcd
Copy link
Contributor Author

ijcd commented Aug 17, 2017

Do you require any changes or can we get this merged?

@amatalai
Copy link
Collaborator

I'm aware that it's been a long time since the PR was created, but it's big one and therefore it requires a lot of free time to review it properly. I'll try over the weekend if other duties won't prevent me.

Copy link
Contributor

@chingor13 chingor13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is great!

We are looking at improving Swagger Codegen for elixir clients and want to be able to support multipart file uploads with autogenerated clients.

One other issue is that this doesn't play nicely with the Tesla.Middleware.JSON middleware. Currently, it will try to serialize the Tesla.Multipart struct and fail on a {file, "name"} tuple if you added a file to your request.

I think we can tell the JSON middleware that a body that is a Multipart struct should not be encodable?.

@@ -19,11 +21,16 @@ if Code.ensure_loaded?(:hackney) do
end
defp request(method, url, headers, %Stream{} = body, opts), do: request_stream(method, url, headers, body, opts)
defp request(method, url, headers, body, opts) when is_function(body), do: request_stream(method, url, headers, body, opts)
defp request(method, url, headers, %Multipart{} = mp, opts) do
headers = Keyword.merge(headers, Multipart.headers(mp))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers may not always be a keyword list. When creating an API client, you can add a middleware that provides headers like:

plug Tesla.Middleware.Headers, %{"Authorization" => "token xyz"}

This is translated to a list of 2-tuples where the first element is a string (so it's not a keyword list).

This example yields:

(ArgumentError) expected a keyword list as the first argument, got: [{'authorization', 'token xyz'}]

@@ -40,6 +41,15 @@ defmodule Tesla.Adapter.Httpc do
:httpc.request(method, {url, headers}, http_opts, opts)
end

defp request(method, url, headers, _content_type, %Multipart{} = mp, opts) do
headers = Keyword.merge(headers, Multipart.headers(mp))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here about headers necessarily being a keyword list.

@@ -21,6 +22,13 @@ if Code.ensure_loaded?(:ibrowse) do
)
end

defp request(url, headers, method, %Multipart{} = mp, opts) do
headers = Keyword.merge(headers, Multipart.headers(mp))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here about headers necessarily being a keyword list.

@amatalai
Copy link
Collaborator

I think that list of possible opts should be added to docs for add_field and add_file.

Also found 2 issues:

  • it doesn't work with DebugLogger middleware due to missing pattern match for %Multipart{}
  • when passed invalid path to add_file and then I tried to send request everything hangs, maybe it should check if file exist first

Anyway big 👍 from me

@teamon what do you think about merging this and working on further improvements in separate PRs?

@amatalai
Copy link
Collaborator

got green light for merge 💚
I'll do it tomorrow. Also, I will copy thing from my previous post to issues to not forget about them

@teamon teamon merged commit be52124 into elixir-tesla:master Sep 11, 2017
@teamon
Copy link
Member

teamon commented Sep 11, 2017

There is a to :mimerl which is not added to dependencies.

warning: function :mimerl.filename/1 is undefined (module :mimerl is not available)
  lib/tesla/multipart.ex:92

@ijcd Have you tried https://hex.pm/packages/mime ?

@ijcd
Copy link
Contributor Author

ijcd commented Sep 12, 2017

I don't recall if I tried that one. It looks like :mimerl came in with hackney for me, possibly, since benoitc works on both. It's probably better to go with the Elixir one as it looks more recently updated and maintained. It's only used for guessing the mime-type on upload.

@fdbeirao
Copy link

I reached this post because I am trying to use elixir-google-api and I was messing around with it in the repl/iex.

This is what happened to me:

when passed invalid path to add_file and then I tried to send request everything hangs, maybe it should check if file exist first

How is it possible to completely halt the repl? Why isn't it "just crashing"?

I now see what I am doing wrong: I am setting "Hello from Elixir!" as the "path".. but it blows my mind that the repl can hang like that, as if it were stuck in some "low-level" receive() method.. :( if I was running this code inside an Elixir process, would the process also just hang (in there)?

I am running in Windows and these were the commands I run:

{:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform")
conn = GoogleApi.Storage.V1.Connection.new(token.token)
GoogleApi.Storage.V1.Api.Objects.storage_objects_insert_simple(conn, "--my--bucket--id--", "multipart", %GoogleApi.Storage.V1.Model.Object{:"contentType" => "plain/text", :"name" => "upload.txt"}, "Hello from Elixir!", [])

My intention was to "upload" a file called upload.txt, with content Hello from Elixir!.. I will now try to find a way to do that. Preferably one that doesn't require writing to temporary files :)

Cheers.

@teamon
Copy link
Member

teamon commented Jan 24, 2018

Thanks for letting me know elixir-google-api uses tesla! 🎉

You don't need to create a tmp file - you can use add_file_content

About the hanging - can you check with raw tesla? Does it hang at add_file or when perfrming the request?

@fdbeirao
Copy link

@teamon your suggestion was spot on! I had to locally create a function that uses add_file_content behind the scenes, because the original elixir-google-api doesn't support that function.

About the freezing thing, it must be something wrong with my setup (I'm looking at you Windows!).

If I run iex -S mix followed by all the Elixir commands that lead to a freeze, and then I Ctrl+C to kill it and do it all over again, the second time it works fine 🤦‍♂️

I have access to a linux VM, so I'll try too see if it also hangs in there (in there).

Massive kudos for your hint 👍

@teamon
Copy link
Member

teamon commented Jan 25, 2018

Happy to help! 🕺

In case you still have some problems please open a separate issue, good luck!

@henry-hz
Copy link

@fdbeirao Fabio, pls, how can you uplaod a file into a bucket folder (in case you need to upload to /my-bucket/my-folder/file.txt) ?

@fdbeirao
Copy link

@henry-hz Henry, in order to place the file in a "folder" I specify the file_name as the full path with folder, separated by /. Do keep in mind that folders are a lie.

This file_name gets added to the query params:

defp storage_objects_insert_from_payload(
         connection,
         bucket,
         content_type,
         file_name,
         file_payload
       ),
       do:
         %{}
         |> method(:post)
         |> url("/upload/storage/v1/b/#{bucket}/o")
         |> add_param(:headers, "content-type", content_type)
         |> add_param(:query, :uploadType, "media")
         |> add_param(:query, :name, file_name)
         |> add_param(:body, :body, file_payload)
         |> Enum.into([])
         |> (&Connection.request(connection, &1)).()
         |> decode(%GoogleApi.Storage.V1.Model.Object{})

Let me know if this helped you. I haven't touched that code for a while.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants