4

I'm using fastify-multer and JSON Schema to submit multipart form data that may include a file. No matter what I do, Fastify keeps giving me a bad response error:

{ "statusCode": 400, "error": "Bad Request", "message": "body must be object" } 

Here is my index.ts:

const server = fastify(); server.register(require("@fastify/cors")); server.register(multer.contentParser).after(() => { if (!isProdEnv) { server.register(require("@fastify/swagger"), { /* ... */ }); } server.register(require("@fastify/auth")).after(() => { server.decorate("authenticateRequest", authenticateRequest); server.decorate("requireAuthentication", requireAuthentication); server.addHook("preHandler", server.auth([server.authenticateRequest])); server.register(indexRouter); server.register(authRouter, { prefix: "/auth" }); server.register(usersRouter, { prefix: "/users" }); server.register(listsRouter, { prefix: "/lists" }); server.register(postsRouter, { prefix: "/posts" }); server.register(searchRouter, { prefix: "/search" }); server.register(settingsRouter, { prefix: "/settings" }); }); }); server.setErrorHandler((err, req, res) => { req.log.error(err.toString()); res.status(500).send(err); }); 

And the /posts/create endpoint:

const postsRouter = (server: FastifyInstance, options: FastifyPluginOptions, next: HookHandlerDoneFunction) => { server.post( "/create", { schema: { consumes: ["multipart/form-data"], body: { content: { type: "string" }, media: { type: "string", format: "binary" }, "media-description": { type: "string" } } }, preHandler: [server.auth([server.requireAuthentication]), uploadMediaFileToCloud] }, postsController.createPost ); next(); }; export default postsRouter; 

Request CURL:

curl -X 'POST' \ 'http://localhost:3072/posts/create' \ -H 'accept: */*' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoYW5kbGUiOiJ1bGtrYSIsInVzZXJJZCI6IjYyNGQ5NmY4NzFhOTI2OGY2YzNjZWExZCIsImlhdCI6MTY1NzEwNTg5NCwiZXhwIjoxNjU3NDA1ODk0fQ.A5WO3M-NhDYGWkILQLVCPfv-Ve-e_Dlm1UYD2vj5UrQ' \ -H 'Content-Type: multipart/form-data' \ -F 'content=Test.' \ -F '[email protected];type=image/png' \ -F 'media-description=' \ 

Why is this not working?

6
  • could you add the client's request? A curl would be perfect Commented Jul 7, 2022 at 8:09
  • Moreover I don't see the github.com/fastify/fastify-multipart registration Commented Jul 7, 2022 at 8:11
  • @ManuelSpigolon I have added CURL. Also, is fastify-multipart required for fastify-multer to work? It's never mentioned anywhere in the documentation! Commented Jul 7, 2022 at 8:33
  • No, but your question doesn't not contain it neither Commented Jul 7, 2022 at 19:18
  • 1
    It is not compatible with fastify v4. Try to use the official fastify-multipart plugin instead Commented Jul 8, 2022 at 6:12

2 Answers 2

8

EDIT 2: Apparently, there is a really easy solution for this: Use multer in the preValidation hook instead of preHandler. So, a piece of working code will look like this:

server.register(multer.contentParser).after(() => { server.register( (instance: FastifyInstance, options: FastifyPluginOptions, next: HookHandlerDoneFunction) => { instance.post( "/create", { schema: { consumes: ["multipart/form-data"], body: { type: "object", properties: { content: { type: "string" }, media: { type: "string", format: "binary" } } } }, preValidation: multer({ limits: { fileSize: 1024 * 1024 * 5 }, storage: multer.memoryStorage() }).single("media") }, (request: FastifyRequest, reply: FastifyReply) => { const content = (request.body as any).content as string; const file = (request as any).file as File; if (file) { delete file.buffer; } reply.send({ content, file: JSON.stringify(file) || "No file selected" }); } ); next(); }, { prefix: "/posts" } ); }); 

EDIT: After I posted the answer below, I was able to find a solution for this. Updating my answer for anyone else who might encounter the same issue.

First, I switched to @fastify/multipart from fastify-multer. Then I removed the type property from the media field.

media: { format: "binary" } 

After this, I added the option{ addToBody: true } when registering @fastify/multipart.

import fastifyMultipart from "@fastify/multipart"; server.register(fastifyMultipart, { addToBody: true }).after(() => { ... }); 

After these changes, the field media became available in request.body.


OLD ANSWER:

Seems like these days I have to answer my own questions here. Anyway, I figured out what's happening. Fastify's built-in schema validation doesn't play well with multipart/form-data. I played around with the schema specification to make sure that this is the case. So I removed schema validation from all routes. My use case here was porting an API from ExpressJS to Fastify, so I had a nice Swagger JSON spec generated using express-oas-generator lying around. I used that to generate Swagger UI and everything worked fine. I hope Fastify gets its act together and sorts out this issue.

Sign up to request clarification or add additional context in comments.

2 Comments

sensei @Mr.X . I am very grateful for your answers.
I created this multipart/form-data parser plugin to solve this issue without any workarounds. npmjs.com/package/formzilla
2

You can now do this with @fastify/multipart by asking it to attach all multipart form fields to an object as if it was JSON, using the magic value "keyValues":

server.register(multipart, { attachFieldsToBody: "keyValues", ... }); 

Mark the file(s) as type "object" in the schema:

const SCHEMA_API_UPLOAD = { consumes: [ "multipart/form-data" ], body: { type: "object", properties: { file: { type: "object" }, description: { type: "string" }, } } }; 

Then each form field will be a field on the request.body object:

server.post("/upload", { schema: SCHEMA_API_UPLOAD }, async (request, reply) => { console.log(request.body); }); 

And you can see the object via curl:

$ curl -v localhost:8080/upload -F description="something good" -F [email protected] ... [20250109-20:02:50.501] INF (uy33kj8cjzw6) [127.0.0.1:48898] POST /upload [20250109-20:02:50.502] DEB (uy33kj8cjzw6) starting multipart parsing [20250109-20:02:50.502] TRA (uy33kj8cjzw6) Providing options to busboy { description: 'something good', file: <Buffer ff d8 ff e0 ... 61483 more bytes> } 

2 Comments

Wow. Will try this, and if it works, I'll mark it as the selected answer.
Alright, tried that, and realised why I avoided @fastify/multipart. My actual schema is a bit complex, involving nested objects, and @fastify/multipart throws an error when I submit them as multipart/form-data.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.