You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ka...@apache.org on 2020/08/31 13:37:27 UTC
[airflow] branch master updated: Unify error messages and complete
type field in response (#10333)
This is an automated email from the ASF dual-hosted git repository.
kamilbregula pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/master by this push:
new aa2db70 Unify error messages and complete type field in response (#10333)
aa2db70 is described below
commit aa2db70494ee9c6369f601bbd5845bdc4ccfef92
Author: Ephraim Anierobi <sp...@gmail.com>
AuthorDate: Mon Aug 31 14:36:52 2020 +0100
Unify error messages and complete type field in response (#10333)
Co-authored-by: Kamil BreguĊa <mi...@users.noreply.github.com>
---
.../api_connexion/endpoints/connection_endpoint.py | 21 ++--
airflow/api_connexion/endpoints/dag_endpoint.py | 4 +-
.../api_connexion/endpoints/dag_run_endpoint.py | 7 +-
.../endpoints/import_error_endpoint.py | 5 +-
airflow/api_connexion/exceptions.py | 113 ++++++++++++++++++---
airflow/api_connexion/openapi/v1.yaml | 29 ++++++
airflow/www/extensions/init_views.py | 4 +-
tests/api/auth/backend/test_basic_auth.py | 15 +--
.../endpoints/test_connection_endpoint.py | 34 +++++--
tests/api_connexion/endpoints/test_dag_endpoint.py | 3 +-
.../endpoints/test_dag_run_endpoint.py | 26 +++--
.../endpoints/test_event_log_endpoint.py | 3 +-
.../endpoints/test_extra_link_endpoint.py | 8 +-
.../endpoints/test_import_error_endpoint.py | 8 +-
tests/api_connexion/endpoints/test_log_endpoint.py | 5 +-
.../api_connexion/endpoints/test_pool_endpoint.py | 53 ++++++----
.../endpoints/test_variable_endpoint.py | 7 +-
tests/test_utils/api_connexion_utils.py | 4 +-
18 files changed, 264 insertions(+), 85 deletions(-)
diff --git a/airflow/api_connexion/endpoints/connection_endpoint.py b/airflow/api_connexion/endpoints/connection_endpoint.py
index daab4f5..ea4a91d 100644
--- a/airflow/api_connexion/endpoints/connection_endpoint.py
+++ b/airflow/api_connexion/endpoints/connection_endpoint.py
@@ -41,7 +41,10 @@ def delete_connection(connection_id, session):
"""
connection = session.query(Connection).filter_by(conn_id=connection_id).one_or_none()
if connection is None:
- raise NotFound('Connection not found')
+ raise NotFound(
+ 'Connection not found',
+ detail=f"The Connection with connection_id: `{connection_id}` was not found",
+ )
session.delete(connection)
return NoContent, 204
@@ -54,7 +57,10 @@ def get_connection(connection_id, session):
"""
connection = session.query(Connection).filter(Connection.conn_id == connection_id).one_or_none()
if connection is None:
- raise NotFound("Connection not found")
+ raise NotFound(
+ "Connection not found",
+ detail=f"The Connection with connection_id: `{connection_id}` was not found",
+ )
return connection_collection_item_schema.dump(connection)
@@ -87,9 +93,12 @@ def patch_connection(connection_id, session, update_mask=None):
non_update_fields = ['connection_id', 'conn_id']
connection = session.query(Connection).filter_by(conn_id=connection_id).first()
if connection is None:
- raise NotFound("Connection not found")
+ raise NotFound(
+ "Connection not found",
+ detail=f"The Connection with connection_id: `{connection_id}` was not found",
+ )
if data.get('conn_id', None) and connection.conn_id != data['conn_id']:
- raise BadRequest("The connection_id cannot be updated.")
+ raise BadRequest(detail="The connection_id cannot be updated.")
if update_mask:
update_mask = [i.strip() for i in update_mask]
data_ = {}
@@ -97,7 +106,7 @@ def patch_connection(connection_id, session, update_mask=None):
if field in data and field not in non_update_fields:
data_[field] = data[field]
else:
- raise BadRequest(f"'{field}' is unknown or cannot be updated.")
+ raise BadRequest(detail=f"'{field}' is unknown or cannot be updated.")
data = data_
for key in data:
setattr(connection, key, data[key])
@@ -125,4 +134,4 @@ def post_connection(session):
session.add(connection)
session.commit()
return connection_schema.dump(connection)
- raise AlreadyExists("Connection already exist. ID: %s" % conn_id)
+ raise AlreadyExists(detail="Connection already exist. ID: %s" % conn_id)
diff --git a/airflow/api_connexion/endpoints/dag_endpoint.py b/airflow/api_connexion/endpoints/dag_endpoint.py
index edf2327..1fddb4b 100644
--- a/airflow/api_connexion/endpoints/dag_endpoint.py
+++ b/airflow/api_connexion/endpoints/dag_endpoint.py
@@ -41,7 +41,7 @@ def get_dag(dag_id, session):
dag = session.query(DagModel).filter(DagModel.dag_id == dag_id).one_or_none()
if dag is None:
- raise NotFound("DAG not found")
+ raise NotFound("DAG not found", detail=f"The DAG with dag_id: {dag_id} was not found")
return dag_schema.dump(dag)
@@ -53,7 +53,7 @@ def get_dag_details(dag_id):
"""
dag: DAG = current_app.dag_bag.get_dag(dag_id)
if not dag:
- raise NotFound("DAG not found")
+ raise NotFound("DAG not found", detail=f"The DAG with dag_id: {dag_id} was not found")
return dag_detail_schema.dump(dag)
diff --git a/airflow/api_connexion/endpoints/dag_run_endpoint.py b/airflow/api_connexion/endpoints/dag_run_endpoint.py
index 0a66b06..0bba126 100644
--- a/airflow/api_connexion/endpoints/dag_run_endpoint.py
+++ b/airflow/api_connexion/endpoints/dag_run_endpoint.py
@@ -52,7 +52,10 @@ def get_dag_run(dag_id, dag_run_id, session):
"""
dag_run = session.query(DagRun).filter(DagRun.dag_id == dag_id, DagRun.run_id == dag_run_id).one_or_none()
if dag_run is None:
- raise NotFound("DAGRun not found")
+ raise NotFound(
+ "DAGRun not found",
+ detail=f"DAGRun with DAG ID: '{dag_id}' and DagRun ID: '{dag_run_id}' not found",
+ )
return dagrun_schema.dump(dag_run)
@@ -195,7 +198,7 @@ def post_dag_run(dag_id, session):
Trigger a DAG.
"""
if not session.query(DagModel).filter(DagModel.dag_id == dag_id).first():
- raise NotFound(f"DAG with dag_id: '{dag_id}' not found")
+ raise NotFound(title="DAG not found", detail=f"DAG with dag_id: '{dag_id}' not found")
post_body = dagrun_schema.load(request.json, session=session)
dagrun_instance = (
diff --git a/airflow/api_connexion/endpoints/import_error_endpoint.py b/airflow/api_connexion/endpoints/import_error_endpoint.py
index 12baeda..10ab501 100644
--- a/airflow/api_connexion/endpoints/import_error_endpoint.py
+++ b/airflow/api_connexion/endpoints/import_error_endpoint.py
@@ -38,7 +38,10 @@ def get_import_error(import_error_id, session):
error = session.query(ImportError).filter(ImportError.id == import_error_id).one_or_none()
if error is None:
- raise NotFound("Import error not found")
+ raise NotFound(
+ "Import error not found",
+ detail=f"The ImportError with import_error_id: `{import_error_id}` was not found",
+ )
return import_error_schema.dump(error)
diff --git a/airflow/api_connexion/exceptions.py b/airflow/api_connexion/exceptions.py
index 7e34f31..a36257c 100644
--- a/airflow/api_connexion/exceptions.py
+++ b/airflow/api_connexion/exceptions.py
@@ -16,48 +16,135 @@
# under the License.
from typing import Dict, Optional
-from connexion import ProblemException
+import werkzeug
+from connexion import FlaskApi, ProblemException, problem
+
+from airflow import version
+
+doc_link = f'https://airflow.apache.org/docs/{version.version}/stable-rest-api-ref.html'
+if 'dev' in version.version:
+ doc_link = "https://airflow.readthedocs.io/en/latest/stable-rest-api-ref.html"
+
+EXCEPTIONS_LINK_MAP = {
+ 400: f"{doc_link}#section/Errors/BadRequest",
+ 404: f"{doc_link}#section/Errors/NotFound",
+ 401: f"{doc_link}#section/Errors/Unauthenticated",
+ 409: f"{doc_link}#section/Errors/AlreadyExists",
+ 403: f"{doc_link}#section/Errors/PermissionDenied",
+ 500: f"{doc_link}#section/Errors/Unknown",
+}
+
+
+def common_error_handler(exception):
+ """
+ Used to capture connexion exceptions and add link to the type field
+ :type exception: Exception
+ """
+ if isinstance(exception, ProblemException):
+
+ link = EXCEPTIONS_LINK_MAP.get(exception.status, None)
+ if link:
+ response = problem(
+ status=exception.status,
+ title=exception.title,
+ detail=exception.detail,
+ type=link,
+ instance=exception.instance,
+ headers=exception.headers,
+ ext=exception.ext,
+ )
+ else:
+ response = problem(
+ status=exception.status,
+ title=exception.title,
+ detail=exception.detail,
+ type=exception.type,
+ instance=exception.instance,
+ headers=exception.headers,
+ ext=exception.ext,
+ )
+ else:
+ if not isinstance(exception, werkzeug.exceptions.HTTPException):
+ exception = werkzeug.exceptions.InternalServerError()
+
+ response = problem(title=exception.name, detail=exception.description, status=exception.code)
+
+ return FlaskApi.get_response(response)
class NotFound(ProblemException):
"""Raise when the object cannot be found"""
- def __init__(self, title='Object not found', detail=None):
- super().__init__(status=404, title=title, detail=detail)
+ def __init__(
+ self, title: str = 'Not Found', detail: Optional[str] = None, headers: Optional[Dict] = None, **kwargs
+ ):
+ super(NotFound, self).__init__(
+ status=404, type=EXCEPTIONS_LINK_MAP[404], title=title, detail=detail, headers=headers, **kwargs
+ )
class BadRequest(ProblemException):
"""Raise when the server processes a bad request"""
- def __init__(self, title='Bad request', detail=None):
- super().__init__(status=400, title=title, detail=detail)
+ def __init__(
+ self,
+ title: str = 'Bad Request',
+ detail: Optional[str] = None,
+ headers: Optional[Dict] = None,
+ **kwargs,
+ ):
+ super(BadRequest, self).__init__(
+ status=400, type=EXCEPTIONS_LINK_MAP[400], title=title, detail=detail, headers=headers, **kwargs
+ )
class Unauthenticated(ProblemException):
"""Raise when the user is not authenticated"""
def __init__(
- self, title: str = 'Unauthorized', detail: Optional[str] = None, headers: Optional[Dict] = None,
+ self,
+ title: str = 'Unauthorized',
+ detail: Optional[str] = None,
+ headers: Optional[Dict] = None,
+ **kwargs,
):
- super().__init__(status=401, title=title, detail=detail, headers=headers)
+ super(Unauthenticated, self).__init__(
+ status=401, type=EXCEPTIONS_LINK_MAP[401], title=title, detail=detail, headers=headers, **kwargs
+ )
class PermissionDenied(ProblemException):
"""Raise when the user does not have the required permissions"""
- def __init__(self, title='Forbidden', detail=None):
- super().__init__(status=403, title=title, detail=detail)
+ def __init__(
+ self, title: str = 'Forbidden', detail: Optional[str] = None, headers: Optional[Dict] = None, **kwargs
+ ):
+ super(PermissionDenied, self).__init__(
+ status=403, type=EXCEPTIONS_LINK_MAP[403], title=title, detail=detail, headers=headers, **kwargs
+ )
class AlreadyExists(ProblemException):
"""Raise when the object already exists"""
- def __init__(self, title='Object already exists', detail=None):
- super().__init__(status=409, title=title, detail=detail)
+ def __init__(
+ self, title='Conflict', detail: Optional[str] = None, headers: Optional[Dict] = None, **kwargs
+ ):
+ super(AlreadyExists, self).__init__(
+ status=409, type=EXCEPTIONS_LINK_MAP[409], title=title, detail=detail, headers=headers, **kwargs
+ )
class Unknown(ProblemException):
"""Returns a response body and status code for HTTP 500 exception"""
- def __init__(self, title='Unknown server error', detail=None):
- super().__init__(status=500, title=title, detail=detail)
+ def __init__(
+ self,
+ title: str = 'Internal Server Error',
+ detail: Optional[str] = None,
+ headers: Optional[Dict] = None,
+ **kwargs,
+ ):
+ super(Unknown, self).__init__(
+ status=500, type=EXCEPTIONS_LINK_MAP[500], title=title, detail=detail, headers=headers, **kwargs
+ )
diff --git a/airflow/api_connexion/openapi/v1.yaml b/airflow/api_connexion/openapi/v1.yaml
index 6943563..6727462 100644
--- a/airflow/api_connexion/openapi/v1.yaml
+++ b/airflow/api_connexion/openapi/v1.yaml
@@ -173,6 +173,35 @@ info:
For details on configuring the authentication, see
[API Authorization](https://airflow.readthedocs.io/en/latest/security/api.html).
+ # Errors
+
+ We follow the error response format proposed in [RFC 7807](https://tools.ietf.org/html/rfc7807)
+ also known as Problem Details for HTTP APIs. As with our normal API responses,
+ your client must be prepared to gracefully handle additional members of the response.
+ ## Unauthenticated
+ This indicates that the request has not been applied because it lacks valid authentication
+ credentials for the target resource. Please check that you have valid credentials.
+ ## PermissionDenied
+ This response means that the server understood the request but refuses to authorize
+ it because it lacks sufficient rights to the resource. It happens when you do not have the
+ necessary permission to execute the action you performed. You need to get the appropriate
+ permissions in other to resolve this error.
+ ## BadRequest
+ This response means that the server cannot or will not process the request due to something
+ that is perceived to be a client error (e.g., malformed request syntax, invalid request message
+ framing, or deceptive request routing). To resolve this, please ensure that your syntax is correct.
+ ## NotFound
+ This client error response indicates that the server cannot find the requested resource.
+ ## NotAcceptable
+ The target resource does not have a current representation that would be acceptable to the user
+ agent, according to the proactive negotiation header fields received in the request, and the
+ server is unwilling to supply a default representation.
+ ## AlreadyExists
+ The request could not be completed due to a conflict with the current state of the target
+ resource, meaning that the resource already exists
+ ## Unknown
+ This means that the server encountered an unexpected condition that prevented it from
+ fulfilling the request.
version: '1.0.0'
license:
diff --git a/airflow/www/extensions/init_views.py b/airflow/www/extensions/init_views.py
index ac1e7a4..ea39c8a 100644
--- a/airflow/www/extensions/init_views.py
+++ b/airflow/www/extensions/init_views.py
@@ -22,6 +22,8 @@ import connexion
from connexion import ProblemException
from flask import Flask
+from airflow.api_connexion.exceptions import common_error_handler
+
log = logging.getLogger(__name__)
# airflow/www/extesions/init_views.py => airflow/
@@ -104,7 +106,7 @@ def init_api_connexion(app: Flask) -> None:
api_bp = connexion_app.add_api(
specification='v1.yaml', base_path='/api/v1', validate_responses=True, strict_validation=True
).blueprint
- app.register_error_handler(ProblemException, connexion_app.common_error_handler)
+ app.register_error_handler(ProblemException, common_error_handler)
app.extensions['csrf'].exempt(api_bp)
diff --git a/tests/api/auth/backend/test_basic_auth.py b/tests/api/auth/backend/test_basic_auth.py
index 9a2a4a7..0646134 100644
--- a/tests/api/auth/backend/test_basic_auth.py
+++ b/tests/api/auth/backend/test_basic_auth.py
@@ -22,6 +22,7 @@ from flask_login import current_user
from parameterized import parameterized
from airflow.www.app import create_app
+from tests.test_utils.api_connexion_utils import assert_401
from tests.test_utils.config import conf_vars
from tests.test_utils.db import clear_db_pools
@@ -89,12 +90,7 @@ class TestBasicAuth(unittest.TestCase):
assert response.status_code == 401
assert response.headers["Content-Type"] == "application/problem+json"
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json == {
- 'detail': None,
- 'status': 401,
- 'title': 'Unauthorized',
- 'type': 'about:blank',
- }
+ assert_401(response)
@parameterized.expand([
("basic " + b64encode(b"test").decode(),),
@@ -110,12 +106,7 @@ class TestBasicAuth(unittest.TestCase):
assert response.status_code == 401
assert response.headers["Content-Type"] == "application/problem+json"
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json == {
- 'detail': None,
- 'status': 401,
- 'title': 'Unauthorized',
- 'type': 'about:blank',
- }
+ assert_401(response)
def test_experimental_api(self):
with self.app.test_client() as test_client:
diff --git a/tests/api_connexion/endpoints/test_connection_endpoint.py b/tests/api_connexion/endpoints/test_connection_endpoint.py
index 7bde11f..134f525 100644
--- a/tests/api_connexion/endpoints/test_connection_endpoint.py
+++ b/tests/api_connexion/endpoints/test_connection_endpoint.py
@@ -18,6 +18,7 @@ import unittest
from parameterized import parameterized
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models import Connection
from airflow.utils.session import provide_session
from airflow.www import app
@@ -76,7 +77,12 @@ class TestDeleteConnection(TestConnectionEndpoint):
assert response.status_code == 404
self.assertEqual(
response.json,
- {'detail': None, 'status': 404, 'title': 'Connection not found', 'type': 'about:blank'},
+ {
+ 'detail': "The Connection with connection_id: `test-connection` was not found",
+ 'status': 404,
+ 'title': 'Connection not found',
+ 'type': EXCEPTIONS_LINK_MAP[404],
+ },
)
def test_should_raises_401_unauthenticated(self):
@@ -122,7 +128,12 @@ class TestGetConnection(TestConnectionEndpoint):
)
assert response.status_code == 404
self.assertEqual(
- {'detail': None, 'status': 404, 'title': 'Connection not found', 'type': 'about:blank'},
+ {
+ 'detail': "The Connection with connection_id: `invalid-connection` was not found",
+ 'status': 404,
+ 'title': 'Connection not found',
+ 'type': EXCEPTIONS_LINK_MAP[404],
+ },
response.json,
)
@@ -362,7 +373,7 @@ class TestPatchConnection(TestConnectionEndpoint):
environ_overrides={'REMOTE_USER': "test"},
)
assert response.status_code == 400
- self.assertEqual(response.json['title'], error_message)
+ self.assertEqual(response.json['detail'], error_message)
@parameterized.expand(
[
@@ -409,7 +420,12 @@ class TestPatchConnection(TestConnectionEndpoint):
)
assert response.status_code == 404
self.assertEqual(
- {'detail': None, 'status': 404, 'title': 'Connection not found', 'type': 'about:blank'},
+ {
+ 'detail': "The Connection with connection_id: `test-connection-id` was not found",
+ 'status': 404,
+ 'title': 'Connection not found',
+ 'type': EXCEPTIONS_LINK_MAP[404],
+ },
response.json,
)
@@ -450,8 +466,8 @@ class TestPostConnection(TestConnectionEndpoint):
{
'detail': "{'conn_type': ['Missing data for required field.']}",
'status': 400,
- 'title': 'Bad request',
- 'type': 'about:blank',
+ 'title': 'Bad Request',
+ 'type': EXCEPTIONS_LINK_MAP[400],
},
)
@@ -469,10 +485,10 @@ class TestPostConnection(TestConnectionEndpoint):
self.assertEqual(
response.json,
{
- 'detail': None,
+ 'detail': 'Connection already exist. ID: test-connection-id',
'status': 409,
- 'title': 'Connection already exist. ID: test-connection-id',
- 'type': 'about:blank',
+ 'title': 'Conflict',
+ 'type': EXCEPTIONS_LINK_MAP[409],
},
)
diff --git a/tests/api_connexion/endpoints/test_dag_endpoint.py b/tests/api_connexion/endpoints/test_dag_endpoint.py
index d89fd85..8c396d6 100644
--- a/tests/api_connexion/endpoints/test_dag_endpoint.py
+++ b/tests/api_connexion/endpoints/test_dag_endpoint.py
@@ -21,6 +21,7 @@ from datetime import datetime
from parameterized import parameterized
from airflow import DAG
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models import DagBag, DagModel
from airflow.models.serialized_dag import SerializedDagModel
from airflow.operators.dummy_operator import DummyOperator
@@ -329,7 +330,7 @@ class TestPatchDag(TestDagEndpoint):
'detail': "Property is read-only - 'schedule_interval'",
'status': 400,
'title': 'Bad Request',
- 'type': 'about:blank',
+ 'type': EXCEPTIONS_LINK_MAP[400],
},
)
diff --git a/tests/api_connexion/endpoints/test_dag_run_endpoint.py b/tests/api_connexion/endpoints/test_dag_run_endpoint.py
index 816beb5..4d05ebe 100644
--- a/tests/api_connexion/endpoints/test_dag_run_endpoint.py
+++ b/tests/api_connexion/endpoints/test_dag_run_endpoint.py
@@ -19,6 +19,7 @@ from datetime import timedelta
from parameterized import parameterized
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models import DagModel, DagRun
from airflow.utils import timezone
from airflow.utils.session import create_session, provide_session
@@ -118,8 +119,8 @@ class TestDeleteDagRun(TestDagRunEndpoint):
{
"detail": "DAGRun with DAG ID: 'INVALID_DAG_RUN' and DagRun ID: 'INVALID_DAG_RUN' not found",
"status": 404,
- "title": "Object not found",
- "type": "about:blank",
+ "title": "Not Found",
+ "type": EXCEPTIONS_LINK_MAP[404],
},
)
@@ -169,7 +170,12 @@ class TestGetDagRun(TestDagRunEndpoint):
"api/v1/dags/invalid-id/dagRuns/invalid-id", environ_overrides={'REMOTE_USER': "test"}
)
assert response.status_code == 404
- expected_resp = {'detail': None, 'status': 404, 'title': 'DAGRun not found', 'type': 'about:blank'}
+ expected_resp = {
+ 'detail': "DAGRun with DAG ID: 'invalid-id' and DagRun ID: 'invalid-id' not found",
+ 'status': 404,
+ 'title': 'DAGRun not found',
+ 'type': EXCEPTIONS_LINK_MAP[404],
+ }
assert expected_resp == response.json
@provide_session
@@ -708,10 +714,10 @@ class TestPostDagRun(TestDagRunEndpoint):
self.assertEqual(response.status_code, 404)
self.assertEqual(
{
- "detail": None,
+ "detail": "DAG with dag_id: 'TEST_DAG_ID' not found",
"status": 404,
- "title": "DAG with dag_id: 'TEST_DAG_ID' not found",
- "type": "about:blank",
+ "title": "DAG not found",
+ "type": EXCEPTIONS_LINK_MAP[404],
},
response.json,
)
@@ -726,7 +732,7 @@ class TestPostDagRun(TestDagRunEndpoint):
"detail": "Property is read-only - 'start_date'",
"status": 400,
"title": "Bad Request",
- "type": "about:blank",
+ "type": EXCEPTIONS_LINK_MAP[400],
},
),
(
@@ -737,7 +743,7 @@ class TestPostDagRun(TestDagRunEndpoint):
"detail": "Property is read-only - 'state'",
"status": 400,
"title": "Bad Request",
- "type": "about:blank",
+ "type": EXCEPTIONS_LINK_MAP[400],
},
),
]
@@ -770,8 +776,8 @@ class TestPostDagRun(TestDagRunEndpoint):
"detail": "DAGRun with DAG ID: 'TEST_DAG_ID' and "
"DAGRun ID: 'TEST_DAG_RUN_ID_1' already exists",
"status": 409,
- "title": "Object already exists",
- "type": "about:blank",
+ "title": "Conflict",
+ "type": EXCEPTIONS_LINK_MAP[409],
},
)
diff --git a/tests/api_connexion/endpoints/test_event_log_endpoint.py b/tests/api_connexion/endpoints/test_event_log_endpoint.py
index 780f447..d8d5d21 100644
--- a/tests/api_connexion/endpoints/test_event_log_endpoint.py
+++ b/tests/api_connexion/endpoints/test_event_log_endpoint.py
@@ -19,6 +19,7 @@ import unittest
from parameterized import parameterized
from airflow import DAG
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models import Log, TaskInstance
from airflow.operators.dummy_operator import DummyOperator
from airflow.utils import timezone
@@ -93,7 +94,7 @@ class TestGetEventLog(TestEventLogEndpoint):
response = self.client.get("/api/v1/eventLogs/1", environ_overrides={'REMOTE_USER': "test"})
assert response.status_code == 404
self.assertEqual(
- {'detail': None, 'status': 404, 'title': 'Event Log not found', 'type': 'about:blank'},
+ {'detail': None, 'status': 404, 'title': 'Event Log not found', 'type': EXCEPTIONS_LINK_MAP[404]},
response.json,
)
diff --git a/tests/api_connexion/endpoints/test_extra_link_endpoint.py b/tests/api_connexion/endpoints/test_extra_link_endpoint.py
index f0bdd7c..19433db 100644
--- a/tests/api_connexion/endpoints/test_extra_link_endpoint.py
+++ b/tests/api_connexion/endpoints/test_extra_link_endpoint.py
@@ -21,6 +21,7 @@ from urllib.parse import quote_plus
from parameterized import parameterized
from airflow import DAG
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models.baseoperator import BaseOperatorLink
from airflow.models.dagrun import DagRun
from airflow.models.xcom import XCom
@@ -114,7 +115,12 @@ class TestGetExtraLinks(unittest.TestCase):
self.assertEqual(404, response.status_code)
self.assertEqual(
- {"detail": expected_detail, "status": 404, "title": expected_title, "type": "about:blank"},
+ {
+ "detail": expected_detail,
+ "status": 404,
+ "title": expected_title,
+ "type": EXCEPTIONS_LINK_MAP[404],
+ },
response.json,
)
diff --git a/tests/api_connexion/endpoints/test_import_error_endpoint.py b/tests/api_connexion/endpoints/test_import_error_endpoint.py
index 78439fa..404ff06 100644
--- a/tests/api_connexion/endpoints/test_import_error_endpoint.py
+++ b/tests/api_connexion/endpoints/test_import_error_endpoint.py
@@ -18,6 +18,7 @@ import unittest
from parameterized import parameterized
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models.errors import ImportError # pylint: disable=redefined-builtin
from airflow.utils import timezone
from airflow.utils.session import provide_session
@@ -87,7 +88,12 @@ class TestGetImportErrorEndpoint(TestBaseImportError):
response = self.client.get("/api/v1/importErrors/2", environ_overrides={'REMOTE_USER': "test"})
assert response.status_code == 404
self.assertEqual(
- {"detail": None, "status": 404, "title": "Import error not found", "type": "about:blank",},
+ {
+ "detail": "The ImportError with import_error_id: `2` was not found",
+ "status": 404,
+ "title": "Import error not found",
+ "type": EXCEPTIONS_LINK_MAP[404],
+ },
response.json,
)
diff --git a/tests/api_connexion/endpoints/test_log_endpoint.py b/tests/api_connexion/endpoints/test_log_endpoint.py
index 2acacf3..2921a90 100644
--- a/tests/api_connexion/endpoints/test_log_endpoint.py
+++ b/tests/api_connexion/endpoints/test_log_endpoint.py
@@ -27,6 +27,7 @@ from unittest.mock import PropertyMock
from itsdangerous.url_safe import URLSafeSerializer
from airflow import DAG, settings
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
from airflow.models import DagRun, TaskInstance
from airflow.operators.dummy_operator import DummyOperator
@@ -256,7 +257,7 @@ class TestGetLog(unittest.TestCase):
'detail': None,
'status': 400,
'title': "Bad Signature. Please use only the tokens provided by the API.",
- 'type': 'about:blank',
+ 'type': EXCEPTIONS_LINK_MAP[400],
},
)
@@ -269,7 +270,7 @@ class TestGetLog(unittest.TestCase):
)
self.assertEqual(
response.json,
- {'detail': None, 'status': 404, 'title': "DAG Run not found", 'type': 'about:blank'},
+ {'detail': None, 'status': 404, 'title': "DAG Run not found", 'type': EXCEPTIONS_LINK_MAP[404]},
)
def test_should_raises_401_unauthenticated(self):
diff --git a/tests/api_connexion/endpoints/test_pool_endpoint.py b/tests/api_connexion/endpoints/test_pool_endpoint.py
index baba612..4136f6a 100644
--- a/tests/api_connexion/endpoints/test_pool_endpoint.py
+++ b/tests/api_connexion/endpoints/test_pool_endpoint.py
@@ -18,6 +18,7 @@ import unittest
from parameterized import parameterized
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models.pool import Pool
from airflow.utils.session import provide_session
from airflow.www import app
@@ -167,8 +168,8 @@ class TestGetPool(TestBasePoolEndpoints):
{
"detail": "Pool with name:'invalid_pool' not found",
"status": 404,
- "title": "Object not found",
- "type": "about:blank",
+ "title": "Not Found",
+ "type": EXCEPTIONS_LINK_MAP[404],
},
response.json,
)
@@ -200,8 +201,8 @@ class TestDeletePool(TestBasePoolEndpoints):
{
"detail": "Pool with name:'invalid_pool' not found",
"status": 404,
- "title": "Object not found",
- "type": "about:blank",
+ "title": "Not Found",
+ "type": EXCEPTIONS_LINK_MAP[404],
},
response.json,
)
@@ -258,8 +259,8 @@ class TestPostPool(TestBasePoolEndpoints):
{
"detail": f"Pool: {pool_name} already exists",
"status": 409,
- "title": "Object already exists",
- "type": "about:blank",
+ "title": "Conflict",
+ "type": EXCEPTIONS_LINK_MAP[409],
},
response.json,
)
@@ -282,7 +283,12 @@ class TestPostPool(TestBasePoolEndpoints):
)
assert response.status_code == 400
self.assertDictEqual(
- {"detail": error_detail, "status": 400, "title": "Bad request", "type": "about:blank",},
+ {
+ "detail": error_detail,
+ "status": 400,
+ "title": "Bad Request",
+ "type": EXCEPTIONS_LINK_MAP[400],
+ },
response.json,
)
@@ -338,7 +344,12 @@ class TestPatchPool(TestBasePoolEndpoints):
)
assert response.status_code == 400
self.assertEqual(
- {"detail": error_detail, "status": 400, "title": "Bad request", "type": "about:blank",},
+ {
+ "detail": error_detail,
+ "status": 400,
+ "title": "Bad Request",
+ "type": EXCEPTIONS_LINK_MAP[400],
+ },
response.json,
)
@@ -361,8 +372,8 @@ class TestModifyDefaultPool(TestBasePoolEndpoints):
{
"detail": "Default Pool can't be deleted",
"status": 400,
- "title": "Bad request",
- "type": "about:blank",
+ "title": "Bad Request",
+ "type": EXCEPTIONS_LINK_MAP[400],
},
response.json,
)
@@ -377,8 +388,8 @@ class TestModifyDefaultPool(TestBasePoolEndpoints):
{
"detail": "Default Pool's name can't be modified",
"status": 400,
- "title": "Bad request",
- "type": "about:blank",
+ "title": "Bad Request",
+ "type": EXCEPTIONS_LINK_MAP[400],
},
),
(
@@ -389,8 +400,8 @@ class TestModifyDefaultPool(TestBasePoolEndpoints):
{
"detail": "Default Pool's name can't be modified",
"status": 400,
- "title": "Bad request",
- "type": "about:blank",
+ "title": "Bad Request",
+ "type": EXCEPTIONS_LINK_MAP[400],
},
),
(
@@ -503,13 +514,13 @@ class TestPatchPoolWithUpdateMask(TestBasePoolEndpoints):
),
(
"Invalid update mask",
- "'names' is not a valid Pool field",
+ "Invalid field: names in update mask",
"api/v1/pools/test_pool?update_mask=slots, names,",
{"name": "test_pool_a", "slots": 2},
),
(
"Invalid update mask",
- "'slot' is not a valid Pool field",
+ "Invalid field: slot in update mask",
"api/v1/pools/test_pool?update_mask=slot, name,",
{"name": "test_pool_a", "slots": 2},
),
@@ -523,8 +534,12 @@ class TestPatchPoolWithUpdateMask(TestBasePoolEndpoints):
session.commit()
response = self.client.patch(url, json=patch_json, environ_overrides={'REMOTE_USER': "test"})
assert response.status_code == 400
- self.assertEqual
- (
- {"detail": error_detail, "status": 400, "title": "Bad Request", "type": "about:blank",},
+ self.assertEqual(
+ {
+ "detail": error_detail,
+ "status": 400,
+ "title": "Bad Request",
+ "type": EXCEPTIONS_LINK_MAP[400],
+ },
response.json,
)
diff --git a/tests/api_connexion/endpoints/test_variable_endpoint.py b/tests/api_connexion/endpoints/test_variable_endpoint.py
index 9413a3c..77e07b4 100644
--- a/tests/api_connexion/endpoints/test_variable_endpoint.py
+++ b/tests/api_connexion/endpoints/test_variable_endpoint.py
@@ -18,6 +18,7 @@ import unittest
from parameterized import parameterized
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.models import Variable
from airflow.www import app
from tests.test_utils.api_connexion_utils import assert_401, create_user, delete_user
@@ -185,7 +186,7 @@ class TestPatchVariable(TestVariableEndpoint):
assert response.json == {
"title": "Invalid post body",
"status": 400,
- "type": "about:blank",
+ "type": EXCEPTIONS_LINK_MAP[400],
"detail": "key from request body doesn't match uri parameter",
}
@@ -195,7 +196,7 @@ class TestPatchVariable(TestVariableEndpoint):
assert response.json == {
"title": "Invalid Variable schema",
"status": 400,
- "type": "about:blank",
+ "type": EXCEPTIONS_LINK_MAP[400],
"detail": "{'value': ['Missing data for required field.']}",
}
@@ -231,7 +232,7 @@ class TestPostVariables(TestVariableEndpoint):
assert response.json == {
"title": "Invalid Variable schema",
"status": 400,
- "type": "about:blank",
+ "type": EXCEPTIONS_LINK_MAP[400],
"detail": "{'value': ['Missing data for required field.'], 'v': ['Unknown field.']}",
}
diff --git a/tests/test_utils/api_connexion_utils.py b/tests/test_utils/api_connexion_utils.py
index bd5d34a..9da434b 100644
--- a/tests/test_utils/api_connexion_utils.py
+++ b/tests/test_utils/api_connexion_utils.py
@@ -14,6 +14,8 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
+
def create_user(app, username, role):
appbuilder = app.appbuilder
@@ -43,5 +45,5 @@ def assert_401(response):
'detail': None,
'status': 401,
'title': 'Unauthorized',
- 'type': 'about:blank'
+ 'type': EXCEPTIONS_LINK_MAP[401]
}