Files
Handling files is traditionally not a concern of a GraphQL server, which is also why the GraphQL over HTTP specification does not mention it.
That said, at some point in the development of a new application you will likely have to deal with files in some way. This page gives you guidance on the available approaches.
Uploading Files
When it comes to uploading files, you have several options.
Completely Decoupled
You can handle file uploads completely decoupled from your GraphQL server, for example using a dedicated web application that offers an HTTP endpoint for uploads.
This has a couple of downsides:
- Authentication and authorization need to be handled by the dedicated endpoint as well.
- The process of uploading a file needs to be documented outside of your GraphQL schema.
Upload Scalar
Hot Chocolate implements the GraphQL multipart request specification which adds a new Upload scalar and lets your GraphQL server handle file upload streams.
Warning: Files cannot yet be uploaded through a gateway to stitched services using the
Uploadscalar.
Usage
Register the Upload scalar to use file upload streams in your input types or as an argument:
builder .AddGraphQL() .AddType<UploadType>();
Note: The
Uploadscalar can only be used as an input type and does not work on output types.
Use the Upload scalar as an argument:
public class Mutation{ public async Task<bool> UploadFileAsync(IFile file) { var fileName = file.Name; var fileSize = file.Length;
await using Stream stream = file.OpenReadStream();
// You can now work with standard stream functionality of .NET // to handle the file. }}
In input object types you can use it as follows:
public class ExampleInput{ [GraphQLType(typeof(NonNullType<UploadType>))] public IFile File { get; set; }}
Learn more about input object types
If you need to upload a list of files, use a List<IFile> or ListType<UploadType>.
UploadValueNode
In v16, the upload literal node was renamed from FileValueNode to UploadValueNode. If you reference this type in custom scalar logic or tests, update your code:
if (valueLiteral is UploadValueNode uploadValue){ var file = uploadValue.File; var key = uploadValue.Key;}
When constructing upload value nodes manually, the constructor now also requires the multipart key:
var valueNode = new UploadValueNode("0", file);
Client Usage
When performing a mutation with the Upload scalar, you need to use variables.
An example mutation:
mutation ($file: Upload!) { uploadFile(file: $file) { success }}
Send this request to your GraphQL server using HTTP multipart:
curl localhost:5000/graphql \ -H "GraphQL-preflight: 1" \ -F operations='{ "query": "mutation ($file: Upload!) { uploadFile(file: $file) { success } }", "variables": { "file": null } }' \ -F map='{ "0": ["variables.file"] }' \ -F 0=@file.txt
Note 1: The
$filevariable is intentionallynull. Hot Chocolate fills it in on the server.
Note 2: The
GraphQL-preflight: 1HTTP header is required since version 13.2 for security reasons.
More examples can be found here
You can check if your GraphQL client supports the specification here.
Both Relay and Apollo support this specification through community packages:
- react-relay-network-modern using the
uploadMiddleware - apollo-upload-client
Options
If you need to upload larger files or set custom upload size limits, configure FormOptions:
builder.Services.Configure<FormOptions>(options =>{ // Set the limit to 256 MB options.MultipartBodyLengthLimit = 268435456;});
Depending on your web server, you might need to configure these limits elsewhere as well. Kestrel and IIS are covered in the ASP.NET Core documentation.
Presigned Upload URLs
The best solution for uploading files is a hybrid approach. Your GraphQL server provides a mutation for uploading files, but the mutation only sets up the file upload. The actual file upload happens through a dedicated endpoint.
You accomplish this by returning presigned upload URLs from your mutations. These are URLs that point to an endpoint through which files can be uploaded. Files can only be uploaded to this endpoint if the URL contains a valid token. Your mutation generates the token, appends it to the upload URL, and returns the presigned URL to the client.
Here is an example mutation resolver:
public record ProfilePictureUploadPayload(string UploadUrl);
public class Mutation{ [Authorize] public ProfilePictureUploadPayload UploadProfilePicture() { var baseUrl = "https://blob.chillicream.com/upload";
// Handle authorization logic here
// If the user is allowed to upload, generate the token var token = "myUploadToken";
var uploadUrl = QueryHelpers.AddQueryString(baseUrl, "token", token);
return new(uploadUrl); }}
If you are using a major cloud provider for storing your BLOBs, they likely support presigned upload URLs:
Here is how a client would upload a new profile picture:
Request
mutation { uploadProfilePicture { uploadUrl }}
Response
{ "data": { "uploadProfilePicture": { "uploadUrl": "https://blob.chillicream.com/upload?token=myUploadToken" } }}
Given the uploadUrl, the client can HTTP POST the file to this endpoint to upload the profile picture.
This solution offers the following benefits:
- Uploading files is treated as a separate concern and your GraphQL server stays focused on GraphQL.
- The GraphQL server maintains control over authorization and all business logic regarding granting a file upload stays in one place.
- The action of uploading a profile picture is described by the schema and therefore more discoverable for developers.
There is still some uncertainty about how the actual file upload happens, such as which HTTP verb to use or which headers to send with the uploadUrl. These additional parameters can be documented separately or made queryable through your mutation.
Serving Files
Imagine you want to expose the file you uploaded as the user's profile picture. How do you query for this file?
You could make the profile picture a queryable field that returns the Base64-encoded image. While this can work, it has several downsides:
- Since the image is part of the JSON serialized GraphQL response, caching is very difficult.
- A query for the user's name might take a few milliseconds. Adding the image data might increase the response time by seconds.
- Streaming (for example, video playback) would not work.
The recommended solution is to serve files through a different HTTP endpoint and reference that endpoint in your GraphQL response. Instead of querying for the profile picture data, query for a URL that points to the profile picture.
Request
{ user { name imageUrl }}
Response
{ "data": { "user": { "name": "John Doe", "imageUrl": "https://blob.chillicream.com/john-doe.png" } }}
Serving the file through a dedicated HTTP endpoint makes caching much easier and supports features like streaming. It gives the client control over how a resource is handled given its URL. In a web application, you pass the imageUrl as src to an HTML img element and let the browser handle fetching and caching.
If you are using a cloud provider for file storage, you are likely already accessing files using a URL and can expose this URL as a String field in your graph. If infrastructure for serving files is not in place, you can set up file serving with ASP.NET Core or a dedicated web server like nginx.
Next Steps
- Arguments for details on defining input arguments.
- Input Object Types for defining complex input types.
- Migrate from v15 to v16 for the
FileValueNoderename details.