From a1d1bb4ad01d2e75fb9db9dc139643a5e864401e Mon Sep 17 00:00:00 2001 From: Jonatan Nevo Date: Tue, 21 Feb 2023 18:01:45 +0200 Subject: [PATCH 1/4] add http headers to GRPCError --- grpclib/client.py | 42 ++++++++++++++++++++++++++++++------------ grpclib/exceptions.py | 8 +++++++- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/grpclib/client.py b/grpclib/client.py index abf4a67..94564ab 100644 --- a/grpclib/client.py +++ b/grpclib/client.py @@ -294,13 +294,17 @@ 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), + headers=headers_map, + http_status=status) 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', + headers=headers_map, + http_status=headers_map.get(":status")) base_content_type, _, sub_type = content_type.partition('+') sub_type = sub_type or ProtoCodec.__content_subtype__ @@ -310,19 +314,26 @@ 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), + headers=headers_map, + http_status=headers_map.get(":status")) 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', + headers=headers_map, + http_status=headers_map.get(":status")) 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), + headers=headers_map, + http_status=headers_map.get(":status")) else: message, details = None, None if status is not Status.OK: @@ -339,10 +350,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) + status = headers.get(":status") if headers is not None else None + raise GRPCError(status, message, details, headers, http_status=status) async def recv_initial_metadata(self) -> None: """Coroutine to wait for headers with initial metadata from the server. @@ -390,7 +406,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 +539,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..44dbd5f 100644 --- a/grpclib/exceptions.py +++ b/grpclib/exceptions.py @@ -1,4 +1,4 @@ -from typing import Optional, Any +from typing import Optional, Any, Dict from .const import Status @@ -31,6 +31,8 @@ def __init__( status: Status, message: Optional[str] = None, details: Any = None, + headers: Dict[str, str] = None, + http_status: Optional[str] = None ) -> None: super().__init__(status, message, details) #: :py:class:`~grpclib.const.Status` of the error @@ -39,6 +41,10 @@ def __init__( self.message = message #: Error details self.details = details + #: http headers + self.headers = headers + #: http status + self.http_status = http_status class ProtocolError(Exception): From db09b3d4ae4e41e570f249b3dd35815188887c59 Mon Sep 17 00:00:00 2001 From: Jonatan Nevo Date: Wed, 22 Feb 2023 10:19:38 +0200 Subject: [PATCH 2/4] moved http details to dataclass --- grpclib/client.py | 19 +++++++------------ grpclib/exceptions.py | 25 +++++++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/grpclib/client.py b/grpclib/client.py index 94564ab..6fb64d2 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 @@ -295,16 +295,14 @@ def _raise_for_status(self, headers_map: Dict[str, str]) -> None: grpc_status = _H2_TO_GRPC_STATUS_MAP.get(status, Status.UNKNOWN) raise GRPCError(grpc_status, 'Received :status = {!r}'.format(status), - headers=headers_map, - http_status=status) + 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', - headers=headers_map, - http_status=headers_map.get(":status")) + HTTPDetails(headers_map.get(":status"), headers_map)) base_content_type, _, sub_type = content_type.partition('+') sub_type = sub_type or ProtoCodec.__content_subtype__ @@ -315,8 +313,7 @@ def _raise_for_content_type(self, headers_map: Dict[str, str]) -> None: raise GRPCError(Status.UNKNOWN, 'Invalid content-type: {!r}' .format(content_type), - headers=headers_map, - http_status=headers_map.get(":status")) + HTTPDetails(headers_map.get(":status"), headers_map)) def _process_grpc_status( self, headers_map: Dict[str, str], @@ -325,15 +322,13 @@ def _process_grpc_status( if grpc_status is None: raise GRPCError(Status.UNKNOWN, 'Missing grpc-status header', - headers=headers_map, - http_status=headers_map.get(":status")) + 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), - headers=headers_map, - http_status=headers_map.get(":status")) + HTTPDetails(headers_map.get(":status"), headers_map)) else: message, details = None, None if status is not Status.OK: @@ -358,7 +353,7 @@ def _raise_for_grpc_status( ) -> None: if status is not Status.OK: status = headers.get(":status") if headers is not None else None - raise GRPCError(status, message, details, headers, http_status=status) + raise GRPCError(status, message, details, HTTPDetails(status, headers)) async def recv_initial_metadata(self) -> None: """Coroutine to wait for headers with initial metadata from the server. diff --git a/grpclib/exceptions.py b/grpclib/exceptions.py index 44dbd5f..94cc720 100644 --- a/grpclib/exceptions.py +++ b/grpclib/exceptions.py @@ -1,8 +1,15 @@ 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,13 +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, - headers: Dict[str, str] = None, - http_status: Optional[str] = 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 @@ -41,10 +48,8 @@ def __init__( self.message = message #: Error details self.details = details - #: http headers - self.headers = headers - #: http status - self.http_status = http_status + #: Http details + self.http_details = http_details class ProtocolError(Exception): From afe3af9bac62a446a46298f6dc8bfbffa834924f Mon Sep 17 00:00:00 2001 From: Jonatan Nevo Date: Wed, 22 Feb 2023 10:36:14 +0200 Subject: [PATCH 3/4] fix bug with passing incorrect status to GRPCError --- grpclib/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grpclib/client.py b/grpclib/client.py index 6fb64d2..8f81a79 100644 --- a/grpclib/client.py +++ b/grpclib/client.py @@ -352,8 +352,8 @@ def _raise_for_grpc_status( headers: Dict[str, str] = None ) -> None: if status is not Status.OK: - status = headers.get(":status") if headers is not None else None - raise GRPCError(status, message, details, HTTPDetails(status, headers)) + 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. From 7e2ed1a412e2c6bf5e883ecebd07139ac3457b13 Mon Sep 17 00:00:00 2001 From: Jonatan Nevo Date: Wed, 22 Feb 2023 10:44:24 +0200 Subject: [PATCH 4/4] add kwargs to HTTPDetails --- grpclib/client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/grpclib/client.py b/grpclib/client.py index 8f81a79..2f36c9b 100644 --- a/grpclib/client.py +++ b/grpclib/client.py @@ -295,14 +295,14 @@ def _raise_for_status(self, headers_map: Dict[str, str]) -> None: grpc_status = _H2_TO_GRPC_STATUS_MAP.get(status, Status.UNKNOWN) raise GRPCError(grpc_status, 'Received :status = {!r}'.format(status), - HTTPDetails(status, headers_map)) + 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', - HTTPDetails(headers_map.get(":status"), headers_map)) + 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__ @@ -313,7 +313,7 @@ def _raise_for_content_type(self, headers_map: Dict[str, str]) -> None: raise GRPCError(Status.UNKNOWN, 'Invalid content-type: {!r}' .format(content_type), - HTTPDetails(headers_map.get(":status"), headers_map)) + http_details=HTTPDetails(headers_map.get(":status"), headers_map)) def _process_grpc_status( self, headers_map: Dict[str, str], @@ -322,13 +322,13 @@ def _process_grpc_status( if grpc_status is None: raise GRPCError(Status.UNKNOWN, 'Missing grpc-status header', - HTTPDetails(headers_map.get(":status"), headers_map)) + 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), - HTTPDetails(headers_map.get(":status"), headers_map)) + http_details=HTTPDetails(headers_map.get(":status"), headers_map)) else: message, details = None, None if status is not Status.OK: