Skip to content

Commit

Permalink
feat: initial work to make v1 API compatible with SIP-40 and SIP-41 (a…
Browse files Browse the repository at this point in the history
…pache#13960)

* WIP

* Use errorhandler

* Add response schema

* Fix status on HTTPException

* s/found/encountered/g

* Fix test

* Fix lint

* Fix lint and test
  • Loading branch information
betodealmeida authored Apr 7, 2021
1 parent 7900af5 commit b67a271
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 26 deletions.
18 changes: 18 additions & 0 deletions docs/src/pages/docs/Miscellaneous/issue_codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,21 @@ The host might be down, and cannot be reached on the provided port.
The host provided when adding a new database doesn't seem to be up.
Additionally, it cannot be reached on the provided port. Please check that
there are no firewall rules preventing access to the host.

## Issue 1010

```
Superset encountered an error while running a command.
```

Something unexpected happened, and Superset encountered an error while
running a command. Please reach out to your administrator.

## Issue 1011

```
Superset encountered an unexpected error.
```

Someething unexpected happened in the Superset backend. Please reach out
to your administrator.
8 changes: 6 additions & 2 deletions superset-frontend/src/components/ErrorMessage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const ErrorTypeEnum = {
GENERIC_DB_ENGINE_ERROR: 'GENERIC_DB_ENGINE_ERROR',
COLUMN_DOES_NOT_EXIST_ERROR: 'COLUMN_DOES_NOT_EXIST_ERROR',
TABLE_DOES_NOT_EXIST_ERROR: 'TABLE_DOES_NOT_EXIST_ERROR',
TEST_CONNECTION_PORT_CLOSED_ERROR: 'TEST_CONNECTION_PORT_CLOSED_ERROR',
TEST_CONNECTION_HOST_DOWN_ERROR: 'TEST_CONNECTION_HOST_DOWN_ERROR',

// Viz errors
VIZ_GET_DF_ERROR: 'VIZ_GET_DF_ERROR',
Expand All @@ -48,8 +50,10 @@ export const ErrorTypeEnum = {
MISSING_TEMPLATE_PARAMS_ERROR: 'MISSING_TEMPLATE_PARAMS_ERROR',
TEST_CONNECTION_INVALID_HOSTNAME_ERROR:
'TEST_CONNECTION_INVALID_HOSTNAME_ERROR',
TEST_CONNECTION_PORT_CLOSED_ERROR: 'TEST_CONNECTION_PORT_CLOSED_ERROR',
TEST_CONNECTION_HOST_DOWN_ERROR: 'TEST_CONNECTION_HOST_DOWN_ERROR',

// Generic errors
GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR',
GENERIC_BACKEND_ERROR: 'GENERIC_BACKEND_ERROR',
} as const;

type ValueOf<T> = T[keyof T];
Expand Down
12 changes: 2 additions & 10 deletions superset/databases/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
DatabaseImportError,
DatabaseInvalidError,
DatabaseNotFoundError,
DatabaseTestConnectionFailedError,
DatabaseUpdateFailedError,
)
from superset.databases.commands.export import ExportDatabasesCommand
Expand All @@ -64,7 +63,6 @@
TableMetadataResponseSchema,
)
from superset.databases.utils import get_table_metadata
from superset.exceptions import SupersetErrorException
from superset.extensions import security_manager
from superset.models.core import Database
from superset.typing import FlaskResponse
Expand Down Expand Up @@ -558,7 +556,6 @@ def select_star(

@expose("/test_connection", methods=["POST"])
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
Expand Down Expand Up @@ -604,13 +601,8 @@ def test_connection( # pylint: disable=too-many-return-statements
# This validates custom Schema with custom validations
except ValidationError as error:
return self.response_400(message=error.messages)
try:
TestConnectionDatabaseCommand(g.user, item).run()
return self.response(200, message="OK")
except DatabaseTestConnectionFailedError as ex:
return self.response_422(message=str(ex))
except SupersetErrorException as ex:
return self.response(ex.status, message=ex.error.message)
TestConnectionDatabaseCommand(g.user, item).run()
return self.response(200, message="OK")

@expose("/<int:pk>/related_objects/", methods=["GET"])
@protect()
Expand Down
1 change: 1 addition & 0 deletions superset/databases/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class DatabaseDeleteFailedReportsExistError(DatabaseDeleteFailedError):


class DatabaseTestConnectionFailedError(CommandException):
status = 422
message = _("Connection failed, please check your connection settings")


Expand Down
22 changes: 20 additions & 2 deletions superset/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class SupersetErrorType(str, Enum):
GENERIC_DB_ENGINE_ERROR = "GENERIC_DB_ENGINE_ERROR"
COLUMN_DOES_NOT_EXIST_ERROR = "COLUMN_DOES_NOT_EXIST_ERROR"
TABLE_DOES_NOT_EXIST_ERROR = "TABLE_DOES_NOT_EXIST_ERROR"
TEST_CONNECTION_PORT_CLOSED_ERROR = "TEST_CONNECTION_PORT_CLOSED_ERROR"
TEST_CONNECTION_HOST_DOWN_ERROR = "TEST_CONNECTION_HOST_DOWN_ERROR"

# Viz errors
VIZ_GET_DF_ERROR = "VIZ_GET_DF_ERROR"
Expand All @@ -57,8 +59,10 @@ class SupersetErrorType(str, Enum):
# Sql Lab errors
MISSING_TEMPLATE_PARAMS_ERROR = "MISSING_TEMPLATE_PARAMS_ERROR"
TEST_CONNECTION_INVALID_HOSTNAME_ERROR = "TEST_CONNECTION_INVALID_HOSTNAME_ERROR"
TEST_CONNECTION_PORT_CLOSED_ERROR = "TEST_CONNECTION_PORT_CLOSED_ERROR"
TEST_CONNECTION_HOST_DOWN_ERROR = "TEST_CONNECTION_HOST_DOWN_ERROR"

# Generic errors
GENERIC_COMMAND_ERROR = "GENERIC_COMMAND_ERROR"
GENERIC_BACKEND_ERROR = "GENERIC_BACKEND_ERROR"


ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
Expand Down Expand Up @@ -135,6 +139,20 @@ class SupersetErrorType(str, Enum):
),
},
],
SupersetErrorType.GENERIC_COMMAND_ERROR: [
{
"code": 1010,
"message": _(
"Issue 1010 - Superset encountered an error while running a command."
),
},
],
SupersetErrorType.GENERIC_BACKEND_ERROR: [
{
"code": 1011,
"message": _("Issue 1011 - Superset encountered an unexpected error."),
},
],
}


Expand Down
14 changes: 9 additions & 5 deletions superset/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def __init__(
)


class SupersetErrorsException(SupersetException):
"""Exceptions with multiple SupersetErrorType associated with them"""

def __init__(self, errors: List[SupersetError]) -> None:
super().__init__(str(errors))
self.errors = errors


class SupersetTimeoutException(SupersetErrorException):
status = 408

Expand Down Expand Up @@ -97,13 +105,9 @@ def __init__(
self.payload = payload


class SupersetVizException(SupersetException):
class SupersetVizException(SupersetErrorsException):
status = 400

def __init__(self, errors: List[SupersetError]) -> None:
super().__init__(str(errors))
self.errors = errors


class NoDataException(SupersetException):
status = 400
Expand Down
49 changes: 49 additions & 0 deletions superset/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from superset.errors import SupersetErrorType

error_payload_content = {
"application/json": {
"schema": {
"type": "object",
"properties": {
# SIP-40 error payload
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"message": {"type": "string"},
"error_type": {
"type": "string",
"enum": [enum.value for enum in SupersetErrorType],
},
"level": {
"type": "string",
"enum": ["info", "warning", "error"],
},
"extra": {"type": "object"},
},
},
},
# Old-style message payload
"message": {"type": "string"},
},
},
},
}
79 changes: 78 additions & 1 deletion superset/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@
get_feature_flags,
security_manager,
)
from superset.commands.exceptions import CommandException, CommandInvalidError
from superset.connectors.sqla import models
from superset.datasets.commands.exceptions import get_dataset_exist_error_msg
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import (
SupersetErrorException,
SupersetErrorsException,
SupersetException,
SupersetSecurityException,
)
Expand Down Expand Up @@ -132,7 +134,7 @@ def json_errors_response(
return Response(
json.dumps(payload, default=utils.json_iso_dttm_ser, ignore_nan=True),
status=status,
mimetype="application/json",
mimetype="application/json; charset=utf-8",
)


Expand Down Expand Up @@ -330,6 +332,81 @@ def common_bootstrap_payload() -> Dict[str, Any]:
}


# pylint: disable=invalid-name
def get_error_level_from_status_code(status: int) -> ErrorLevel:
if status < 400:
return ErrorLevel.INFO
if status < 500:
return ErrorLevel.WARNING
return ErrorLevel.ERROR


# SIP-40 compatible error responses; make sure APIs raise
# SupersetErrorException or SupersetErrorsException
@superset_app.errorhandler(SupersetErrorException)
def show_superset_error(ex: SupersetErrorException) -> FlaskResponse:
logger.warning(ex)
return json_errors_response(errors=[ex.error], status=ex.status)


@superset_app.errorhandler(SupersetErrorsException)
def show_superset_errors(ex: SupersetErrorsException) -> FlaskResponse:
logger.warning(ex)
return json_errors_response(errors=ex.errors, status=ex.status)


@superset_app.errorhandler(HTTPException)
def show_http_exception(ex: HTTPException) -> FlaskResponse:
logger.warning(ex)
return json_errors_response(
errors=[
SupersetError(
message=utils.error_msg_from_exception(ex),
error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
level=ErrorLevel.ERROR,
extra={},
),
],
status=ex.code or 500,
)


# Temporary handler for CommandException; if an API raises a
# CommandException it should be fixed to map it to SupersetErrorException
# or SupersetErrorsException, with a specific status code and error type
@superset_app.errorhandler(CommandException)
def show_command_errors(ex: CommandException) -> FlaskResponse:
logger.warning(ex)
extra = ex.normalized_messages() if isinstance(ex, CommandInvalidError) else {}
return json_errors_response(
errors=[
SupersetError(
message=ex.message,
error_type=SupersetErrorType.GENERIC_COMMAND_ERROR,
level=get_error_level_from_status_code(ex.status),
extra=extra,
),
],
status=ex.status,
)


# Catch-all, to ensure all errors from the backend conform to SIP-40
@superset_app.errorhandler(Exception)
def show_unexpected_exception(ex: Exception) -> FlaskResponse:
logger.warning(ex)
return json_errors_response(
errors=[
SupersetError(
message=utils.error_msg_from_exception(ex),
error_type=SupersetErrorType.GENERIC_BACKEND_ERROR,
level=ErrorLevel.ERROR,
extra={},
),
],
)


@superset_app.context_processor
def get_common_bootstrap_data() -> Dict[str, Any]:
def serialize_bootstrap_data() -> str:
Expand Down
20 changes: 19 additions & 1 deletion superset/views/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.schemas import error_payload_content
from superset.sql_lab import Query as SqllabQuery
from superset.stats_logger import BaseStatsLogger
from superset.typing import FlaskResponse
Expand Down Expand Up @@ -77,7 +78,12 @@ def statsd_metrics(f: Callable[..., Any]) -> Callable[..., Any]:
"""

def wraps(self: "BaseSupersetModelRestApi", *args: Any, **kwargs: Any) -> Response:
duration, response = time_function(f, self, *args, **kwargs)
try:
duration, response = time_function(f, self, *args, **kwargs)
except Exception as ex:
self.incr_stats("error", f.__name__)
raise ex

self.send_stats_metrics(response, f.__name__, duration)
return response

Expand Down Expand Up @@ -198,6 +204,18 @@ class BaseSupersetModelRestApi(ModelRestApi):
list_columns: List[str]
show_columns: List[str]

responses = {
"400": {"description": "Bad request", "content": error_payload_content},
"401": {"description": "Unauthorized", "content": error_payload_content},
"403": {"description": "Forbidden", "content": error_payload_content},
"404": {"description": "Not found", "content": error_payload_content},
"422": {
"description": "Could not process entity",
"content": error_payload_content,
},
"500": {"description": "Fatal error", "content": error_payload_content},
}

def __init__(self) -> None:
# Setup statsd
self.stats_logger = BaseStatsLogger()
Expand Down
Loading

0 comments on commit b67a271

Please sign in to comment.