Skip to content

Commit 6c95243

Browse files
Add support for debug in error details (#147)
Signed-off-by: Stefan VanBuren <svanburen@buf.build>
1 parent 853f975 commit 6c95243

2 files changed

Lines changed: 69 additions & 8 deletions

File tree

src/connectrpc/_protocol.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from http import HTTPStatus
77
from typing import TYPE_CHECKING, Protocol, TypeVar, cast
88

9+
from google.protobuf import symbol_database
910
from google.protobuf.any_pb2 import Any
11+
from google.protobuf.json_format import MessageToDict
1012

1113
from ._compression import Compression
1214
from .code import Code
@@ -157,19 +159,29 @@ def to_http_status(self) -> ExtendedHTTPStatus:
157159
def to_dict(self) -> dict:
158160
data: dict = {"code": self.code.value, "message": self.message}
159161
if self.details:
160-
details: list[dict[str, str]] = []
162+
details: list[dict] = []
161163
for detail in self.details:
162164
if detail.type_url.startswith("type.googleapis.com/"):
163165
detail_type = detail.type_url[len("type.googleapis.com/") :]
164166
else:
165167
detail_type = detail.type_url
166-
details.append(
167-
{
168-
"type": detail_type,
169-
# Connect requires unpadded base64
170-
"value": b64encode(detail.value).decode("utf-8").rstrip("="),
171-
}
172-
)
168+
detail_dict: dict = {
169+
"type": detail_type,
170+
# Connect requires unpadded base64
171+
"value": b64encode(detail.value).decode("utf-8").rstrip("="),
172+
}
173+
# Try to produce debug info, but expect failure when we don't
174+
# have descriptors for the message type.
175+
debug = None
176+
try:
177+
msg_instance = symbol_database.Default().GetSymbol(detail_type)()
178+
if detail.Unpack(msg_instance):
179+
debug = MessageToDict(msg_instance)
180+
except Exception:
181+
debug = None
182+
if debug is not None:
183+
detail_dict["debug"] = debug
184+
details.append(detail_dict)
173185
data["details"] = details
174186
return data
175187

test/test_details.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
from typing import NoReturn
44

55
import pytest
6+
from google.protobuf.any_pb2 import Any as AnyPb
7+
from google.protobuf.duration_pb2 import Duration
68
from google.protobuf.struct_pb2 import Struct, Value
79
from pyqwest import Client, SyncClient
810
from pyqwest.testing import ASGITransport, WSGITransport
911

12+
from connectrpc._protocol import ConnectWireError
1013
from connectrpc.code import Code
1114
from connectrpc.errors import ConnectError, pack_any
1215

@@ -81,3 +84,49 @@ async def make_hat(self, request, ctx) -> NoReturn:
8184
s1 = Struct()
8285
assert exc_info.value.details[1].Unpack(s1)
8386
assert s1.fields["color"].string_value == "red"
87+
88+
89+
def test_error_detail_debug_field() -> None:
90+
"""Debug field is populated when proto descriptors are available."""
91+
wire_error = ConnectWireError.from_exception(
92+
ConnectError(
93+
Code.RESOURCE_EXHAUSTED,
94+
"Resource exhausted",
95+
details=[Struct(fields={"animal": Value(string_value="bear")})],
96+
)
97+
)
98+
data = wire_error.to_dict()
99+
assert len(data["details"]) == 1
100+
detail = data["details"][0]
101+
assert "debug" in detail
102+
# Struct uses proto-JSON well-known type mapping: becomes a plain JSON object
103+
assert detail["debug"] == {"animal": "bear"}
104+
105+
106+
def test_error_detail_debug_field_well_known_type() -> None:
107+
"""Debug field uses proto-JSON well-known type representation (e.g. Duration as string)."""
108+
wire_error = ConnectWireError.from_exception(
109+
ConnectError(
110+
Code.RESOURCE_EXHAUSTED, "Resource exhausted", details=[Duration(seconds=1)]
111+
)
112+
)
113+
data = wire_error.to_dict()
114+
assert len(data["details"]) == 1
115+
detail = data["details"][0]
116+
assert "debug" in detail
117+
# Duration uses proto-JSON well-known type mapping: serializes as "1s"
118+
assert detail["debug"] == "1s"
119+
120+
121+
def test_error_detail_debug_field_absent_for_unknown_type() -> None:
122+
"""Debug field is omitted when no descriptor is available for the type."""
123+
unknown_detail = AnyPb(
124+
type_url="type.googleapis.com/completely.Unknown.Message", value=b"\x08\x01"
125+
)
126+
wire_error = ConnectWireError(
127+
code=Code.INTERNAL, message="test", details=[unknown_detail]
128+
)
129+
data = wire_error.to_dict()
130+
assert len(data["details"]) == 1
131+
detail = data["details"][0]
132+
assert "debug" not in detail

0 commit comments

Comments
 (0)