I’m trying to build an upload component to allow uploading of files over graphql. I’m using Absinthe for the server aspect and React + URQL (along with the multipart fetch exchange) for the frontend. I’ve followed the file uploads section of the Absinthe docs and I have it working via curl as follows:
curl -v -X POST -F query="mutation { uploadFile(input: {file: \"myfile\"}) { result }}" -F myfile=@avatar.png localhost:4000/graphql
However I can’t seem to get this same mutation to work when using URQL. The server just responds with a “no query document supplied” error.
Here’s an example of the multipart request from the browser:
Yes, URQL is using a different pattern for file uploads. I’ve been fighting this spec forever because I hate the use of null as a marker for a value that is not null. However it seems this spec is getting popular and this is a fight I’m going to lose eventually. sigh.
I see. I agree with your thinking. It seems like URQL itself is pretty modular and I’m using the @urql/exchange-multipart-fetch package (which is part of the core repo) to provide this multipart functionality.
I guess it shouldn’t be too hard to make an Absinthe-specific equivilent.
It would be nice if there was agreement on a standard spec though.
Yes this is exactly the spec I’m talking about. To be clear, this is the proposed spec by one person. I grant that it has seen increasing adoption within the JS graphql ecosystem, but it is not an official spec by the GraphQL body.
I found this thread when googling. I didn’t want to rewrite anything on the client because that sounds pretty rough, so I went for the Elixir rewriting approach. I hooked into multipart_to_params, an option to the Plug.Parsers plug that’s usually put in Endpoint
This code works for my simple case locally. I didn’t test with multiple files because that’s not a use case for me. I also didn’t worry about graceful params handling. Use at own risk, but possibly a decent starting point:
defmodule RewriteMultipart do
def multipart_params(parts, conn) do
params =
for {name, _headers, body} <- parts,
name != nil,
reduce: %{} do
acc -> Plug.Conn.Query.decode_pair({name, body}, acc)
end
params = case params do
%{"operations" => operations, "map" => map} ->
map = Jason.decode!(map)
operations = Jason.decode!(operations)
# Transform the file map to the query variables
vars =
Enum.reduce(map, operations["variables"], fn mapping, vars ->
{name, [path]} = mapping
path = String.split(path, ".") -- ["variables"]
put_in(vars, path, name)
end)
# New query drops operations/map in favor of query/variables
params
|> Map.drop(["operations", "map"])
|> Map.merge(%{"variables" => vars, "query" => operations["query"]})
_ ->
params
end
{:ok, params, conn}
end
end