fastify-multipart icon indicating copy to clipboard operation
fastify-multipart copied to clipboard

multipart does not work with validation and fastify-swagger

Open xplodeart opened this issue 3 years ago • 14 comments

🐛 Bug Report

When used with validation and fastify-swagger with the same code from the example the file object is empty.

When validation is disabled, the upload works as intended.

To Reproduce

Register fastify-multipart and fastify-swagger, add a POST route with schema as in the example and try to send a multipart upload with the same name as defined in the schema.

Expected behavior

Needs to work same as without validation.

Your Environment

  • node version: 12
  • fastify version: 3.7.0
  • os: Windows

xplodeart avatar Oct 21 '20 11:10 xplodeart

Can you please provide a full example to reproduce? I'm not sure where the problem is and it will be very helpful. Essentially a server and a client calling it on the same javascript file / or maybe a repo. Essentially something that we can run that clearly show the failure.

mcollina avatar Oct 21 '20 11:10 mcollina

@mcollina sure, sorry for making this harder.

Here's a package.json:

{
  "name": "fastify-bug",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "author": "Max <[email protected]>",
  "dependencies": {
    "fastify": "^3.7.0",
    "fastify-multipart": "^3.2.1",
    "fastify-swagger": "^3.4.0",
    "pino-pretty": "^4.3.0"
  }
}

Here's a full example of how it won't work:

const fastify = require( 'fastify' );

const swagger = require( 'fastify-swagger' );

const app = fastify( {
  logger: {
    prettyPrint: true
  },
  ajv: {
    customOptions: {
      jsonPointers: true,
      allErrors: true
    }
  }
} );

app.register( require( 'fastify-multipart' ), {
  attachFieldsToBody: true,
  sharedSchemaId: '#mySharedSchema'
} )

app.register( swagger, {
  routePrefix: '/docs',
  swagger: {
    info: {
      title: 'Sample',
      description: 'Sample',
      version: '0.0.1'
    },
    host: 'localhost:3333',
    schemes: ['http'],
    securityDefinitions: {
      bearerAuth: {
        type: 'apiKey',
        name: 'Authorization',
        in: 'header',
        description: 'Format: "Bearer $TOKEN"'
      }
    },
    consumes: ['application/json','multipart/form-data'],
    produces: ['application/json'],
    tags: [
      {
        name: 'Files',
        description: 'Upload, delete, get signed url'
      }
    ]
  },
  exposeRoute: true
} );

app.post( '/upload', {
  schema: {
    description: 'Upload a File, the field name should be "file"',
    tags: [ 'Files' ],
    summary: 'Upload',
    consumes: ['multipart/form-data'],
    body: {
      type: 'object',
      required: ['file'],
      properties: {
        file: { $ref: '#mySharedSchema' }
      }
    },
    response: {
      201: {
        description: 'Upload OK',
        type: 'object'
      },
      400: {
        description: 'Bad Request',
        type: 'object'
      }
    }
  }
}, async function ( request, response ) {
  try {
    const data = await request.file();
    console.log( data );
    response.code( 201 ).send();
  } catch ( error ) {
    console.error( error );
    response.code( 400 ).send();
  }
} );

app.listen( 3333, '0.0.0.0', function ( err, address ) {
  if ( err ) {
    app.log.error( err );
    process.exit( 1 );
  }
  app.log.info( 'server listening on ' + address );
} );

If we comment out body in schema and attachFieldsToBody: true in the multipart regitration it will work without a problem.

Also there is some warning: (node:11548) ExperimentalWarning: The fs.promises API is experimental.

Here's the working example:

const fastify = require( 'fastify' );

const swagger = require( 'fastify-swagger' );

const app = fastify( {
  logger: {
    prettyPrint: true
  },
  ajv: {
    customOptions: {
      jsonPointers: true,
      allErrors: true
    }
  }
} );

app.register( require( 'fastify-multipart' ), {
  //attachFieldsToBody: true,
  sharedSchemaId: '#mySharedSchema'
} )

app.register( swagger, {
  routePrefix: '/docs',
  swagger: {
    info: {
      title: 'Sample',
      description: 'Sample',
      version: '0.0.1'
    },
    host: 'localhost:3333',
    schemes: ['http'],
    securityDefinitions: {
      bearerAuth: {
        type: 'apiKey',
        name: 'Authorization',
        in: 'header',
        description: 'Format: "Bearer $TOKEN"'
      }
    },
    consumes: ['application/json','multipart/form-data'],
    produces: ['application/json'],
    tags: [
      {
        name: 'Files',
        description: 'Upload, delete, get signed url'
      }
    ]
  },
  exposeRoute: true
} );

app.post( '/upload', {
  schema: {
    description: 'Upload a File, the field name should be "file"',
    tags: [ 'Files' ],
    summary: 'Upload',
    consumes: ['multipart/form-data'],
    /*body: {
      type: 'object',
      required: ['file'],
      properties: {
        file: { $ref: '#mySharedSchema' }
      }
    },*/
    response: {
      201: {
        description: 'Upload OK',
        type: 'object'
      },
      400: {
        description: 'Bad Request',
        type: 'object'
      }
    }
  }
}, async function ( request, response ) {
  try {
    const data = await request.file();
    console.log( data );
    response.code( 201 ).send();
  } catch ( error ) {
    console.error( error );
    response.code( 400 ).send();
  }
} );

app.listen( 3333, '0.0.0.0', function ( err, address ) {
  if ( err ) {
    app.log.error( err );
    process.exit( 1 );
  }
  app.log.info( 'server listening on ' + address );
} );

xplodeart avatar Oct 21 '20 11:10 xplodeart

(I've formatted your example)

mcollina avatar Oct 21 '20 11:10 mcollina

(I've formatted your example)

Me too 👍

xplodeart avatar Oct 21 '20 11:10 xplodeart

What error are you getting?

mcollina avatar Oct 21 '20 11:10 mcollina

@mcollina no error at all, just the file object is undefined if the validation is set.

attachFieldsToBody: true seems to be causing this result.

xplodeart avatar Oct 21 '20 11:10 xplodeart

Note that your code is not correct. Check out https://github.com/fastify/fastify-multipart#parse-all-fields-and-assign-them-to-the-body for the attachFieldsToBody option. You can use that together with await request.file(), we should throw an error if those are used together.

mcollina avatar Oct 21 '20 13:10 mcollina

@mcollina I see, so it would be helpful if we can use request.file() and have validation in the same time, instead of relying on buffers only to have validation to work properly.

Thank you!

xplodeart avatar Oct 22 '20 00:10 xplodeart

I see, so it would be helpful if we can use request.file() and have validation in the same time, instead of relying on buffers only to have validation to work properly.

Validation happens at a specific phase of a the request lifecycle, i.e. before the handler is executed. The way request.file() works is to have the server receive the file inside the handler execution, after validation was completed.

Maybe it's possible to perform it with the files stored on disk... wdyt @StarpTech?

mcollina avatar Oct 22 '20 06:10 mcollina

Sorry for the late response. I have no time to invest.

StarpTech avatar Dec 19 '20 12:12 StarpTech

No quite the same example but i made it work using this api spec: https://github.com/keycap-archivist/app/blob/master/src/assets/v2-spec.yaml#L81-L99

using this configuration: https://github.com/keycap-archivist/app/blob/master/src/app.ts#L39

@xplodeart can you test those specs?

zekth avatar Dec 19 '20 13:12 zekth

@zekth what about a dynamic example? Unfortunately requestBody doesn't seem to be allowed when setting a inline schema in app.ts (not an external file like you do).

Javarome avatar Sep 19 '21 10:09 Javarome

@zekth what about a dynamic example? Unfortunately requestBody doesn't seem to be allowed when setting a inline schema in app.ts (not an external file like you do).

requestBody is an OpenAPI naming, which is mapped in body in fastify schemas. I can try to put a dynamic example up when i'll have time.

zekth avatar Sep 19 '21 16:09 zekth

I have a same issue.

Evnironment

  • fastify 4.5.3
  • @fastify/multipart 7.1.2
  • @fastify/swagger 7.5.1

Problem

fastify/multipart cannot create fileupload widget on swagger.io document

Reproducable Repo

  • https://github.com/imjuni/maeum/blob/e8e94a8f6d80181281a863abd38a3a5ac7916c25/src/server/server.ts#L43 execute npm scripts npm run dev and open url http://localhost:7878/swagger.io in your browser

Clue

fastify_swagger_ref_error

  • Cannot found schemaId in @fastify/multipart() issue because prefix character '#'. I think that need fix documentation.

swagger_fileupload_widget

  • I think people want to myFile1 (swagger file upload widget disply), but raise validation error display because myFile1(include me) set type string. but swagger ui upload buffer type.

Something wrong in my code?

imjuni avatar Sep 07 '22 18:09 imjuni

Yes, this is exactly what I need! 🥹

Problem

fastify/multipart cannot create fileupload widget on swagger.io document

swagger_fileupload_widget

ningaro avatar Oct 16 '22 13:10 ningaro

I have a same issue.

Evnironment

  • fastify 4.5.3
  • @fastify/multipart 7.1.2
  • @fastify/swagger 7.5.1

Problem

fastify/multipart cannot create fileupload widget on swagger.io document

Reproducable Repo

  • https://github.com/imjuni/maeum/blob/e8e94a8f6d80181281a863abd38a3a5ac7916c25/src/server/server.ts#L43 execute npm scripts npm run dev and open url http://localhost:7878/swagger.io in your browser

Clue

fastify_swagger_ref_error

  • Cannot found schemaId in @fastify/multipart() issue because prefix character '#'. I think that need fix documentation.

swagger_fileupload_widget

  • I think people want to myFile1 (swagger file upload widget disply), but raise validation error display because myFile1(include me) set type string. but swagger ui upload buffer type.

Something wrong in my code?

I'm facing this exact issue. Is there any solution yet?

smamun19 avatar Oct 30 '22 22:10 smamun19

I manage to make it work like this

register multipart:

fastify.register(multipart);

schema.ts

export const createAppIntegrationSchema = {
	tags: ["integrations"],
	consumes: ["multipart/form-data"],
	body: {
		type: "object",
		properties: {
			name: { type: "string" },
			description: { type: "string" },
			icon_image: {
				type: "file",
			},
			demo_url: { type: "string" },
		},
	},
	response: {
		201: {
			description: "Successful response",
			type: "object",
			properties: {
				created_at: { type: "string" },
				updated_at: { type: "string" },
				name: { type: "string" },
				description: { type: "string" },
				icon_image: { type: "string" },
				demo_url: { type: "string" },
				is_enabled: { type: "boolean" },
			},
		},
		401: {
			type: "object",
			properties: {
				status: { type: "number", default: 401 },
				message: { type: "string" },
			},
		},
		500: {
			description: "Error response",
			type: "object",
			properties: {
				status: { type: "number", default: 500 },
				message: { type: "string" },
			},
		},
	},
};

and in your handler use it like this:

const data = await request.file();

lord007tn avatar Apr 16 '23 00:04 lord007tn

Could you send a PR to document this?

mcollina avatar Apr 16 '23 07:04 mcollina