Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode error with validate_responses=True and multiple response types specified #1434

Closed
njeirath opened this issue Oct 6, 2021 · 6 comments

Comments

@njeirath
Copy link

njeirath commented Oct 6, 2021

Description

Sample repo demonstrating the issue.

With the following in my swagger definition:

paths:
  /document:
    get:
      x-openapi-router-controller: test.entry
      operationId: get_document
      responses:
        "200":
          description: The requested document in PDF format
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          description: Auth failure
          content:
            application/json:
              schema:
                type: object

And having validate_responses=True I'm receiving the following error:

Traceback (most recent call last):                                                                                                                                                                                                                            
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/flask/app.py", line 2447, in wsgi_app                                                                                                                                         
    response = self.full_dispatch_request()                                                                                                                                                                                                                   
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/flask/app.py", line 1821, in handle_user_exception                                                                                                                           
    reraise(exc_type, exc_value, tb)                                                                                                                                                                                                                          
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/flask/_compat.py", line 39, in reraise                                                                                                                                        
    raise value
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/decorators/decorator.py", line 68, in wrapper
    response = function(request)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/decorators/uri_parsing.py", line 149, in wrapper
    response = function(request)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/decorators/response.py", line 110, in wrapper
    return _wrapper(request, response)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/decorators/response.py", line 91, in _wrapper
    self.validate_response(
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/decorators/response.py", line 52, in validate_response
    data = self.operation.json_loads(data)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/operations/abstract.py", line 455, in json_loads
    return self.api.json_loads(data)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/apis/abstract.py", line 456, in json_loads
    return self.jsonifier.loads(data)
  File "/Users/user/.virtualenvs/flask-test/lib/python3.8/site-packages/connexion/jsonifier.py", line 64, in loads
    data = data.decode()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd3 in position 10: invalid continuation byte

It appears having the 401 response type set to application/json is causing the response validator to try to parse my PDF as JSON. I'm curious if the way I'm returning the PDF may be causing this issue or if this is genuinely a problem so wanted to post an issue to get some feedback.

Expected behaviour

Calling the endpoint should not produce an error.

Actual behaviour

Calling the endpoint is causing the above error.

Steps to reproduce

Run the linked sample repo with FLASK_APP=test flask run
then curl http://localhost:5000/document

Additional info:

Output of the commands:

  • python --version
    Python 3.8.5
  • pip show connexion | grep "^Version\:"
    Version: 2.9.0
@languitar
Copy link
Contributor

languitar commented Oct 6, 2021

I just stumbled across the same issue. What happens here is that ResponseValidator is instantiated with a single mime type passed to its mimetype parameter and that mime type is then used to determine if all mime types (that single one) are JSON before attempting validation. The single mime type is computed by get_mimetype in abstract.py:

    def get_mimetype(self):
        """
        If the endpoint has no 'produces' then the default is
        'application/json'.

        :rtype str
        """
        if all_json(self.produces):
            try:
                return self.produces[0]
            except IndexError:
                return DEFAULT_MIMETYPE
        elif len(self.produces) == 1:
            return self.produces[0]
        else:
            return DEFAULT_MIMETYPE

A method with more than one produced response type including a non-JSON type will always receive DEFAULT_MIMETYPE, which is JSON again. Therefore, the ResponseValidator receives a wrong abstraction of reality and always assumes JSON.

As far as I understand the structure, a ResponseValidator is created statically at application start. At that point in time, there's never a chance to decide on a single mime type to validate in case of content negotiation with multiple possible response types that are selected at runtime. Something is broken in how reality is modeled for validation here.

@languitar
Copy link
Contributor

Here's a hacky workaround that trusts the returned content type by an operation to disable validation for non-JSON responses:

class ResponseValidatorWithBinarySupport(ResponseValidator):
    def validate_response(self, data, status_code, headers, url):
        # Skip validation for non JSON-like reponses such as images and files
        if not all_json([headers.get("Content-Type", "application/json")]):
            return
        super().validate_response(data, status_code, headers, url)

# ...

 app.add_api(
    # ...
    validator_map={"response": ResponseValidatorWithBinarySupport},
)

@njeirath
Copy link
Author

njeirath commented Oct 6, 2021

Thanks @languitar I'll give that a shot.

@njeirath
Copy link
Author

njeirath commented Oct 7, 2021

Can confirm the above workaround was successful. Thanks again.

@languitar
Copy link
Contributor

Looks like this was already tracked at #860

@RobbeSneyders
Copy link
Member

Thanks for the report.
Closing as duplicate of #860.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants