File uploads
The HTTP plugin can receive files sent as multipart/form-data. It is disabled by default. Enable it in the plugin options and configure the default limits applied to every endpoint:
HttpPluginOptionsFactory.create({
fileUpload: true,
fileUploadOptions: {
maxFileSize: 5 * 1024 * 1024,
maxFiles: 5,
allowedMimeTypes: ['image/png', 'image/jpeg', 'application/pdf']
}
});
It works the same with the Express and Fastify adapters. Each adapter parses the request with its own multipart parser (busboy for Express and @fastify/multipart for Fastify) behind the common HttpRequest abstraction, so you don't have to register any adapter-specific middleware.
Options
/** The reason why a multipart upload was rejected. */
export type FileUploadLimitReason = 'fileSize' | 'files' | 'mimeType';
export interface FileUploadOptions {
/** Maximum size in bytes allowed per file. */
maxFileSize: number;
/** Maximum number of files allowed per request. */
maxFiles?: number;
/** Allowed MIME types (e.g. `['image/png', 'application/pdf']`). All allowed if omitted. */
allowedMimeTypes?: string[];
/** The message to display when an upload limit is exceeded. By default, it will display a generic message. */
message?: string | ((limit: number, reason: FileUploadLimitReason) => string);
}
When fileUploadOptions or one of its values is omitted, the defaults are 5 MiB per file and 10 files per request, and every MIME type is allowed. HttpPluginOptionsFactory.create() sets fileUpload: false and fileUploadOptions: { maxFileSize: 5 * 1024 * 1024, maxFiles: 10 }.
MIME types are compared without case sensitivity against the Content-Type declared by the client for each file. The file contents are not inspected, so validate them in your handler if you need to trust the type.
Receiving files
Use @UploadedFile(field) to inject the first file sent in a form field, and @UploadedFiles(field?) to inject every file of a field, or every file of the request when no field is given. The text fields of the form are available in @Body(). Repeated text fields are collected into an array.
import { Body, Controller, Path, Post, UploadedFile, UploadedFiles } from '@axisparkjs/http';
@Controller('users')
export class UserController {
@Post(':id/avatar')
uploadAvatar(@Path('id') id: string, @UploadedFile('avatar') avatar: UploadedFile, @Body() body: { description: string }) {
return { id, name: avatar.originalName, size: avatar.size, description: body.description };
}
@Post('documents')
uploadDocuments(@UploadedFiles('documents') documents: UploadedFile[]) {
return { count: documents.length };
}
}
UploadedFile is also the type of the injected files. Files are kept in memory:
interface UploadedFile {
/** The name of the form field the file was sent in. */
fieldName: string;
/** The file name provided by the client. */
originalName: string;
/** The MIME type declared by the client. */
mimeType: string;
/** The size of the file in bytes. */
size: number;
/** The contents of the file. */
buffer: Buffer;
}
@UploadedFile() resolves to undefined when the field has no file, and @UploadedFiles() resolves to an empty array. When the request is not multipart/form-data, both resolve to undefined.
Overriding the limits
Use @FileUpload() on a controller or handler to override the global options. A handler decorator takes precedence over the controller decorator, which takes precedence over fileUploadOptions. Partial options use the next level for the values they omit:
import { Controller, FileUpload, Post, UploadedFile, UploadedFiles } from '@axisparkjs/http';
@FileUpload({ maxFiles: 1 })
@Controller('contracts')
export class ContractController {
@Post()
@FileUpload({ maxFileSize: 20 * 1024 * 1024, allowedMimeTypes: ['application/pdf'] })
upload(@UploadedFile('file') file: UploadedFile) {
return { name: file.originalName };
}
@Post('annexes')
@FileUpload({ maxFiles: 5 })
uploadAnnexes(@UploadedFiles('annexes') annexes: UploadedFile[]) {
return { count: annexes.length };
}
}
Errors
When a limit is exceeded, the request is rejected before the handler runs:
| Reason | Status | Default message |
|---|---|---|
fileSize | 413 Content Too Large | File exceeds the maximum allowed size of <n> bytes. |
files | 413 Content Too Large | Too many files. The maximum allowed is <n>. |
mimeType | 415 Unsupported Media Type | Unsupported file type. |
A malformed multipart body is rejected with 400 Bad Request.
The message option replaces the default messages. A callback receives the exceeded limit and the reason, so a single function can build every message. The limit is the size in bytes for fileSize, the number of files for files, and the number of allowed MIME types for mimeType:
HttpPluginOptionsFactory.create({
fileUpload: true,
fileUploadOptions: {
maxFileSize: 1024 * 1024,
message: (limit, reason) => (reason === 'mimeType' ? 'Only images are accepted' : `Upload limit exceeded (${limit})`)
}
});
How it works
Files are parsed by the FileUploadGuard, a global HTTP guard with the lowest priority. It runs after the other guards, including the authentication and authorization guards of the security plugin, so files are never buffered for requests that are rejected. Parsing happens inside the request pipeline, so it counts towards the timeoutOptions.time of the request.
Keep in mind:
- The guard only parses requests with a
multipart/form-datacontent type. Other requests keep using the JSON and URL-encoded parsers. - While
fileUploadisfalse,@UploadedFile(),@UploadedFiles()and@FileUpload()have no effect and the parameters resolve toundefined. With the Fastify adapter,multipart/form-datarequests are rejected by Fastify with415 Unsupported Media Typebecause no parser is registered. - Files are buffered in memory, so the worst case memory per request is roughly
maxFileSize × maxFiles. Keep the limits as low as your endpoints allow.