# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed 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. """HTTP Exceptions used by keystoneauth1.""" import typing as ty import requests from keystoneauth1.exceptions import auth from keystoneauth1.exceptions import base __all__ = ( 'HttpError', 'HTTPClientError', 'BadRequest', 'Unauthorized', 'PaymentRequired', 'Forbidden', 'NotFound', 'MethodNotAllowed', 'NotAcceptable', 'ProxyAuthenticationRequired', 'RequestTimeout', 'Conflict', 'Gone', 'LengthRequired', 'PreconditionFailed', 'RequestEntityTooLarge', 'RequestUriTooLong', 'UnsupportedMediaType', 'RequestedRangeNotSatisfiable', 'ExpectationFailed', 'UnprocessableEntity', 'HttpServerError', 'InternalServerError', 'HttpNotImplemented', 'BadGateway', 'ServiceUnavailable', 'GatewayTimeout', 'HttpVersionNotSupported', 'from_response', ) class HttpError(base.ClientException): """The base exception class for all HTTP exceptions.""" http_status = 0 message = "HTTP Error" def __init__( self, message: ty.Optional[str] = None, details: ty.Optional[str] = None, response: ty.Optional[requests.Response] = None, request_id: ty.Optional[str] = None, url: ty.Optional[str] = None, method: ty.Optional[str] = None, http_status: ty.Optional[int] = None, retry_after: int = 0, ): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = f"{self.message} (HTTP {self.http_status})" self.retry_after = retry_after if request_id: formatted_string += f" (Request-ID: {request_id})" super().__init__(formatted_string) class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = "HTTP Client Error" class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = "HTTP Server Error" class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = "Bad Request" class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = "Unauthorized" class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = "Payment Required" class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = "Forbidden" class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = "Not Found" class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = "Method Not Allowed" class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = "Not Acceptable" class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = "Conflict" class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Precondition Failed" class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = "Request Entity Too Large" def __init__( self, message: ty.Optional[str] = None, details: ty.Optional[str] = None, response: ty.Optional[requests.Response] = None, request_id: ty.Optional[str] = None, url: ty.Optional[str] = None, method: ty.Optional[str] = None, http_status: ty.Optional[int] = None, retry_after: int = 0, ): try: self.retry_after = int(retry_after) except (KeyError, ValueError): self.retry_after = 0 super().__init__( message=message, details=details, response=response, request_id=request_id, url=url, method=method, http_status=http_status, ) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = "Request-URI Too Long" class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = "Unprocessable Entity" class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = "Internal Server Error" # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = "Not Implemented" class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = "Service Unavailable" class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "HTTP Version Not Supported" # _code_map contains all the classes that have http_status attribute. _code_map = { 400: BadRequest, 401: Unauthorized, 402: PaymentRequired, 403: Forbidden, 404: NotFound, 405: MethodNotAllowed, 406: NotAcceptable, 407: ProxyAuthenticationRequired, 408: RequestTimeout, 409: Conflict, 410: Gone, 411: LengthRequired, 412: PreconditionFailed, 413: RequestEntityTooLarge, 414: RequestUriTooLong, 415: UnsupportedMediaType, 416: RequestedRangeNotSatisfiable, 417: ExpectationFailed, 422: UnprocessableEntity, 500: InternalServerError, 501: HttpNotImplemented, 502: BadGateway, 503: ServiceUnavailable, 504: GatewayTimeout, 505: HttpVersionNotSupported, } def from_response( response: requests.Response, method: str, url: str ) -> ty.Union[HttpError, auth.MissingAuthMethods]: """Return an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ req_id = response.headers.get("x-openstack-request-id") message = None details = None retry_after = 0 if "retry-after" in response.headers: try: retry_after = int(response.headers["retry-after"]) except ValueError: pass content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if isinstance(body, dict) and isinstance(body.get("error"), dict): message = body["error"].get("message") details = body["error"].get("details") elif isinstance(body, dict) and isinstance( body.get("errors"), list ): # if the error response follows the API SIG guidelines, it # will return a list of errors. in this case, only the first # error is shown, but if there are multiple the user will be # alerted to that fact. errors = body["errors"] if len(errors) == 0: # just in case we get an empty array message = None details = None else: if len(errors) > 1: # if there is more than one error, let the user know # that multiple were seen. msg_hdr = ( "Multiple error responses, showing first only: " ) else: msg_hdr = "" message = "{}{}".format(msg_hdr, errors[0].get("title")) details = errors[0].get("detail") else: message = "Unrecognized schema in response body." elif content_type.startswith("text/"): details = response.text # we check explicity for 401 in case of auth receipts if ( response.status_code == 401 and "Openstack-Auth-Receipt" in response.headers ): return auth.MissingAuthMethods(response) try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls( http_status=response.status_code, response=response, method=method, url=url, request_id=req_id, retry_after=retry_after, message=message, details=details, )