Description
I have a simple test case with incorrect headers that causes a 500 error. Although the cause is in the test, it is something that could happen in production with a public facing API built on Connexion. I have these concerns/opinions:
- 500 errors are pretty tough to manage, for both provider and consumer, since one should not return too much information about the cause of the error.
- By convention, they should be pretty rare to see, and ideally never caused by invalid input. i.e. as a general matter, validations should prevent most 500's caused by invalid input.
- This case should be treated as a validation error
- Question: Where do the details related to the 500 go, when connexion experiences them ? I could only see the details in debug mode. In a production environment, details related to 500 errors must be logged.
- I could not find information about configuring connexion logging in to docs.
Thanks for your work here, BTW, I've been using connexion for about 2 years, and am just switching to V3..
Code below.. The 'get' test function does not see an error, but the 'post' does.
Test app.py:
`import connexion
from flask import jsonify
from datetime import datetime
from connexion.middleware import MiddlewarePosition
from starlette.middleware.cors import CORSMiddleware
""" In-memory storage for todos """
todos = {}
next_id = 1
""" API endpoints implementation """
def get_todos():
"""Retrieve all todos"""
return jsonify(list(todos.values()))
def create_todo(body):
"""Create a new todo"""
global next_id
new_todo = {
'id': next_id,
'task': body['task'],
'completed': body.get('completed', False)
}
todos[next_id] = new_todo
next_id += 1
return jsonify(new_todo), 201
def get_todo(todo_id):
"""Retrieve a specific todo"""
if todo_id not in todos:
return jsonify({'error': 'Todo not found'}), 404
return jsonify(todos[todo_id])
""" Create the Connexion app """
app = connexion.FlaskApp(name, specification_dir='./')
app.add_middleware(
CORSMiddleware,
position=MiddlewarePosition.BEFORE_EXCEPTION,
allow_origins=[""],
allow_credentials=True,
allow_methods=[""],
allow_headers=["*"],
)
with app.app.app_context():
app.add_api('swagger.yaml')
""" Add some sample data """
todos[1] = {'id': 1, 'task': 'Learn Connexion', 'completed': False}
todos[2] = {'id': 2, 'task': 'Build REST API', 'completed': True}
if name == 'main':
app.run(port=5000, debug=True)`
Sample pytest. Note that the header has two definitions for content-type, which is the input error in the test:
`import pytest
from starlette.testclient import TestClient
from app import app # Import the Connexion app from your main file
import json
""" Test fixtures """
@pytest.fixture
def client():
"""Create a test client for the app"""
# Use Starlette's TestClient with the Connexion app
with TestClient(app) as client:
# Reset the todos dictionary before each test
global todos, next_id
from app import todos, next_id
todos.clear()
todos[1] = {'id': 1, 'task': 'Test task 1', 'completed': False}
todos[2] = {'id': 2, 'task': 'Test task 2', 'completed': True}
next_id = 3
yield client
@pytest.fixture
def json_headers():
"""Common headers for JSON requests"""
return {
'content-type': 'application/json',
'X-ApiKey': 'ApiKey',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
def test_get_all_todos(client):
"""Test retrieving all todos"""
response = client.get('/todos')
assert response.status_code == 200
assert len(response.json()) == 2 # Should match initial test data
assert response.json()[0]['id'] == 1
assert response.json()[0]['task'] == 'Test task 1'
assert response.json()[1]['completed'] == True
def test_create_todo_success(client, json_headers):
"""Test creating a new todo with valid data"""
new_todo = {
'task': 'New test task',
'completed': False
}
response = client.post(
'/todos',
data=json.dumps(new_todo),
headers=json_headers
)
assert response.status_code == 201
assert response.json()['id'] in [1, 2, 3] # Should be next available ID
assert response.json()['task'] == 'New test task'
assert response.json()['completed'] == False
`
yaml:
`
openapi: 3.0.0
info:
title: Todo List API
version: 1.0.0
description: A simple Todo List management API
paths:
/todos:
get:
summary: List all todos
operationId: app.get_todos
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Todo'
post:
summary: Create a new todo
operationId: app.create_todo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoInput'
responses:
'201':
description: Todo created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
components:
schemas:
Todo:
type: object
properties:
id:
type: integer
task:
type: string
completed:
type: boolean
required:
- id
- task
TodoInput:
type: object
properties:
task:
type: string
completed:
type: boolean
required:
- task
`
Output: <connexion==3.2.0, Flask==3.1.0>
Response.text for post test:
{"type": "about:blank", "title": "Internal Server Error", "detail": "The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.", "status": 500}
stdout in debug mode:
File "....venv/lib/python3.13/site-packages/connexion/lifecycle.py", line 120, in get_body
if is_json_mimetype(self.content_type):
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "....venv/lib/python3.13/site-packages/connexion/utils.py", line 161, in is_json_mimetype
maintype, subtype = mimetype.split("/") # type: str, str
^^^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)