Skip to content

Passing in invalid headers to an API causes 500 error #2040

Open
@johnbyrne7

Description

@johnbyrne7

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions