fastify-swagger: file upload schema error 400

Hi, when i define the schema consumer in endpoint /upload to ‘multipart/form-data’ for uploading file i dont know wich is the schema structure body for passing the base64 string or the file that i need to upload.

If y define the schema:

schema: {
      description: 'Upload file',
      tags: ['Info'],
      consumes: [ 'multipart/form-data' ],
      summary: 'Upload the file to the server',
      body: {
        type: 'object',
        properties: {
          file: {type: 'string'}
        }
      },
      response: {
        200: {
          description: 'Succesful response',
          type: 'boolean'
        }
      }
    }

when i send the post get the error:

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

I fetch in the dynamic.js code looking for the solution but i dont get with it.

Can you help me?

PD: Im sorry if this is not the place for this kind of issues.

Thx

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 2
  • Comments: 47 (19 by maintainers)

Most upvoted comments

I found another workaround based on @SkeLLLa’s suggestion, where I add a small ajv plugin to fastify, which I believe might be a little bit less intrusive?

Below is a fully functioning example:

const Fastify = require('fastify') // 2.11.0
const multer = require('fastify-multer') // 1.5.2
const swagger = require('fastify-swagger') // 2.5.0
const uploads = multer({ dest: '/tmp/file-uploads' })

function ajvPlugin(ajv, options) {
  ajv.addKeyword('isFileType', {
    compile: (schema, parent, it) => {
      // Change the schema type, as this is post validation it doesn't appear to error.
      parent.type = 'file'
      delete parent.isFileType
      return () => true
    },
  })

  return ajv
}

const fastify = Fastify({ logger: true, ajv: { plugins: [ajvPlugin] } })

fastify.register(multer.contentParser, { addToBody: true })
fastify.register(swagger, { exposeRoute: true })

fastify.post('/uploads', {
  schema: {
    consumes: ['multipart/form-data'],
    body: { properties: { file_1: { isFileType: true } } },
    // Shortened for the example
  },
  preHandler: uploads.single('file_1'),
  handler: async (req, reply) => req.file,
})

fastify.listen(3000)

Here’s another workaround.

First of all as it was mentioned above we should add custom binary format:

const fastify = require('fastify')();
// ...
const ajv = new Ajv({
  // the fastify defaults (if needed)
  removeAdditional: true,
  useDefaults: true,
  coerceTypes: true,
  allErrors: true,
  nullable: true,
});
fastify.setSchemaCompiler(function(schema) {
  ajv.addFormat('binary', () => {
    // here you can do some additional checks
    return true;
  });
  return ajv.compile(schema);
});

Afterwards in your routes we should do some hack that will allow to pass ajv validation for string type. So we need preValidation hook where we’ll replace what’s come from user with some string, so ajv will validate it and hide original file inside non-enumerable property.

fastify.post(
    '/',
    {
      preValidation: async (req) => {
        const file = {...req.body.file};
        req.body.file = '@file';
        req.body[Symbol.for('file')] = file;
      },
      schema: {
        description: `create new file`,
        tags: ['files'],
        summary: 'create file',
        consumes: ['multipart/form-data'],
        body: {
          type: 'object',
          properties: {
            file: {
              type: 'string',
              format: 'binary',
            },
          },
        },
        response: {
         '200': { description: 'ok' },
        },
      },
    },
    async (request, reply) => {
      const files = request.body[Symbol.for('file')];
      // do something with your files.
    }
  );

I have managed to get file uploads work for my use case, its a little bit hacky.

I had a couple of issues, firstly swagger wants type to be file, but as Ajv uses that and doesn’t allow custom types, there was either altering the schema post validation or altering the fastify-swagger module code to look for another property and output that as the type property.

Secondly, the multipart module didn’t add the files data to the request body prior to the schema validation which caused errors about the body being expected as an object. So i created a custom content type parser to build the request body from the stream.

So far my solution seems to be working, although i have not tested it with larger files or in different scenarios.

If it helps anyone, here’s my code;

import Ajv = require("ajv");
import Busboy = require("busboy");
import mime = require("mime");
import { isBuffer } from "util";

const ajv = new Ajv({ coerceTypes: true, removeAdditional: true, useDefaults: true });
ajv.addKeyword("isFileType", {
	compile: (schema, parent, it) => {
                // Change the schema type, as this is post validation it doesn't appear to error.
		parent.type = "file";
		delete parent.isFileType;

		return () => true;
	},
});

fastify.setSchemaCompiler((schema) => ajv.compile(schema));

fastify.addContentTypeParser("multipart/form-data", (req, done) => {
	const body = {};
	const stream = new Busboy({ headers: req.headers });

	req.on("error", (err) => {
		stream.destroy();
		done(err);
	});

	stream.on("finish", () => done(null, body));

	stream.on("file", (field, file, filename, encoding, mimetype) => {
		file.on("data", (data) => {
			if (! body[field]) {
				body[field] = [];
			}

			body[field].push(data);
		});

		file.on("end", () => {
			body[field] = Buffer.concat(body[field].map((part) => isBuffer(part) ? part : Buffer.from(part)));
		});
	});

	req.pipe(stream);
});

Then just added the isFileType property into the schema;

fastify.post("/upload", {
	schema: {
		body: {
			properties: {
				file: {
					isFileType: true,
					type: "object",
				},
			},
			required: [ "file" ],
			type: "object",
		},
		consumes: [ "multipart/form-data" ],
		response: {
			200: {
				properties: {
					success: {
						example: true,
						type: "boolean",
					},
				},
				type: "object",
			},
		},
	},
}, (request, reply) => {
	console.info("File", request.body.file);
	reply.code(200).send({ success: true });
});

Error: Error: unknown format “binary” is used in schema at path “#”

Can you please get a stacktrace for that?

I think we should add a custom keyword to Fastify ajv: https://github.com/epoberezkin/ajv#defining-custom-keywords.

We would have to replace the stock ajv instance with a custom one with the new keyword: https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler.

Would you like to send a PR to this module? It think it’s something that we should ship.

cc @delvedor

I’m not that expert on swagger.

@sarneeh I think that adding a new key could create misunderstanding.

The content type defines “what and how” the data are written in the body of the HTTP request. In fact, the standard talk about the “body” also with the multipart content type.

So what we do here is translating the multipart body, to the json format (that is the easiest format to read the data) and we apply the validation on it.

If we would add a new key for the multipart, we should add a key for each content-types out there and develop a validator that can manage that type of format: this could be a HUGE work.

For this reason, I think that translating the multipart to a json-body is a good compromise