connexion icon indicating copy to clipboard operation
connexion copied to clipboard

openapi.json cannot be delivered after calling an endpoint with a $ref request body

Open bene1618 opened this issue 11 months ago • 12 comments

Problem description

When creating a Connexion app with an OpenAPI spec which uses $ref to reference the request body schema, requesting /openapi.json leads to an error after the endpoint which uses $ref in the specification has been called.

Minimal example

  • Install connexion["swagger-ui"] (and uvicorn for serving), and paste the files below.
  • Run the app with uvicorn run:app
  • Navigate to Swagger UI: http://localhost:8000/ui and confirm that everything looks good
  • Call the /greeting endpoint
  • Reload Swagger UI, which leads to an error (see below)

Contents of openapi.yaml:

openapi: "3.0.0"
info:
  title: Greeting application
  version: 0.0.1
paths:
  /greeting:
    post:
      operationId: run.post_greeting
      responses:
        '200':
          description: "Greeting response"
          content:
            text/plain:
              schema:
                type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Test'
components:
  schemas:
    Test:
      type: object
      properties:
        name:
          type: string
          description: A name

Contents of run.py:

from connexion import AsyncApp

app = AsyncApp(__name__)

def post_greeting(body):
    return f"Hello {body['name']}", 200

app.add_api("openapi.yaml")

Error message

connexion.exceptions.InvalidSpecification: {'content': {'application/json': {'schema': {'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'A name'}}, 'components': {'schemas': {'Test': {'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'A name'}}}}}}}}} is not valid under any of the given schemas

Failed validating 'oneOf' in schema['properties']['paths']['patternProperties']['^\\/']['patternProperties']['^(get|put|post|delete|options|head|patch|trace)$']['properties']['requestBody']:
    {'oneOf': [{'$ref': '#/definitions/RequestBody'},
               {'$ref': '#/definitions/Reference'}]}

On instance['paths']['/greeting']['post']['requestBody']:
    {'content': {'application/json': {'schema': {'type': 'object',
                                                 'properties': {'name': {'type': 'string',
                                                                         'description': 'A '
                                                                                        'name'}},
                                                 'components': {'schemas': {'Test': {'type': 'object',
                                                                                     'properties': {'name': {'type': 'string',
                                                                                                             'description': 'A '
                                                                                                                            'name'}}}}}}}}}

More details

The issue seems to be that #2002 returns the processed OpenAPI spec, which contains a components key inside the schema, so that the OpenAPI schema validator complains (correctly) about an invalid spec and /openapi.json refuses to deliver the spec. In fact, some testing shows that reverting bb48fb3e3d3866977777eb4bfcb2c4f502efd4ca fixes the issue.

I'm not really sure but this might be related to the discussion in #1829.

Sample test

Something like the following might work as an integration test with the openapi.yaml from above (I used this for finding bb48fb3e3d3866977777eb4bfcb2c4f502efd4ca; maybe this helps):

from connexion import AsyncApp
from os import path

def post_greeting(body):
    return f"Hello {body['name']}", 200

def test_esoteric_error():
    app = AsyncApp(__name__)
    app.add_api(path.join(path.dirname(__file__), "fixtures/simple/esoteric-error.yaml"))

    client = app.test_client()

    response = client.get("/openapi.json")
    assert response.status_code == 200

    response = client.post("/greeting", data={"name": "XYZ"}, headers={"Content-Type": "application/json"})
    # assert response.status_code == 200 <-- returns Bad Request for some reason I didn't investigate; however it's sufficient to reproduce the error

    response = client.get("/openapi.json")
    assert response.status_code == 200

bene1618 avatar Jan 17 '25 15:01 bene1618

I have a similar error - spec validation failing with components appearing out of nowhere alongside properties and type in requestBody - but I don't have a $ref in requestBody, only in responses" So I don't think that the $ref in requestBody is the cause.

skibbi avatar Jan 21 '25 12:01 skibbi

I have the same problem with the migration to connection 3

jhuot9 avatar Jan 22 '25 20:01 jhuot9

Same here. Looks like this was introduced in the latest release. As a workaround, downgrading from connexion==3.2.0 to connexion==3.1.0 resolved it for me.

my-master avatar Feb 03 '25 23:02 my-master

Can confirm I've hit this with Connexion 3.2.0 as well. Rolling back to Connexion 3.1.0 fixes it for now.

mweigel avatar Feb 04 '25 02:02 mweigel

Rolling back to 3.1.0 helped us too with an inconsistent validation failure due to a supposed recursion issue when using $ref.

Failed validating 'oneOf' in schema['properties']['paths']['patternProperties']['^\\/']['patternProperties']['^(get|put|post|delete|options|head|patch|trace)$']['properties']['requestBody']:
    {'oneOf': [{'$ref': '#/definitions/RequestBody'},
               {'$ref': '#/definitions/Reference'}]}

On instance['paths']['/sample']['post']['requestBody']:
{
  "required": True,
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "description": "...",
        "properties": {...},
        "components": {
          "parameters": {...},
          "schemas": {...},
          "requestBodies": {
            "Sample": {
              "required": True,
              "content": "<Recursion on dict with id=140649069447936>"
            },
            ...
          },
          "responses": {...}
        }
      }
    }
  }
}

Simplified version of the spec that was used:

openapi: 3.0.0
info:
  title: sample
  version: 0.0.1
paths:
  /sample:
    post:
      operationId: sample
      requestBody:
        $ref: '#/components/requestBodies/Sample'
      responses:
        200:
          $ref: '#/components/responses/Sample'
components:
  schemas:
    Sample:
      type: object
      properties:
        example:
          type: string
  requestBodies:
    Sample:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Sample'
  responses:
    Sample:
      description: Response of sample endpoint
      content:
        application/json:
          schema:
            type: object

M4C4R avatar Feb 11 '25 12:02 M4C4R

I have run into the same issue, rolling back to 3.1.0 fixes the problem. If I have time, I will look into this tomorrow to see if I can find any clues.

wemcouncil avatar Feb 19 '25 19:02 wemcouncil

In my situation, the (second) spec being validated has changed such the $ref in 'requestBody' in the working (first) validation has been replaced by the full expansion of that requestBody ref / schema. Then the rest of the data structure matches the original working one.

notably, the full expansion also includes the 'components' and 'securitySchemes' sections of my original YAML specification instead of just the schema that defines the requestBody. So yes, some sort of recursion...

the original YAMl is certainly ok, otherwise nothing would ever work.

The spec it is being validated against expects a $ref or a fully compliant RequestBody definition - so something is re-parsing(?) the original YAML file improperly and adding things to the def of the schema used for the requestBody

wemcouncil avatar Feb 20 '25 08:02 wemcouncil

We have the same issue. We host our service in a Docker container. Running it locally works fine. The issue only appears when deployed behind our reverse proxy.

alfechner avatar Feb 26 '25 10:02 alfechner

Also having this problem with connexion 3.2.0 on Python 3.13.2-slim.

We also always use Vacuum (https://github.com/daveshanley/vacuum) to lint the OpenAPI file in the build process. There are no reported problems and I suspect this connexion error is an error. It also seems to comply with the examples in Swagger's own Describing Responses documentation (https://swagger.io/docs/specification/v3_0/describing-responses/).

The specific area of complaint relates to this block in our OpenAPI YAML file (which also uses a reference in #/components): -

  /version:
    get:
      summary: Gets the Account Server version
      tags:
      - state
      operationId: app.api_state.get_version
      x-semantic-name: getVersion
      security: []
      responses:
        "400":
          description: >
            Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/as_error'
        "401":
          description: >
            No authorisation token provided
        "403":
          description: >
            You're not authorised to use this path
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/as_error'
        "200":
          description: >
            The Account Server version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/state_get_version_response'

Workaround

I can also confirm that rolling back to connexion 3.1.0 resolves the issue, so we too are now stuck on 3.1.0.

alanbchristie avatar Apr 10 '25 09:04 alanbchristie

I also saw this and downgrading to 3.1.0 resolved it. This issue does not instill me with great confidence in this library going forward.

mivade avatar May 02 '25 17:05 mivade

I have also encountered this issue,

after investigating I have found that the following line is responsible for injecting the components in the schema

https://github.com/spec-first/connexion/blob/a1c53db7a74d0fbbc22913faf70bc3d1c1b08ee2/connexion/operations/openapi.py#L140C5-L140C19

    def with_definitions(self, schema: dict):
        if self.components:
            schema.setdefault("schema", {})
            schema["schema"]["components"] = self.components
        return schema

I have confirmed (at least in my case) that commenting the schema["schema"]["components"] = self.components fixes the issue.

However, without knowing the original intent of this line, I am unable to know why this line was added in the first place.

Does anybody have any insight on why we would be needing to inject the components in the schemas of request bodies?

f-atwi avatar Jun 09 '25 11:06 f-atwi

Same is happening for me! Downgrading to 3.1.0 also resolved it.

felipesetti avatar Jun 13 '25 07:06 felipesetti

Same issue here Are there any news on this ?

SeaweedbrainCY avatar Jul 14 '25 14:07 SeaweedbrainCY

greetings to the community, please test the fix from PR https://github.com/spec-first/connexion/pull/2069 on your use case and comment here or there.

I don't know if y'all have a watch set on this issue so I'm tagging the commenters on this issue, sorry if you get multiple notifications: @SeaweedbrainCY @felipesetti @f-atwi @mivade @alanbchristie @alfechner @wemcouncil @skibbi @jhuot9 @my-master

chrisinmtown avatar Jul 22 '25 10:07 chrisinmtown

Sorry @chrisinmtown for the very late replay.

I do confirm #2069 fixes my issue on my use case. I'll conduct extensive tests and let you know if the issue comes again, but from now, this is working fine !

SeaweedbrainCY avatar Aug 15 '25 03:08 SeaweedbrainCY

I applied the patch from #2069 manually to 3.2.0 and the error persists in my case. I added a print("with_definitions called") to connexion to check the updated code is being executed and it is.

INFO:     Application startup complete.
INFO:     127.0.0.1:41678 - "GET /ingest/v1/ui/ HTTP/1.1" 200 OK
INFO:     127.0.0.1:41678 - "GET /ingest/v1/openapi.json HTTP/1.1" 200 OK
with_definitions called
DEBUG: /ingest/v1/jobs validating parameters...
with_definitions called
with_definitions called
with_definitions called
with_definitions called
with_definitions called
with_definitions called
with_definitions called
INFO:     127.0.0.1:41678 - "POST /ingest/v1/jobs HTTP/1.1" 201 Created
INFO:     127.0.0.1:41678 - "GET /ingest/v1/ui/ HTTP/1.1" 200 OK
ERROR: <InvalidSpecification: "{'description': 'Success', ...
# many lines follow...

stativ avatar Aug 21 '25 16:08 stativ

Is there any traction on this? Do we know whether this is a problem in 3.3.0? I'm on 3.1.0, blocked from moving forward because of this.

alanbchristie avatar Oct 23 '25 11:10 alanbchristie

I believe this was resolved in 3.3.0 (https://github.com/spec-first/connexion/pull/2089 must have made it into that release), as it seems to be working for me now.

burnettk avatar Oct 23 '25 11:10 burnettk

I just tested this. Created a new Python 3.12 venv, installed connexion[swagger-ui,uvicorn]==3.3.0, created the two files from @bene1618 and followed the procedure. I can POST to the /greeting endpoint using the Swagger UI as many times as I like, and reload the Swagger UI similarly. So as far as I can tell, this is fixed. I guess that PR #2089 solved this? I see that PR references ("Fixes") this issue, yet this remains open. I hope @RobbeSneyders or @Ruwann will close this issue and mark it as fixed.

chrisinmtown avatar Oct 23 '25 12:10 chrisinmtown

Thanks for confirming. This issue was indeed referenced as fixed inPR #2089 but probably not in the right format for Github to automatically pick it up.

Ruwann avatar Oct 23 '25 12:10 Ruwann