Skip to content
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
28 changes: 20 additions & 8 deletions src/connectrpc/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from http import HTTPStatus
from typing import TYPE_CHECKING, Protocol, TypeVar, cast

from google.protobuf import symbol_database
from google.protobuf.any_pb2 import Any
from google.protobuf.json_format import MessageToDict

from ._compression import Compression
from .code import Code
Expand Down Expand Up @@ -157,19 +159,29 @@ def to_http_status(self) -> ExtendedHTTPStatus:
def to_dict(self) -> dict:
data: dict = {"code": self.code.value, "message": self.message}
if self.details:
details: list[dict[str, str]] = []
details: list[dict] = []
for detail in self.details:
if detail.type_url.startswith("type.googleapis.com/"):
detail_type = detail.type_url[len("type.googleapis.com/") :]
else:
detail_type = detail.type_url
details.append(
{
"type": detail_type,
# Connect requires unpadded base64
"value": b64encode(detail.value).decode("utf-8").rstrip("="),
}
)
detail_dict: dict = {
"type": detail_type,
# Connect requires unpadded base64
"value": b64encode(detail.value).decode("utf-8").rstrip("="),
}
# Try to produce debug info, but expect failure when we don't
# have descriptors for the message type.
debug = None
try:
msg_instance = symbol_database.Default().GetSymbol(detail_type)()
if detail.Unpack(msg_instance):
debug = MessageToDict(msg_instance)
except Exception:
debug = None
if debug is not None:
detail_dict["debug"] = debug
details.append(detail_dict)
data["details"] = details
return data

Expand Down
49 changes: 49 additions & 0 deletions test/test_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
from typing import NoReturn

import pytest
from google.protobuf.any_pb2 import Any as AnyPb
from google.protobuf.duration_pb2 import Duration
from google.protobuf.struct_pb2 import Struct, Value
from pyqwest import Client, SyncClient
from pyqwest.testing import ASGITransport, WSGITransport

from connectrpc._protocol import ConnectWireError
from connectrpc.code import Code
from connectrpc.errors import ConnectError, pack_any

Expand Down Expand Up @@ -81,3 +84,49 @@ async def make_hat(self, request, ctx) -> NoReturn:
s1 = Struct()
assert exc_info.value.details[1].Unpack(s1)
assert s1.fields["color"].string_value == "red"


def test_error_detail_debug_field() -> None:
"""Debug field is populated when proto descriptors are available."""
wire_error = ConnectWireError.from_exception(
ConnectError(
Code.RESOURCE_EXHAUSTED,
"Resource exhausted",
details=[Struct(fields={"animal": Value(string_value="bear")})],
)
)
data = wire_error.to_dict()
assert len(data["details"]) == 1
detail = data["details"][0]
assert "debug" in detail
# Struct uses proto-JSON well-known type mapping: becomes a plain JSON object
assert detail["debug"] == {"animal": "bear"}


def test_error_detail_debug_field_well_known_type() -> None:
"""Debug field uses proto-JSON well-known type representation (e.g. Duration as string)."""
wire_error = ConnectWireError.from_exception(
ConnectError(
Code.RESOURCE_EXHAUSTED, "Resource exhausted", details=[Duration(seconds=1)]
)
)
data = wire_error.to_dict()
assert len(data["details"]) == 1
detail = data["details"][0]
assert "debug" in detail
# Duration uses proto-JSON well-known type mapping: serializes as "1s"
assert detail["debug"] == "1s"


def test_error_detail_debug_field_absent_for_unknown_type() -> None:
"""Debug field is omitted when no descriptor is available for the type."""
unknown_detail = AnyPb(
type_url="type.googleapis.com/completely.Unknown.Message", value=b"\x08\x01"
)
wire_error = ConnectWireError(
code=Code.INTERNAL, message="test", details=[unknown_detail]
)
data = wire_error.to_dict()
assert len(data["details"]) == 1
detail = data["details"][0]
assert "debug" not in detail