kogosoftwarellc / open-api

A Monorepo of various packages to power OpenAPI in node
MIT License
889 stars 233 forks source link

[openapi-request-validator] Validating multipart/form-data requests #712

Open Envek opened 3 years ago

Envek commented 3 years ago

This is related, but different from #710. I'm not sure that there is a bug, maybe I just need use these packages differently as I'm using them at quite low level (with manual parsing of request body and etc).

The problem

In OpenAPI 3 multipart/form-data requests should be declared as objects in requestBody according to Describing Request Body / File Uploads docs:

openapi: 3.0.3
info:
  title: Sample Application API
  version: v1
paths:
  /fileAttachments:
    post:
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                someId:
                  type: string
                  nullable: false
                status:
                  type: string
                  nullable: true
                file:
                  type: string
                  format: binary
                  nullable: false
      responses:
        204:
          description: Success

I'm trying to validate such a request on AWS Lambda. Due to platform validations, I'm getting raw body, so I parse it with lambda-multipart-parser and validate with openapi-request-validator with code like this:

import OpenAPIRequestValidator } from "openapi-request-validator"
import { parse as parseMultipart } from "lambda-multipart-parser"

const { files, ...plainFields } = await parseMultipart(event)
const fileFields  = Object.fromEntries(files.map(file => [file.fieldname, file.content.toString()]))
data   = { ...plainFields, ...fileFields }

(new OpenAPIRequestValidator({…})).validateRequest({
      headers: headers,
      body: data,
      params: event.pathParameters,
      query: event.queryStringParameters,
    })

Which gives me following object as a body:

{
  someId: "deadbeef",
  status: "whatever",
  file: '�PNG\r\n…',
}

Expected result

openapi-request-validator validates request and doesn't pass requests with file or someId missing

Actual result

openapi-request-validator passes requests with missing non-nullable fields

Workaround

Then I went to openapi-request-validator source and found that it expects for multipart requests that all input fields should be declared as input formdata parameters instead of request body (see this test case)

So I tried to change schema in a way that openapi-request-validator should understand: replaced request body with parameters

apiSchema.yml with formData parameters ```yaml openapi: 3.0.3 info: title: Sample Application API version: v1 paths: /fileAttachments: post: parameters: - name: someId in: formData required: true schema: type: string - name: status in: formData required: false schema: type: string - name: file in: formData required: true schema: type: string format: binary responses: 204: description: Success ```

And success! Now it is being validated as expected.

But Swagger Editor tells me that this schema is now invalid:

Structural error at paths./fileAttachments.post.parameters.0.in
should be equal to one of the allowed values
allowedValues: path, query, header, cookie

(and 2 more errors)

To complete this workaround I had to preprocess schema, converting multipart bodies into formData parameters on schema load with code like this.

Some hacky TypeScript code ```typescript import yaml from "js-yaml" import { readFileSync } from "fs" import $RefParser from "@apidevtools/json-schema-ref-parser" import { OpenAPIV3 } from "openapi-types" type OpenApiHttpMethods = "get" | "post" | "put" | "delete" | "options" | "head" | "patch" | "trace" const apiSpec = yaml.safeLoad(readFileSync("./apiSchema.yaml").toString()) async function preprocessOpenapiSpec(apiSpec: OpenAPIV3.Document): Promise { const schema = await $RefParser.dereference(apiSpec) // Replace multipart request body with formData parameters for validation for (const methods of Object.values(schema.paths)) { for (const method of ["get", "post", "put", "delete", "options", "head", "patch", "trace"]) { const methodOptions = methods[method] if (!methodOptions) continue const reqBody = methodOptions?.requestBody if (reqBody?.content && "multipart/form-data" in reqBody.content) { const props = (reqBody.content["multipart/form-data"].schema)?.properties if (!props) continue methodOptions.parameters ||= [] for (const [name, subschema] of Object.entries(props)) { const required = ("nullable" in subschema) ? !subschema.nullable : false methodOptions.parameters.push({ name, in: "formData", required, schema: subschema }) } delete methodOptions?.requestBody } } } return schema } ```

Question

Is there a bug in openapi-request-validator? If so, can you point me where to start fixing it, so I can prepare a pull request (I'm not sure where to start, honestly)

Or should I somehow preprocess schema or body using some other package from this suite?


Originally posted by @Envek in https://github.com/kogosoftwarellc/open-api/issues/710#issuecomment-788128653

anthonysette commented 3 years ago

+1 I am having a problem with this as well. Any updates?