diff --git a/grpclib/client.py b/grpclib/client.py index abf4a67..2f36c9b 100644 --- a/grpclib/client.py +++ b/grpclib/client.py @@ -27,7 +27,7 @@ from .metadata import Deadline, USER_AGENT, decode_grpc_message, encode_timeout from .metadata import encode_metadata, decode_metadata, _MetadataLike, _Metadata from .metadata import _STATUS_DETAILS_KEY, decode_bin_value -from .exceptions import GRPCError, ProtocolError, StreamTerminatedError +from .exceptions import GRPCError, ProtocolError, StreamTerminatedError, HTTPDetails from .encoding.base import GRPC_CONTENT_TYPE, CodecBase, StatusDetailsCodecBase from .encoding.proto import ProtoCodec, ProtoStatusDetailsCodec from .encoding.proto import _googleapis_available @@ -294,13 +294,15 @@ def _raise_for_status(self, headers_map: Dict[str, str]) -> None: if status is not None and status != _H2_OK: grpc_status = _H2_TO_GRPC_STATUS_MAP.get(status, Status.UNKNOWN) raise GRPCError(grpc_status, - 'Received :status = {!r}'.format(status)) + 'Received :status = {!r}'.format(status), + http_details=HTTPDetails(status, headers_map)) def _raise_for_content_type(self, headers_map: Dict[str, str]) -> None: content_type = headers_map.get('content-type') if content_type is None: raise GRPCError(Status.UNKNOWN, - 'Missing content-type header') + 'Missing content-type header', + http_details=HTTPDetails(headers_map.get(":status"), headers_map)) base_content_type, _, sub_type = content_type.partition('+') sub_type = sub_type or ProtoCodec.__content_subtype__ @@ -310,19 +312,23 @@ def _raise_for_content_type(self, headers_map: Dict[str, str]) -> None: ): raise GRPCError(Status.UNKNOWN, 'Invalid content-type: {!r}' - .format(content_type)) + .format(content_type), + http_details=HTTPDetails(headers_map.get(":status"), headers_map)) def _process_grpc_status( self, headers_map: Dict[str, str], ) -> Tuple[Status, Optional[str], Any]: grpc_status = headers_map.get('grpc-status') if grpc_status is None: - raise GRPCError(Status.UNKNOWN, 'Missing grpc-status header') + raise GRPCError(Status.UNKNOWN, + 'Missing grpc-status header', + http_details=HTTPDetails(headers_map.get(":status"), headers_map)) try: status = Status(int(grpc_status)) except ValueError: - raise GRPCError(Status.UNKNOWN, ('Invalid grpc-status: {!r}' - .format(grpc_status))) + raise GRPCError(Status.UNKNOWN, + 'Invalid grpc-status: {!r}'.format(grpc_status), + http_details=HTTPDetails(headers_map.get(":status"), headers_map)) else: message, details = None, None if status is not Status.OK: @@ -339,10 +345,15 @@ def _process_grpc_status( return status, message, details def _raise_for_grpc_status( - self, status: Status, message: Optional[str], details: Any, + self, + status: Status, + message: Optional[str], + details: Any, + headers: Dict[str, str] = None ) -> None: if status is not Status.OK: - raise GRPCError(status, message, details) + http_status = headers.get(":status") if headers is not None else None + raise GRPCError(status, message, details, HTTPDetails(http_status, headers)) async def recv_initial_metadata(self) -> None: """Coroutine to wait for headers with initial metadata from the server. @@ -390,7 +401,7 @@ async def recv_initial_metadata(self) -> None: ) self.trailing_metadata = tm - self._raise_for_grpc_status(status, message, details) + self._raise_for_grpc_status(status, message, details, headers_map) else: im = decode_metadata(headers) im, = await self._dispatch.recv_initial_metadata(im) @@ -523,20 +534,22 @@ async def _maybe_finish(self) -> None: await self.recv_trailing_metadata() def _maybe_raise(self) -> None: + headers_map = {} if self._stream.headers is not None: - self._raise_for_status(dict(self._stream.headers)) + headers_map = dict(self._stream.headers) + self._raise_for_status(headers_map) if self._stream.trailers is not None: status, message, details = self._process_grpc_status( dict(self._stream.trailers), ) - self._raise_for_grpc_status(status, message, details) + self._raise_for_grpc_status(status, message, details, headers) elif self._stream.headers is not None: headers_map = dict(self._stream.headers) if 'grpc-status' in headers_map: status, message, details = self._process_grpc_status( headers_map, ) - self._raise_for_grpc_status(status, message, details) + self._raise_for_grpc_status(status, message, details, headers_map) async def __aexit__( self, diff --git a/grpclib/exceptions.py b/grpclib/exceptions.py index 72e7066..94cc720 100644 --- a/grpclib/exceptions.py +++ b/grpclib/exceptions.py @@ -1,8 +1,15 @@ -from typing import Optional, Any +from typing import Optional, Any, Dict +from dataclasses import dataclass, field from .const import Status +@dataclass(frozen=True) +class HTTPDetails: + status: str = field(default="") + headers: Dict[str, str] = field(default_factory=dict) + + class GRPCError(Exception): """Expected error, may be raised during RPC call @@ -26,11 +33,13 @@ class GRPCError(Exception): `(e.g. server returned unsupported` ``:content-type`` `header)` """ + def __init__( - self, - status: Status, - message: Optional[str] = None, - details: Any = None, + self, + status: Status, + message: Optional[str] = None, + details: Any = None, + http_details: HTTPDetails = None ) -> None: super().__init__(status, message, details) #: :py:class:`~grpclib.const.Status` of the error @@ -39,6 +48,8 @@ def __init__( self.message = message #: Error details self.details = details + #: Http details + self.http_details = http_details class ProtocolError(Exception):