python-openapi / openapi-core

Openapi-core is a Python library that adds client-side and server-side support for the OpenAPI v3.0 and OpenAPI v3.1 specification.
BSD 3-Clause "New" or "Revised" License
286 stars 131 forks source link

[Bug]: Does Not Support Binary Response Types #854

Open its-hammer-time opened 2 weeks ago

its-hammer-time commented 2 weeks ago

Actual Behavior

I hacked together the code below so that I could test this library in isolation, but for context we are using it inside of a Pyramid app. When I run the case below, I get the following exception:

openapi_core.validation.response.exceptions.InvalidData: InvalidData: Value b'<pdf_content>' not valid for schema of type string: (<ValidationError: "b'<pdf_content>' is not of type 'string'">,)

If I then try to wrap it inside of a str(), I get is not of type 'binary' so I believe there is an issue with the formatter understanding the bytes as a string.

Expected Behavior

If I return a binary object (pdf file) it should pass validation.

Steps to Reproduce

import typing as t

from openapi_core import OpenAPI
from openapi_core.validation.request.datatypes import RequestParameters

class OpenAPIRequest:
    def __init__(self) -> None:
        self.parameters = RequestParameters(
            path={},
            query={},
            header={},
            cookie={},
        )

    @property
    def host_url(self) -> str:
        return "https://example.com"

    @property
    def path(self) -> str:
        return "/test"

    @property
    def path_pattern(self) -> str:
        return "/test"

    @property
    def method(self) -> str:
        return "get"

    @property
    def body(self) -> t.Optional[t.Union[bytes, str, t.Dict]]:
        return None

    @property
    def content_type(self) -> str:
        return ""

    @property
    def mimetype(self) -> str:
        return ""

class OpenAPIResponse:
    def __init__(self, data: any) -> None:
        self._data = data

    @property
    def data(self) -> t.Optional[bytes]:
        return self._data

    @property
    def status_code(self) -> int:
        return 200

    @property
    def content_type(self) -> str:
        return "application/pdf"

    @property
    def mimetype(self) -> str:
        return "application/pdf"

    @property
    def headers(self) -> t.Mapping[str, t.Any]:
        return {
            "Content-Type": "application/pdf",
            "Content-Disposition": "attachment; filename=invoice.pdf",
        }

if __name__ == "__main__":
    with open('example-pdf.pdf', 'rb') as f:
        pdf_content = f.read()

    open_api_spec = OpenAPI.from_dict({
        "openapi": "3.1.0",
        "info": {
            "title": "Test spec",
            "version": "1.0.0",
            "description": "Test spec",
        },
        "servers": [
            {"url": "https://example.com"},
        ],
        "paths": {
            "/test": {
                "get": {
                    "responses": {
                        "200": {
                            "description": "OK",
                            "content": {
                                "application/pdf": {
                                    "schema": {
                                        "type": "string",
                                        "format": "binary",
                                    },
                                },
                            },
                        },
                    },
                },
            }
        }
    })

    request = OpenAPIRequest()
    response = OpenAPIResponse(data=pdf_content)
    open_api_spec.validate_response(request=request, response=response)

OpenAPI Core Version

0.19.1

OpenAPI Core Integration

Natively using openapi-core

Affected Area(s)

Validation

References

No response

Anything else we need to know?

No response

Would you like to implement a fix?

None

its-hammer-time commented 2 weeks ago

I tried adding a deserializers for the mimetype, but it's complaining that it's no longer "binary".

    def deserialize_pdf(value: bytes) -> str:
        return value.decode("utf-8")

    open_api_config: Config = Config(
        extra_media_type_deserializers={
            "application/pdf": deserialize_pdf,
        }
    )

    open_api_spec = OpenAPI.from_dict({ ... }, config=open_api_config)

If it's binary the library complains that it's not a 'string' and if it's a string it complains that it's not 'binary'

p1c2u commented 1 week ago

Hi @its-hammer-time

thanks for the report. it's a known issue that will requires breaking change #647

p1c2u commented 16 hours ago

After further investigation the problem exist with Openapi 3.1.

If you change to openapi 3.0.3 it works 🤔