def grammar_not_implemented_handler(request: Request, exc: VisitError) -> JSONAPIResponse: """Handle an error raised by Lark during filter transformation All errors raised during filter transformation are wrapped in the Lark `VisitError`. According to the OPTIMADE specification, these errors are repurposed to be 501 NotImplementedErrors. For special exceptions, like [`BadRequest`][optimade.server.exceptions.BadRequest], we pass-through to [`general_exception()`][optimade.server.exception_handlers.general_exception], since they should not return a 501 NotImplementedError. Parameters: request: The HTTP request resulting in the exception being raised. exc: The exception being raised. Returns: A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception]. """ pass_through_exceptions = (BadRequest, ) orig_exc = getattr(exc, "orig_exc", None) if isinstance(orig_exc, pass_through_exceptions): return general_exception(request, orig_exc) rule = getattr(exc.obj, "data", getattr(exc.obj, "type", str(exc))) status = 501 title = "NotImplementedError" detail = (f"Error trying to process rule '{rule}'" if not str(exc.orig_exc) else str(exc.orig_exc)) error = OptimadeError(detail=detail, status=status, title=title) return general_exception(request, exc, status_code=status, errors=[error])
def validation_exception_handler(request: Request, exc: ValidationError) -> JSONAPIResponse: """Handle a general pydantic validation error The pydantic `ValidationError` usually contains a list of errors, this function extracts them and wraps them in the OPTIMADE specific error resource. Parameters: request: The HTTP request resulting in the exception being raised. exc: The exception being raised. Returns: A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception]. """ status = 500 title = "ValidationError" errors = set() for error in exc.errors(): pointer = "/" + "/".join([str(_) for _ in error["loc"]]) source = ErrorSource(pointer=pointer) code = error["type"] detail = error["msg"] errors.add( OptimadeError(detail=detail, status=status, title=title, source=source, code=code)) return general_exception(request, exc, status_code=status, errors=list(errors))
async def request_validation_exception_handler( request: "Request", exc: "RequestValidationError") -> "JSONResponse": """Special handler if a `RequestValidationError` comes from wrong `POST` data""" status_code = 500 if request.method in ("POST", "post"): status_code = 400 errors = set() for error in exc.errors(): pointer = "/" + "/".join([str(_) for _ in error["loc"]]) source = ErrorSource(pointer=pointer) code = error["type"] detail = error["msg"] errors.add( OptimadeError( detail=detail, status=status_code, title=str(exc.__class__.__name__), source=source, code=code, )) return general_exception(request, exc, status_code=status_code, errors=list(errors))
def grammar_not_implemented_handler(request: Request, exc: VisitError): rule = getattr(exc.obj, "data", getattr(exc.obj, "type", str(exc))) status = 501 title = "NotImplementedError" detail = (f"Error trying to process rule '{rule}'" if not str(exc.orig_exc) else str(exc.orig_exc)) error = OptimadeError(detail=detail, status=status, title=title) return general_exception(request, exc, status_code=status, errors=[error])
def general_exception( request: Request, exc: Exception, status_code: int = 500, # A status_code in `exc` will take precedence errors: List[OptimadeError] = None, ) -> JSONResponse: debug_info = {} if CONFIG.debug: tb = "".join( traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__) ) print(tb) debug_info[f"_{CONFIG.provider.prefix}_traceback"] = tb try: http_response_code = int(exc.status_code) except AttributeError: http_response_code = int(status_code) try: title = str(exc.title) except AttributeError: title = str(exc.__class__.__name__) try: detail = str(exc.detail) except AttributeError: detail = str(exc) if errors is None: errors = [OptimadeError(detail=detail, status=http_response_code, title=title)] try: response = ErrorResponse( meta=meta_values( url=str(request.url), data_returned=0, data_available=0, more_data_available=False, **debug_info, ), errors=errors, ) except Exception: # This was introduced due to the original raise of an HTTPException if the # path prefix could not be found, e.g., `/v0`. # However, due to the testing, this error cannot be raised anymore. # Instead, an OPTIMADE warning should be issued. # Having this try and except is still good practice though. response = ErrorResponse(errors=errors) return JSONResponse( status_code=http_response_code, content=jsonable_encoder(response, exclude_unset=True), )
def general_exception( request: Request, exc: Exception, status_code: int = 500, # A status_code in `exc` will take precedence errors: List[OptimadeError] = None, ) -> JSONResponse: debug_info = {} if CONFIG.debug: tb = "".join( traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__)) LOGGER.error("Traceback:\n%s", tb) debug_info[f"_{CONFIG.provider.prefix}_traceback"] = tb try: http_response_code = int(exc.status_code) except AttributeError: http_response_code = int(status_code) try: title = str(exc.title) except AttributeError: title = str(exc.__class__.__name__) try: detail = str(exc.detail) except AttributeError: detail = str(exc) if errors is None: errors = [ OptimadeError(detail=detail, status=http_response_code, title=title) ] response = ErrorResponse( meta=meta_values( url=request.url, data_returned=0, data_available=0, more_data_available=False, **debug_info, ), errors=errors, ) return JSONResponse( status_code=http_response_code, content=jsonable_encoder(response, exclude_unset=True), )
def validation_exception_handler(request: Request, exc: ValidationError): status = 500 title = "ValidationError" errors = set() for error in exc.errors(): pointer = "/" + "/".join([str(_) for _ in error["loc"]]) source = ErrorSource(pointer=pointer) code = error["type"] detail = error["msg"] errors.add( OptimadeError( detail=detail, status=status, title=title, source=source, code=code ) ) return general_exception(request, exc, status_code=status, errors=list(errors))
def not_implemented_handler(request: Request, exc: NotImplementedError) -> JSONAPIResponse: """Handle a standard NotImplementedError Python exception Parameters: request: The HTTP request resulting in the exception being raised. exc: The exception being raised. Returns: A JSON HTTP response through [`general_exception()`][optimade.server.exception_handlers.general_exception]. """ status = 501 title = "NotImplementedError" detail = str(exc) error = OptimadeError(detail=detail, status=status, title=title) return general_exception(request, exc, status_code=status, errors=[error])
def handle_errors(response: dict) -> Tuple[str, set]: """Handle any errors""" if "data" not in response and "errors" not in response: raise InputError(f"No data and no errors reported in response: {response}") if "errors" in response: LOGGER.error("Errored response:\n%s", json.dumps(response, indent=2)) if "data" in response: msg = ( '<font color="red">Error(s) during querying,</font> but ' f"<strong>{len(response['data'])}</strong> structures found." ) elif isinstance(response["errors"], dict) and "detail" in response["errors"]: msg = ( '<font color="red">Error(s) during querying. ' f"Message from server:<br>{response['errors']['detail']!r}.</font>" ) elif isinstance(response["errors"], list) and any( ["detail" in _ for _ in response["errors"]] ): details = [_["detail"] for _ in response["errors"] if "detail" in _] msg = ( '<font color="red">Error(s) during querying. Message(s) from server:<br> - ' f"{'<br> - '.join(details)!r}</font>" ) else: msg = ( '<font color="red">Error during querying, ' "please try again later.</font>" ) http_errors = set() for raw_error in response.get("errors", []): try: error = OptimadeError(**raw_error) status = int(error.status) except (ValidationError, TypeError, ValueError): status = 400 http_errors.add(status) return msg, http_errors return "", set()
async def test_post_queries_bad_data( client: ( 'Callable[[str, FastAPI, str, Literal["get", "post", "put", "delete", "patch"]], Awaitable[Response]]' ), ): """Test POST /queries with bad data""" from optimade.models import ErrorResponse, OptimadeError from optimade_gateway.common.config import CONFIG from optimade_gateway.routers.utils import collection_factory data = { "gateway_id": "non-existent", "query_parameters": {"filter": 'elements HAS "Cu"', "page_limit": 15}, } response = await client("/queries", method="post", json=data) assert ( response.status_code == 404 ), f"Request succeeded, where it should have failed: {response.json()}" response = ErrorResponse(**response.json()) assert response assert len(response.errors) == 1, response.errors assert ( response.errors[0].dict() == OptimadeError( title="Not Found", status="404", detail=( "Resource <id=non-existent> not found in " f"{await collection_factory(CONFIG.gateways_collection)}." ), ).dict() )
def not_implemented_handler(request: Request, exc: NotImplementedError): status = 501 title = "NotImplementedError" detail = str(exc) error = OptimadeError(detail=detail, status=status, title=title) return general_exception(request, exc, status_code=status, errors=[error])
def general_exception( request: Request, exc: Exception, status_code: int = 500, # A status_code in `exc` will take precedence errors: List[OptimadeError] = None, ) -> JSONAPIResponse: """Handle an exception Parameters: request: The HTTP request resulting in the exception being raised. exc: The exception being raised. status_code: The returned HTTP status code for the error response. errors: List of error resources as defined in [the OPTIMADE specification](https://github.com/Materials-Consortia/OPTIMADE/blob/develop/optimade.rst#json-response-schema-common-fields). Returns: A JSON HTTP response based on [`ErrorResponse`][optimade.models.responses.ErrorResponse]. """ debug_info = {} if CONFIG.debug: tb = "".join( traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)) LOGGER.error("Traceback:\n%s", tb) debug_info[f"_{CONFIG.provider.prefix}_traceback"] = tb try: http_response_code = int(exc.status_code) except AttributeError: http_response_code = int(status_code) try: title = str(exc.title) except AttributeError: title = str(exc.__class__.__name__) try: detail = str(exc.detail) except AttributeError: detail = str(exc) if errors is None: errors = [ OptimadeError(detail=detail, status=http_response_code, title=title) ] response = ErrorResponse( meta=meta_values( url=request.url, data_returned=0, data_available=0, more_data_available=False, **debug_info, ), errors=errors, ) return JSONAPIResponse( status_code=http_response_code, content=jsonable_encoder(response, exclude_unset=True), )