How to Handle Custom Content-Type Fields in FormData With Fastify
Table of Contents
How To Handle Custom Content-Types in FormData with Fastify
Introduction
In this tutorial, we’ll explore how to handle custom Content-Types in FormData when working with Fastify, a popular web framework for Node.js. We’ll address a common limitation in the FormData Web API and implement a workaround that allows you to send non-file data with custom Content-Types without being treated as files by the server.
Prerequisites
This is aimed at those who:
- Use Fastify as web framework
- Make multipart/form-data requests from browsers
Step 1: Understanding the Limitation
The FormData Web API in JavaScript has a limitation: it doesn’t allow changing the Content-Type header of a part that is not a file. This means that if you want to change the Content-Type header, a “filename” parameter will always be included.
This becomes problematic when using Fastify with the @fastify/multipart plugin, which uses the busboy library to parse form data. Busboy considers any part with a “filename” parameter to be a file, making it impossible to send a request using the FormData Web API that includes a part with a custom Content-Type that isn’t treated as a file by the backend.
Step 2: Implementing the Workaround in Fastify
To address this issue, we can use a workaround implemented in @fastify/multipart. This solution allows us to decide whether a multipart/form-data part is a file or a field. Here’s how to implement it:
import fastify from 'fastify'
import multiPart from '@fastify/multipart'
const fastifyBase = fastify()
fastifyBase.register(multiPart, {
isPartAFile: (fieldName, contentType, fileName) => {
const isAField = fileName === undefined || (contentType === "application/json" && fileName.length === 0);
return !isAField;
},
});
This configuration tells Fastify to treat parts with undefined filenames or empty filenames (when the Content-Type is “application/json”) as fields rather than files.
Step 3: Creating a Custom FormData Class
To take advantage of this workaround on the client side, it’s useful to create a custom FormData class that sets an empty filename for non-file data with custom Content-Types. Here’s an example implementation:
class FormDataExt extends FormData {
append(name: string, value: string | Json): void;
append(name: string, blobValue: Blob, filename?: string): void;
append(name: string, value: string | Blob | Json, fileName?: string): void {
if (typeof value === "string") {
super.append(name, value);
} else if (value instanceof Blob) {
super.append(name, value, fileName);
} else {
const jsonBlob = new Blob([JSON.stringify(value)], { type: "application/json" });
super.append(name, jsonBlob, "");
}
}
}
This custom class extends the native FormData class and overrides the append
method to accept JSON data and treat it differently. When appending JSON data, it creates a Blob with the “application/json” Content-Type and sets an empty filename.
Step 4: Using the Custom FormData Class
Now you can use the custom FormDataExt class in your client-side code to send data with custom Content-Types without them being treated as files by the server:
const formData = new FormDataExt();
formData.append('textField', 'Hello, world!');
formData.append('jsonField', { key: 'value' });
// Send the formData to your Fastify server
fetch('/upload', {
method: 'POST',
body: formData
});
In this example, the ‘jsonField’ will be sent with a Content-Type of “application/json” but will be treated as a field, not a file, by the Fastify server.
Conclusion
By implementing this workaround, we’ve successfully addressed the limitation of the FormData Web API when working with Fastify and @fastify/multipart. This solution allows you to send non-file data with custom Content-Types without them being misinterpreted as files by the server.
This approach is particularly useful when you want to send structured data, like JSON, alongside other form fields and files in a single request. It maintains the flexibility of multipart/form-data while overcoming the constraints imposed by the FormData API and common server-side parsing libraries.
Remember to always validate and sanitize data on the server-side, regardless of how it’s sent, to ensure the security and integrity of your application.
For Interested Readers: The Technical Background
For those curious about the underlying reasons for this workaround, it’s worth diving into the technical specifications and implementations that led to this situation.
The FormData Web API Limitation
The FormData Web API follows a specific serialization process. According to the Web API FormData Serialization specification, when appending a value that’s not a string, it’s treated as a File object:
If value is a string:
Otherwise:
1. Assert: value is a File.
2. Append
; filename="
,
This means a filename parameter is always included in the Content-Disposition header, even when you’re not actually sending a file.
The multipart/form-data Specification
Interestingly, this behavior is not mandated by the multipart/form-data specification itself. RFC 7578, which defines the “Returning Values from Forms: multipart/form-data” standard, does not limit the Content-Type that a field part can have.
Server-Side Parsing
The issue is compounded on the server side. Many Node.js libraries, including busboy (which is used by @fastify/multipart), interpret any part with a filename parameter as a file. This interpretation, while practical in many cases, doesn’t align perfectly with the RFC’s more flexible approach.
The Workaround and Its Implications
The workaround we’ve implemented allows us to bridge the gap between the FormData Web API’s behavior, the actual multipart/form-data specification, and common server-side parsing implementations. By setting an empty filename for non-file data with custom Content-Types, we’re able to send this data in a way that aligns with the Web API’s constraints while also being correctly interpreted by our modified server configuration.
Prevalence and Support in the Ecosystem
It is difficult to assess how widely form-data fields with varying Content-Type headers are used, but there is reason to believe that it’s not uncommon. The official OpenAPI 3.0 Guide uses an example request that has a field with Content-Type: application/json
. Additionally, several web frameworks support automatic JSON parsing of fields with this content type. This suggests that while this use case might not be the most common, it’s certainly recognized and catered for in the broader web development ecosystem.