Skip to content

feat: support standard graphql-multipart-request-spec for file uploads#309

Open
donleandro wants to merge 2 commits intoabsinthe-graphql:mainfrom
donleandro:feat/standard-multipart-upload-spec
Open

feat: support standard graphql-multipart-request-spec for file uploads#309
donleandro wants to merge 2 commits intoabsinthe-graphql:mainfrom
donleandro:feat/standard-multipart-upload-spec

Conversation

@donleandro
Copy link
Copy Markdown
Contributor

Hey all! We ran into this while building Shiko, a veterinary clinic management platform. We needed file uploads over GraphQL (images, documents, etc.) and found that standard clients just wouldn't work with Absinthe's custom upload format.

We initially worked around it with a REST endpoint for uploads, but having half the API on GraphQL and half on REST felt wrong. After digging into the source, we realized the issue was well-known (#296) but nobody had sent a fix yet, so here we are.

What this does

Adds support for the graphql-multipart-request-spec, which is the standard used by Apollo Client, urql, Relay, Flutter clients, and basically every other GraphQL ecosystem out there.

The approach is simple: when a request comes in with operations + map fields (the standard format), we transform it into Absinthe's native format before it hits the pipeline. This means:

  • The existing :upload scalar works without any changes
  • Absinthe's current custom format keeps working exactly as before
  • No changes needed in user schemas or resolvers
  • Single file, multiple files, and batched operations all work

How it works

The standard spec sends:

operations: {"query": "mutation($file: Upload!) {...}", "variables": {"file": null}}
map: {"0": ["variables.file"]}
0: <the actual file>

We parse operations and map, then replace the null placeholders in variables with the form field name strings (e.g., "0"). Since those field names already exist as Plug.Upload structs in conn.params, the existing uploaded_files/1 function picks them up and the :upload scalar resolves them as usual.

Testing

  • All 91 existing tests pass (no regressions)
  • Added 5 new tests covering: single upload, multiple uploads, uploads with extra variables, invalid JSON error, and missing file error
  • We also tested locally with curl against jpg, png, zip, and mp3 files in both formats

Closes #296

Add support for the widely-used graphql-multipart-request-spec format
(https://github.com/jaydenseric/graphql-multipart-request-spec) used by
Apollo Client, urql, Relay, and most GraphQL clients.

The implementation transforms the standard format (operations + map fields)
into Absinthe's native format before it reaches the pipeline, so the
existing :upload scalar works without changes. This is fully backward
compatible - the current Absinthe-specific upload format continues to
work as before.

Supports single file, multiple files, and batched operations.

Closes absinthe-graphql#296
Copy link
Copy Markdown
Contributor

@benwilson512 benwilson512 left a comment

Choose a reason for hiding this comment

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

Hey there!

While in general we have favored doing this sort of thing as an external package, this is an area where the graphql spec itself doesn't have an opinion AND Absinthe has created its own workaround, so we may as well support a broader community approach. This is also MUCH nicer than an older way of doing this which abused null variables.

The "Changes Requested" part of this is documentation. We need to update the relevant documentation to make it clear we support this spec.

Update Types moduledoc with standard spec examples and usage guide.
Add File Uploads section to README showing the recommended approach.
Add changelog entry for the new feature.
@donleandro
Copy link
Copy Markdown
Contributor Author

Added the documentation updates:

  • Types moduledoc: expanded the :upload section with a "Standard multipart spec (recommended)" guide showing single and multi-file examples via curl, and relabeled the existing format as "Absinthe's legacy format"
  • README: added a "File Uploads" section with a schema example and curl usage
  • CHANGELOG: added entry under Unreleased

About the CI failure on the first commit: the failing test is the subscription SSE test (line 553) hitting a race condition with TestPubSub.start_link() returning already started. That test is unrelated to the upload changes. The second CI run (with the docs commit) is waiting for approval.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: support standard GQL upload requests

2 participants