spec-first / connexion

Connexion is a modern Python web framework that makes spec-first and api-first development easy.
https://connexion.readthedocs.io/en/latest/
Apache License 2.0
4.47k stars 758 forks source link

Traceback checking whether encoding: is json (mishandling of `contentType: image/png, image/jpeg`) #1722

Open danielbprice opened 1 year ago

danielbprice commented 1 year ago

Description

We are developing an application which accepts multipart form data for a file upload of an image. We have:

paths:
  /assessment:
    post:
      summary: "Submit image for assessment"
      description: Redacted
      security:
        - apiKeyAuth: []
      operationId: REDACTED.wsgi_operations.assessment_post
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - image
              properties:
                image:
                  description: "Image data"
                  type: string
                  format: binary
            encoding:
              image:
                contentType: image/png, image/jpeg

(I have omitted some fields from this spec for brevity; they appear in the traceback below but are not relevant here)

Expected behaviour

We constructed a test case in which we passed non-file data to image, to make sure the endpoint would cleanly handle this case, essentially:

curl --header 'X-Auth: REDACTED' --form 'image=hello' http://localhost:8080/assessment
                                         ^^^^^^^^^^^

Actual behaviour

We see a traceback. It seems that the logic which is checking to see encoding.image.contentType is JSON-y is not handling the case in which there is a comma-separated list of MIME types. This is permitted by the OpenAPI 3.0.3 spec as per "Encoding Object" (The Content-Type for encoding a specific property. Default value depends on the property type: for object - application/json; for array – the default is defined based on the inner type; for all other cases the default is application/octet-stream. The value can be a specific media type (e.g. application/json), a wildcard media type (e.g. image/*), or a comma-separated list of the two types.)

Indeed, changing the contentType: image/png, image/jpeg line to contentType: image/* makes the problem go away.

│ /home/dp/REDACTED/python3.10/site-packages/connexion/decorators/uri_parsing.py:18 │
│ 3 in resolve_form                                                                                │
│                                                                                                  │
│   180 │   │   │   │   self._resolve_param_duplicates(form_data[k], encoding, 'form')             │
│   181 │   │   │   if defn and defn["type"] == "array":                                           │
│   182 │   │   │   │   form_data[k] = self._split(form_data[k], encoding, 'form')                 │
│ ❱ 183 │   │   │   elif 'contentType' in encoding and utils.all_json([encoding.get('contentType   │
│   184 │   │   │   │   form_data[k] = json.loads(form_data[k])                                    │
│   185 │   │   return form_data                                                                   │
│   186                                                                                            │
│                                                                                                  │
│ ╭────────────────────────────────────────── locals ──────────────────────────────────────────╮   │
│ │      defn = {'description': 'Image data', 'type': 'string', 'format': 'binary'}            │   │
│ │  encoding = {'contentType': 'image/png, image/jpeg'}                                       │   │
│ │ form_data = {'image_ts': '2023-07-20T19:10:07+00:00', 'image_group': '', 'image': 'hello'} │   │
│ │         k = 'image'                                                                        │   │
│ │      self = <OpenAPIURIParser>                                                             │   │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────╯   │
│                                                                                                  │
│ /home/dp/REDACTED/python3.10/site-packages/connexion/utils.py:165 in all_json     │
│                                                                                                  │
│   162 │   >>> all_json(['application/json', 'application/x.custom+json'])                        │
│   163 │   True                                                                                   │
│   164 │   """                                                                                    │
│ ❱ 165 │   return all(is_json_mimetype(mimetype) for mimetype in mimetypes)                       │
│   166                                                                                            │
│   167                                                                                            │
│   168 def is_nullable(param_def):                                                                │
│                                                                                                  │
│ ╭─────────────── locals ────────────────╮                                                        │
│ │ mimetypes = ['image/png, image/jpeg'] │                                                        │
│ ╰───────────────────────────────────────╯                                                        │
│                                                                                                  │
│ /home/dp/REDACTED/python3.10/site-packages/connexion/utils.py:165 in <genexpr>    │
│                                                                                                  │
│   162 │   >>> all_json(['application/json', 'application/x.custom+json'])                        │
│   163 │   True                                                                                   │
│   164 │   """                                                                                    │
│ ❱ 165 │   return all(is_json_mimetype(mimetype) for mimetype in mimetypes)                       │
│   166                                                                                            │
│   167                                                                                            │
│   168 def is_nullable(param_def):                                                                │
│                                                                                                  │
│ ╭────────────────────── locals ───────────────────────╮                                          │
│ │       .0 = <list_iterator object at 0x7ff058e16ce0> │                                          │
│ │ mimetype = 'image/png, image/jpeg'                  │                                          │
│ ╰─────────────────────────────────────────────────────╯                                          │
│                                                                                                  │
│ /home/dp/REDACTED/python3.10/site-packages/connexion/utils.py:139 in              │
│ is_json_mimetype                                                                                 │
│                                                                                                  │
│   136 │   :type mimetype: str                                                                    │
│   137 │   :rtype: bool                                                                           │
│   138 │   """                                                                                    │
│ ❱ 139 │   maintype, subtype = mimetype.split('/')  # type: str, str                              │
│   140 │   return maintype == 'application' and (subtype == 'json' or subtype.endswith('+json')   │
│   141                                                                                            │
│   142                                                                                            │
│                                                                                                  │
│ ╭────────────── locals ──────────────╮                                                           │
│ │ mimetype = 'image/png, image/jpeg' │                                                           │
│ ╰────────────────────────────────────╯                                                           │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: too many values to unpack (expected 2)    <-------------------------

Steps to reproduce

Additional info:

Output of the commands:

danielbprice commented 1 year ago

I found by experiment that changing line 183 of connexion/decorators/uri_parsing.py

            elif 'contentType' in encoding and utils.all_json([x.strip() for x in encoding.get('contentType').split(',')]):
                  form_data[k] = json.loads(form_data[k])

Seemed to do the trick, but I don't know if other instances are lurking in the code.

My reading of the code on main says that this problem is still there, although I have not tested against it.