Skip to main content

structured_proxy/transcode/
error.rs

1//! gRPC → HTTP error mapping.
2//!
3//! Converts `tonic::Status` to appropriate HTTP status codes and JSON error bodies
4//! following the gRPC-HTTP status code mapping from the gRPC specification, and
5//! renders the typed `google.rpc.Status` details the upstream attached in the
6//! `grpc-status-details-bin` trailer.
7
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use axum::Json;
11use base64::Engine as _;
12use globset::GlobMatcher;
13use prost::Message as _;
14use prost_reflect::{
15    Cardinality, DescriptorPool, DynamicMessage, Kind, MessageDescriptor, ReflectMessage,
16    SerializeOptions, Value as PbValue,
17};
18use serde_json::{Map, Value};
19
20/// Full name of `google.rpc.DebugInfo`. It carries stack traces and server
21/// internals meant for the service's operators, so it never reaches an HTTP
22/// client.
23const DEBUG_INFO: &str = "google.rpc.DebugInfo";
24
25/// Map a gRPC status code to the corresponding HTTP status code.
26///
27/// Based on <https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md>
28pub fn grpc_to_http_status(code: tonic::Code) -> StatusCode {
29    match code {
30        tonic::Code::Ok => StatusCode::OK,
31        tonic::Code::Cancelled => StatusCode::from_u16(499).unwrap_or(StatusCode::BAD_REQUEST),
32        tonic::Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
33        tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
34        tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
35        tonic::Code::NotFound => StatusCode::NOT_FOUND,
36        tonic::Code::AlreadyExists => StatusCode::CONFLICT,
37        tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
38        tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
39        tonic::Code::FailedPrecondition => StatusCode::BAD_REQUEST,
40        tonic::Code::Aborted => StatusCode::CONFLICT,
41        tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
42        tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
43        tonic::Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
44        tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
45        tonic::Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
46        tonic::Code::Unauthenticated => StatusCode::UNAUTHORIZED,
47    }
48}
49
50/// Message of the `INTERNAL` a client gets instead of an upstream error whose
51/// details cannot be rendered faithfully. The cause stays in the proxy's log.
52const MALFORMED_STATUS_MESSAGE: &str = "upstream returned a malformed error status";
53
54/// An upstream error status whose details cannot be rendered faithfully: a
55/// trailer that is not a `google.rpc.Status`, or a detail of a known type whose
56/// bytes do not decode or whose value has no valid JSON form. Its cause is
57/// logged where it is detected.
58#[derive(Debug)]
59struct MalformedStatus;
60
61/// Convert a `tonic::Status` into an axum HTTP response with a JSON error body
62/// `{"error", "message", "code"}`, without status details.
63///
64/// Use [`status_to_response_with_details`] to also render the upstream's
65/// `google.rpc.Status` details.
66pub fn status_to_response(status: tonic::Status) -> Response {
67    status_to_response_with_details(&status, None)
68}
69
70/// Convert a `tonic::Status` into an axum HTTP response with a JSON error body.
71///
72/// The body is `{"error", "message", "code"}`, plus a `details` array when
73/// `details` is given (see [`error_body`]). The HTTP status follows the code the
74/// body reports, so a malformed upstream status answers 500.
75pub fn status_to_response_with_details(
76    status: &tonic::Status,
77    details: Option<&StatusDetails>,
78) -> Response {
79    render_response(status, details).0
80}
81
82/// [`status_to_response_with_details`], plus whether the response reports the
83/// upstream's own error: `false` when its details could not be rendered
84/// faithfully and the response is the generic `INTERNAL` instead, which then
85/// carries nothing else from the upstream either.
86pub(crate) fn render_response(
87    status: &tonic::Status,
88    details: Option<&StatusDetails>,
89) -> (Response, bool) {
90    let (code, body, faithful) = render(status, details);
91    (
92        (grpc_to_http_status(code), Json(body)).into_response(),
93        faithful,
94    )
95}
96
97/// Message of the `INTERNAL` a client gets instead of a successful upstream
98/// answer the proxy cannot turn into a faithful HTTP response (an invalid
99/// `x-http-code`, an `HttpBody` content type that is not a header value).
100const MALFORMED_RESPONSE_MESSAGE: &str = "upstream returned a malformed response";
101
102/// The `INTERNAL` (500) answering a successful upstream call whose response
103/// cannot be passed on faithfully, in the route's error body. Like a malformed
104/// error status, nothing of the upstream's answer reaches the client; the
105/// caller logs the cause.
106pub(crate) fn malformed_response(details: Option<&StatusDetails>) -> Response {
107    let body = body(
108        tonic::Code::Internal,
109        MALFORMED_RESPONSE_MESSAGE,
110        details.map(|_| RenderedDetails::default()),
111    );
112    (StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response()
113}
114
115/// The error body of a request a guard turned away: the shape of the
116/// transcoder's own errors, with empty `details`.
117pub(crate) fn guard_error_body(code: tonic::Code, message: &str) -> Value {
118    body(code, message, Some(RenderedDetails::default()))
119}
120
121/// The JSON error body for a failed call, shared by the unary response and the
122/// terminal frame of a stream so a client parses one shape everywhere.
123///
124/// `error` is the gRPC code name, `code` its number and `message` the status
125/// message. With `details`, the body also carries `details`: the upstream's
126/// `google.rpc.Status.details` in proto3 JSON form (empty when the upstream sent
127/// none), plus `opaqueDetails` for details whose type no descriptor describes
128/// when the renderer forwards them ([`StatusDetails::with_opaque_details`]);
129/// without it, both keys are absent and the trailer is not read. When the
130/// details cannot be rendered faithfully the whole body is a generic `INTERNAL`
131/// instead, never a partial or reinterpreted set of details.
132pub fn error_body(status: &tonic::Status, details: Option<&StatusDetails>) -> Value {
133    render(status, details).1
134}
135
136/// The error body, the gRPC code it reports and whether that is the
137/// upstream's own error: `false` when its details cannot be rendered
138/// faithfully and the body is the generic `INTERNAL`.
139fn render(status: &tonic::Status, details: Option<&StatusDetails>) -> (tonic::Code, Value, bool) {
140    let Some(details) = details else {
141        return (
142            status.code(),
143            body(status.code(), status.message(), None),
144            true,
145        );
146    };
147    match details.render(status) {
148        Ok(rendered) => (
149            status.code(),
150            body(status.code(), status.message(), Some(rendered)),
151            true,
152        ),
153        Err(MalformedStatus) => (
154            tonic::Code::Internal,
155            body(
156                tonic::Code::Internal,
157                MALFORMED_STATUS_MESSAGE,
158                Some(RenderedDetails::default()),
159            ),
160            false,
161        ),
162    }
163}
164
165/// `opaqueDetails` appears only when a detail went to the extension, so a
166/// client that does not handle it sees nothing new otherwise.
167fn body(code: tonic::Code, message: &str, details: Option<RenderedDetails>) -> Value {
168    let mut body = Map::with_capacity(5);
169    body.insert("error".into(), grpc_code_name(code).into());
170    body.insert("message".into(), message.into());
171    body.insert("code".into(), (code as i32).into());
172    if let Some(rendered) = details {
173        body.insert("details".into(), Value::Array(rendered.details));
174        if !rendered.opaque.is_empty() {
175            body.insert("opaqueDetails".into(), Value::Array(rendered.opaque));
176        }
177    }
178    Value::Object(body)
179}
180
181/// Human-readable gRPC code name for JSON error responses.
182pub(crate) fn grpc_code_name(code: tonic::Code) -> &'static str {
183    match code {
184        tonic::Code::Ok => "OK",
185        tonic::Code::Cancelled => "CANCELLED",
186        tonic::Code::Unknown => "UNKNOWN",
187        tonic::Code::InvalidArgument => "INVALID_ARGUMENT",
188        tonic::Code::DeadlineExceeded => "DEADLINE_EXCEEDED",
189        tonic::Code::NotFound => "NOT_FOUND",
190        tonic::Code::AlreadyExists => "ALREADY_EXISTS",
191        tonic::Code::PermissionDenied => "PERMISSION_DENIED",
192        tonic::Code::ResourceExhausted => "RESOURCE_EXHAUSTED",
193        tonic::Code::FailedPrecondition => "FAILED_PRECONDITION",
194        tonic::Code::Aborted => "ABORTED",
195        tonic::Code::OutOfRange => "OUT_OF_RANGE",
196        tonic::Code::Unimplemented => "UNIMPLEMENTED",
197        tonic::Code::Internal => "INTERNAL",
198        tonic::Code::Unavailable => "UNAVAILABLE",
199        tonic::Code::DataLoss => "DATA_LOSS",
200        tonic::Code::Unauthenticated => "UNAUTHENTICATED",
201    }
202}
203
204/// Which routes return `details` in their error bodies.
205///
206/// A global switch plus per-route overrides, checked in the order they were
207/// added: the first whose pattern matches the route decides. A pattern is a
208/// glob over the mounted route path, where `*` stays within one segment and
209/// `**` spans segments; every path parameter counts as one segment, so
210/// `/v1/users/*` matches the route `/v1/users/{id}`. Decided once per route
211/// when the router is built, so a request pays nothing for it.
212///
213/// # Examples
214///
215/// ```
216/// use structured_proxy::transcode::error::ErrorDetailsPolicy;
217///
218/// // Details everywhere except the internal admin surface.
219/// let policy = ErrorDetailsPolicy::default()
220///     .route("/v1/admin/**", false)
221///     .unwrap();
222/// assert!(policy.enabled_for("/v1/users/{id}"));
223/// assert!(!policy.enabled_for("/v1/admin/users/{id}"));
224///
225/// // Off by default, back on for one sub-route.
226/// let policy = ErrorDetailsPolicy::disabled()
227///     .route("/v1/public/**", true)
228///     .unwrap();
229/// assert!(!policy.enabled_for("/v1/users/{id}"));
230/// assert!(policy.enabled_for("/v1/public/items"));
231///
232/// // The opaque-detail extension, off unless switched on, here for one route.
233/// let policy = ErrorDetailsPolicy::default()
234///     .opaque_route("/v1/partner/**", true)
235///     .unwrap();
236/// assert!(!policy.opaque_for("/v1/users/{id}"));
237/// assert!(policy.opaque_for("/v1/partner/orders"));
238/// ```
239#[derive(Debug, Clone)]
240pub struct ErrorDetailsPolicy {
241    enabled: bool,
242    opaque: bool,
243    routes: Vec<RouteRule>,
244}
245
246/// One override: a route glob and the switches it sets. A switch it leaves
247/// `None` is decided by a later rule or the global value.
248#[derive(Debug, Clone)]
249struct RouteRule {
250    matcher: GlobMatcher,
251    enabled: Option<bool>,
252    opaque: Option<bool>,
253}
254
255impl ErrorDetailsPolicy {
256    /// No details on any route, until a [`route`](Self::route) switches them on.
257    pub fn disabled() -> Self {
258        Self {
259            enabled: false,
260            opaque: false,
261            routes: Vec::new(),
262        }
263    }
264
265    /// Add an override for the routes `pattern` matches, checked after the
266    /// ones added before it.
267    ///
268    /// # Errors
269    ///
270    /// A pattern that does not start with `/` (it could never match a route)
271    /// or is not a valid glob.
272    pub fn route(self, pattern: &str, enabled: bool) -> Result<Self, String> {
273        self.rule(pattern, Some(enabled), None)
274    }
275
276    /// Set, for every route without an override, whether a detail whose type
277    /// no descriptor describes goes to `opaqueDetails` (see
278    /// [`StatusDetails::with_opaque_details`]). Off by default: such bytes
279    /// cannot be checked for a nested DebugInfo.
280    pub fn with_opaque_details(mut self, enabled: bool) -> Self {
281        self.opaque = enabled;
282        self
283    }
284
285    /// Add an override of the opaque-detail extension for the routes `pattern`
286    /// matches, checked after the ones added before it.
287    ///
288    /// # Errors
289    ///
290    /// As [`route`](Self::route).
291    pub fn opaque_route(self, pattern: &str, enabled: bool) -> Result<Self, String> {
292        self.rule(pattern, None, Some(enabled))
293    }
294
295    /// Add an override setting either switch or both; the first matching rule
296    /// that sets a switch decides it.
297    ///
298    /// # Errors
299    ///
300    /// As [`route`](Self::route).
301    pub(crate) fn rule(
302        mut self,
303        pattern: &str,
304        enabled: Option<bool>,
305        opaque: Option<bool>,
306    ) -> Result<Self, String> {
307        // Route paths always start with `/`; a relative pattern is a
308        // missing-slash typo that would silently never apply.
309        if !pattern.starts_with('/') {
310            return Err(format!(
311                "error details route pattern {pattern:?} must start with '/'"
312            ));
313        }
314        self.routes.push(RouteRule {
315            matcher: crate::shield::matcher::path_glob(pattern)?,
316            enabled,
317            opaque,
318        });
319        Ok(self)
320    }
321
322    /// Whether the route mounted at `route_path` (axum form, e.g.
323    /// `/v1/users/{id}`) returns details: the first matching rule that sets
324    /// it decides, otherwise the global switch.
325    pub fn enabled_for(&self, route_path: &str) -> bool {
326        self.decide(route_path, |rule| rule.enabled)
327            .unwrap_or(self.enabled)
328    }
329
330    /// Whether the route mounted at `route_path` forwards details of unknown
331    /// types in `opaqueDetails`: the first matching rule that sets it decides,
332    /// otherwise the global switch.
333    pub fn opaque_for(&self, route_path: &str) -> bool {
334        self.decide(route_path, |rule| rule.opaque)
335            .unwrap_or(self.opaque)
336    }
337
338    fn decide(
339        &self,
340        route_path: &str,
341        switch: impl Fn(&RouteRule) -> Option<bool>,
342    ) -> Option<bool> {
343        self.routes
344            .iter()
345            .filter_map(|rule| switch(rule).map(|value| (rule, value)))
346            .find(|(rule, _)| rule.matcher.is_match(route_path))
347            .map(|(_, value)| value)
348    }
349}
350
351impl Default for ErrorDetailsPolicy {
352    /// Details on every route, the opaque-detail extension off.
353    fn default() -> Self {
354        Self {
355            enabled: true,
356            opaque: false,
357            routes: Vec::new(),
358        }
359    }
360}
361
362/// Whether `full_name` is a well-known type with a special ProtoJSON
363/// representation, which an `Any` carries under `value`
364/// (<https://protobuf.dev/programming-guides/json/#any>). The same set
365/// prost-reflect wraps when it serializes an `Any`.
366fn has_special_json(full_name: &str) -> bool {
367    matches!(
368        full_name,
369        "google.protobuf.Any"
370            | "google.protobuf.Timestamp"
371            | "google.protobuf.Duration"
372            | "google.protobuf.Struct"
373            | "google.protobuf.FloatValue"
374            | "google.protobuf.DoubleValue"
375            | "google.protobuf.Int32Value"
376            | "google.protobuf.Int64Value"
377            | "google.protobuf.UInt32Value"
378            | "google.protobuf.UInt64Value"
379            | "google.protobuf.BoolValue"
380            | "google.protobuf.StringValue"
381            | "google.protobuf.BytesValue"
382            | "google.protobuf.FieldMask"
383            | "google.protobuf.ListValue"
384            | "google.protobuf.Value"
385            | "google.protobuf.Empty"
386    )
387}
388
389/// Full name of `google.protobuf.Any`.
390const ANY: &str = "google.protobuf.Any";
391
392/// The type name an `Any.type_url` names, or `None` when the URL is malformed.
393///
394/// The URL must contain a `/`, and the part after the last one is the type's
395/// full name (`google/protobuf/any.proto`). Anything else (a bare name, a
396/// trailing `/`, a query suffix, an empty segment) could disguise a
397/// DebugInfo, so callers refuse the whole status on `None`.
398fn any_type_name(type_url: &str) -> Option<&str> {
399    let (_, name) = type_url.rsplit_once('/')?;
400    is_full_name(name).then_some(name)
401}
402
403/// Whether `name` is a protobuf full name: dot-separated identifiers, each a
404/// letter or `_` followed by letters, digits or `_`.
405fn is_full_name(name: &str) -> bool {
406    !name.is_empty()
407        && name.split('.').all(|part| {
408            let mut chars = part.chars();
409            matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
410                && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
411        })
412}
413
414/// The rendered details of one status.
415#[derive(Debug, Default)]
416struct RenderedDetails {
417    /// ProtoJSON `Any` entries, in upstream order.
418    details: Vec<Value>,
419    /// Opaque-detail extension entries, in upstream order.
420    opaque: Vec<Value>,
421}
422
423/// An entry of the structured-proxy opaque-detail extension, for a detail whose
424/// type no descriptor describes: `{"index", "typeUrl", "bytes"}`, where `index`
425/// is its position among the forwarded details (so merging `details` and
426/// `opaqueDetails` by position restores the upstream order), `typeUrl` the
427/// original type URL and `bytes` the standard base64 of the original bytes.
428/// It carries no `@type` and lives outside `details`, so it cannot be taken for
429/// a ProtoJSON `Any`.
430fn opaque_entry(index: usize, type_url: &str, value: &[u8]) -> Value {
431    let mut out = Map::with_capacity(3);
432    out.insert("index".into(), index.into());
433    out.insert("typeUrl".into(), type_url.into());
434    out.insert(
435        "bytes".into(),
436        base64::engine::general_purpose::STANDARD
437            .encode(value)
438            .into(),
439    );
440    Value::Object(out)
441}
442
443/// Add to `pool` every top-level message and enum of `canonical` it does not
444/// define, per type rather than per file.
445///
446/// A product may define some canonical types itself: in a file of another name,
447/// or in its own revision of the canonical file holding only a subset. Those
448/// definitions win. The canonical types it lacks still go in, through a copy
449/// of the canonical file renamed under `structured-proxy/canonical/` and
450/// reduced to the missing types; the copy also imports the product files that
451/// define the types it dropped, so its references resolve to the product's
452/// revisions.
453fn complete_with_canonical(pool: &mut DescriptorPool, canonical: &DescriptorPool) {
454    use std::collections::{BTreeSet, HashMap};
455
456    // Canonical file name → the pool files that now provide its types in its
457    // place (its renamed copy, product files defining some of them). A file
458    // importing it imports those instead.
459    let mut stand_ins: HashMap<String, BTreeSet<String>> = HashMap::new();
460    // `files()` lists dependencies before their dependents, so each file finds
461    // its imports already merged.
462    for file in canonical.files() {
463        let mut proto = file.file_descriptor_proto().clone();
464        let package = proto.package().to_owned();
465        let full = |name: &str| {
466            if package.is_empty() {
467                name.to_owned()
468            } else {
469                format!("{package}.{name}")
470            }
471        };
472        // Files of the pool that already define a type of this file.
473        let mut defining_files = BTreeSet::new();
474        proto.message_type.retain(|message| {
475            match pool.get_message_by_name(&full(message.name())) {
476                Some(existing) => {
477                    defining_files.insert(existing.parent_file().name().to_owned());
478                    false
479                }
480                None => true,
481            }
482        });
483        proto
484            .enum_type
485            .retain(|en| match pool.get_enum_by_name(&full(en.name())) {
486                Some(existing) => {
487                    defining_files.insert(existing.parent_file().name().to_owned());
488                    false
489                }
490                None => true,
491            });
492        let everything_present = proto.message_type.is_empty() && proto.enum_type.is_empty();
493        let file_present = pool.get_file_by_name(file.name()).is_some();
494        if everything_present {
495            // Typically the very same file. When the types live in files of
496            // other names, importers of this one import those instead.
497            if !file_present {
498                stand_ins.insert(file.name().to_owned(), defining_files);
499            }
500            continue;
501        }
502        // A copy that leaves types out, or whose name the product already uses,
503        // goes in under its own name; importers then need it plus the product
504        // files that define the rest.
505        let renamed = !defining_files.is_empty() || file_present;
506        let mut provided_by = defining_files.clone();
507        if renamed {
508            let name = format!("structured-proxy/canonical/{}", file.name());
509            proto.name = Some(name.clone());
510            provided_by.insert(name);
511        }
512        let mut dependencies = BTreeSet::new();
513        for dep in &proto.dependency {
514            match stand_ins.get(dep) {
515                Some(files) => {
516                    // A product revision of the dependency stays importable
517                    // next to the copy that completes it.
518                    if pool.get_file_by_name(dep).is_some() {
519                        dependencies.insert(dep.clone());
520                    }
521                    dependencies.extend(files.iter().cloned());
522                }
523                None => {
524                    dependencies.insert(dep.clone());
525                }
526            }
527        }
528        dependencies.extend(defining_files);
529        proto.dependency = dependencies.into_iter().collect();
530        // Indexes into the old dependency list; nothing here needs them.
531        proto.public_dependency.clear();
532        proto.weak_dependency.clear();
533        proto.source_code_info = None;
534        match pool.add_file_descriptor_proto(proto) {
535            Ok(()) => {
536                if renamed {
537                    stand_ins.insert(file.name().to_owned(), provided_by);
538                }
539            }
540            Err(e) => {
541                tracing::debug!(file = %file.name(), "canonical descriptor not merged: {e}");
542            }
543        }
544    }
545}
546
547/// Renders the typed details of a gRPC status (`grpc-status-details-bin`) as
548/// proto3 JSON.
549///
550/// Detail types resolve in one pool: the product descriptors, completed with
551/// the well-known types and the canonical `google/rpc/status.proto` and
552/// `error_details.proto` for whatever the product does not define itself. A
553/// service's own detail messages (and its own `google.rpc` revision) render as
554/// it defines them, the canonical ones are always available even when the
555/// product descriptors do not import them, and a type packed inside another
556/// detail resolves from the same pool as the detail itself.
557///
558/// Details use the canonical proto3 JSON mapping (unset fields omitted, 64-bit
559/// integers as strings), the form clients of the `google.rpc` model expect.
560#[derive(Debug, Clone)]
561pub struct StatusDetails {
562    pool: DescriptorPool,
563    opaque: bool,
564}
565
566impl StatusDetails {
567    /// Build a renderer over `product`, completed with the canonical
568    /// descriptors it lacks. The opaque-detail extension starts off.
569    pub fn new(product: &DescriptorPool) -> Self {
570        let mut canonical = DescriptorPool::global();
571        canonical
572            .decode_file_descriptor_set(tonic_types::pb::FILE_DESCRIPTOR_SET)
573            .expect("tonic-types ships a valid google.rpc descriptor set");
574        // The global pool is process-wide; another crate may have added it.
575        if canonical
576            .get_message_by_name(super::httpbody::HTTP_BODY)
577            .is_none()
578        {
579            canonical
580                .add_file_descriptor_proto(super::httpbody::file_descriptor())
581                .expect("google/api/httpbody.proto is a valid descriptor");
582        }
583        let mut pool = product.clone();
584        complete_with_canonical(&mut pool, &canonical);
585        Self {
586            pool,
587            opaque: false,
588        }
589    }
590
591    /// Whether a detail whose type no descriptor describes goes to
592    /// `opaqueDetails` (`{"index", "typeUrl", "bytes"}` entries: its position
593    /// among the forwarded details, its type URL and the standard base64 of
594    /// its bytes) instead of being withheld. Its
595    /// bytes cannot be inspected, so a DebugInfo it carries in a field would
596    /// reach the client: switch it on only for upstreams trusted not to nest
597    /// one in types the proxy has no descriptor for.
598    pub fn with_opaque_details(mut self, enabled: bool) -> Self {
599        self.opaque = enabled;
600        self
601    }
602
603    /// The details of `status`, `google.rpc.DebugInfo` left out.
604    ///
605    /// A detail whose type resolves goes to `details` in its ProtoJSON `Any`
606    /// form: `@type` plus the message fields, or `@type` plus `value` for a
607    /// well-known type with a special JSON representation. A type no descriptor
608    /// describes has no ProtoJSON form (the mapping requires the type) and its
609    /// bytes cannot be checked for a DebugInfo, so it is withheld, or goes to
610    /// the opaque-detail extension (see [`opaque_entry`]) when switched on.
611    ///
612    /// # Errors
613    ///
614    /// [`MalformedStatus`] when the trailer is not a `google.rpc.Status`, or a
615    /// detail of a known type does not decode or has no valid JSON form. Such a
616    /// detail is never passed on as opaque bytes, since that would present a
617    /// broken value as an unknown one.
618    fn render(&self, status: &tonic::Status) -> Result<RenderedDetails, MalformedStatus> {
619        let mut rendered = RenderedDetails::default();
620        let raw = status.details();
621        if raw.is_empty() {
622            return Ok(rendered);
623        }
624        let decoded = tonic_types::pb::Status::decode(raw).map_err(|e| {
625            tracing::error!("malformed grpc-status-details-bin trailer: {e}");
626            MalformedStatus
627        })?;
628        // The trailer must describe the same error as grpc-status and
629        // grpc-message (gRPC richer error model); otherwise its details would
630        // be attached to an error they were not written for.
631        if decoded.code != status.code() as i32 || decoded.message != status.message() {
632            tracing::error!(
633                trailer_code = decoded.code,
634                status_code = status.code() as i32,
635                "grpc-status-details-bin disagrees with grpc-status / grpc-message"
636            );
637            return Err(MalformedStatus);
638        }
639        rendered.details.reserve(decoded.details.len());
640        // Position among the forwarded details: DebugInfo takes no index, so
641        // the numbering does not reveal that one was withheld.
642        let mut index = 0usize;
643        for any in &decoded.details {
644            let type_url = any.type_url.as_str();
645            let Some(type_name) = any_type_name(type_url) else {
646                tracing::error!(%type_url, "error detail with a malformed type URL");
647                return Err(MalformedStatus);
648            };
649            if type_name == DEBUG_INFO {
650                continue;
651            }
652            match self.resolve(type_name) {
653                Some(desc) => match self.typed_entry(type_url, type_name, desc, &any.value)? {
654                    Some(entry) => rendered.details.push(entry),
655                    // An Any detail packing DebugInfo, withheld like a direct one.
656                    None => continue,
657                },
658                None if self.opaque => {
659                    rendered
660                        .opaque
661                        .push(opaque_entry(index, type_url, &any.value));
662                }
663                None => {
664                    tracing::debug!(%type_url, "error detail of an unknown type withheld");
665                    continue;
666                }
667            }
668            index += 1;
669        }
670        Ok(rendered)
671    }
672
673    /// The ProtoJSON `Any` form of a detail whose type resolved, with every
674    /// DebugInfo packed inside it removed; `None` when the detail is itself an
675    /// `Any` packing a DebugInfo.
676    fn typed_entry(
677        &self,
678        type_url: &str,
679        type_name: &str,
680        desc: MessageDescriptor,
681        value: &[u8],
682    ) -> Result<Option<Value>, MalformedStatus> {
683        // ProtoJSON puts a well-known type with a special JSON representation
684        // under `value` whatever that JSON looks like (a Struct is an object,
685        // yet still wrapped), so the choice follows the type, not the shape.
686        // Empty is one of them (JSON `{}` in the ProtoJSON table), as in Go's
687        // protojson and prost-reflect, so it is wrapped too.
688        let wrapped = has_special_json(desc.full_name());
689        let mut msg = DynamicMessage::decode(desc, value).map_err(|e| {
690            tracing::error!(detail = %type_name, "undecodable error detail: {e}");
691            MalformedStatus
692        })?;
693        if self.scrub(&mut msg)?.is_none() {
694            return Ok(None);
695        }
696        let json = msg
697            .serialize_with_options(serde_json::value::Serializer, &SerializeOptions::new())
698            .map_err(|e| {
699                tracing::error!(detail = %type_name, "error detail has no valid JSON form: {e}");
700                MalformedStatus
701            })?;
702        let mut out = Map::new();
703        out.insert("@type".into(), type_url.into());
704        match json {
705            Value::Object(fields) if !wrapped => {
706                // A field whose JSON name is `@type` would replace the Any's
707                // own type URL, and there is no faithful ProtoJSON form for it.
708                if fields.contains_key("@type") {
709                    tracing::error!(detail = %type_name, "error detail has a field named @type");
710                    return Err(MalformedStatus);
711                }
712                out.extend(fields);
713            }
714            other => {
715                out.insert("value".into(), other);
716            }
717        }
718        Ok(Some(Value::Object(out)))
719    }
720
721    fn resolve(&self, type_name: &str) -> Option<MessageDescriptor> {
722        self.pool.get_message_by_name(type_name)
723    }
724
725    /// Remove every DebugInfo anywhere below `msg`: a message field typed as
726    /// DebugInfo, or an `Any` packing one, whether singular, repeated or a map
727    /// value. Only the message types decide, so data such as a `Struct` key
728    /// named `@type` is never mistaken for one. Returns `None` when `msg` is
729    /// itself a DebugInfo or an `Any` packing one (the caller drops it),
730    /// otherwise whether anything changed. A message missing a proto2
731    /// `required` field is [`MalformedStatus`]: decoding accepts it, but it has
732    /// no valid ProtoJSON form.
733    fn scrub(&self, msg: &mut DynamicMessage) -> Result<Option<bool>, MalformedStatus> {
734        let desc = msg.descriptor();
735        if desc.full_name() == ANY {
736            return self.scrub_any(msg);
737        }
738        if desc.full_name() == DEBUG_INFO {
739            return Ok(None);
740        }
741        for field in desc.fields() {
742            if field.cardinality() == Cardinality::Required && !msg.has_field(&field) {
743                tracing::error!(
744                    message = %desc.full_name(),
745                    field = %field.name(),
746                    "error detail lacks a required field"
747                );
748                return Err(MalformedStatus);
749            }
750        }
751        let mut changed = false;
752        let mut dropped_fields = Vec::new();
753        for (field, value) in msg.fields_mut() {
754            if !matches!(field.kind(), Kind::Message(_)) {
755                continue;
756            }
757            if !self.scrub_value(value, &mut changed)? {
758                // A proto2 `required` field cannot be cleared without making
759                // the message invalid, so the whole message goes instead.
760                if field.cardinality() == Cardinality::Required {
761                    return Ok(None);
762                }
763                dropped_fields.push(field);
764            }
765        }
766        // Extensions are not among the message's own fields, yet ProtoJSON
767        // renders them (as `[full.name]`), so they are scrubbed the same way.
768        let mut dropped_extensions = Vec::new();
769        for (extension, value) in msg.extensions_mut() {
770            if !matches!(extension.kind(), Kind::Message(_)) {
771                continue;
772            }
773            if !self.scrub_value(value, &mut changed)? {
774                dropped_extensions.push(extension);
775            }
776        }
777        for field in dropped_fields {
778            msg.clear_field(&field);
779            changed = true;
780        }
781        for extension in dropped_extensions {
782            msg.clear_extension(&extension);
783            changed = true;
784        }
785        Ok(Some(changed))
786    }
787
788    /// [`scrub`](Self::scrub) applied to one set field or extension value:
789    /// repeated elements and map entries that must go are removed in place.
790    /// Returns `false` when the value is a singular message that must go, so
791    /// the caller clears the field.
792    fn scrub_value(
793        &self,
794        value: &mut PbValue,
795        changed: &mut bool,
796    ) -> Result<bool, MalformedStatus> {
797        match value {
798            PbValue::Message(inner) => match self.scrub(inner)? {
799                Some(inner_changed) => *changed |= inner_changed,
800                None => return Ok(false),
801            },
802            PbValue::List(items) => {
803                let before = items.len();
804                let mut failed = None;
805                items.retain_mut(|item| match item {
806                    PbValue::Message(inner) if failed.is_none() => match self.scrub(inner) {
807                        Ok(Some(inner_changed)) => {
808                            *changed |= inner_changed;
809                            true
810                        }
811                        Ok(None) => false,
812                        Err(e) => {
813                            failed = Some(e);
814                            true
815                        }
816                    },
817                    _ => true,
818                });
819                if let Some(e) = failed {
820                    return Err(e);
821                }
822                *changed |= items.len() != before;
823            }
824            PbValue::Map(entries) => {
825                let mut dropped = Vec::new();
826                for (key, value) in entries.iter_mut() {
827                    if let PbValue::Message(inner) = value {
828                        match self.scrub(inner)? {
829                            Some(inner_changed) => *changed |= inner_changed,
830                            None => dropped.push(key.clone()),
831                        }
832                    }
833                }
834                for key in dropped {
835                    entries.remove(&key);
836                    *changed = true;
837                }
838            }
839            _ => {}
840        }
841        Ok(true)
842    }
843
844    /// [`scrub`](Self::scrub) for an `Any`: its type URL is checked like a
845    /// top-level detail's, a DebugInfo is reported for dropping, and a packed
846    /// message of a known type is scrubbed and re-packed if it changed.
847    fn scrub_any(&self, any: &mut DynamicMessage) -> Result<Option<bool>, MalformedStatus> {
848        let type_url = match any.get_field_by_name("type_url").as_deref() {
849            Some(PbValue::String(url)) => url.clone(),
850            _ => String::new(),
851        };
852        let Some(type_name) = any_type_name(&type_url) else {
853            tracing::error!(%type_url, "packed Any with a malformed type URL");
854            return Err(MalformedStatus);
855        };
856        if type_name == DEBUG_INFO {
857            return Ok(None);
858        }
859        // An unknown packed type has no JSON form; serialization reports it.
860        let Some(desc) = self.resolve(type_name) else {
861            return Ok(Some(false));
862        };
863        let bytes = match any.get_field_by_name("value").as_deref() {
864            Some(PbValue::Bytes(bytes)) => bytes.clone(),
865            _ => Default::default(),
866        };
867        let mut inner = DynamicMessage::decode(desc, bytes).map_err(|e| {
868            tracing::error!(detail = %type_name, "undecodable packed Any: {e}");
869            MalformedStatus
870        })?;
871        match self.scrub(&mut inner)? {
872            None => Ok(None),
873            Some(false) => Ok(Some(false)),
874            Some(true) => {
875                any.set_field_by_name("value", PbValue::Bytes(inner.encode_to_vec().into()));
876                Ok(Some(true))
877            }
878        }
879    }
880}
881
882#[cfg(test)]
883mod tests;