Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial work to make v1 API compatible with SIP-40 and SIP-41 #13960

Merged
merged 8 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
betodealmeida marked this conversation as resolved.
Show resolved Hide resolved

@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