diff --git a/grpc-protobuf/Cargo.toml b/grpc-protobuf/Cargo.toml index 11e044faf..2f9ea26b3 100644 --- a/grpc-protobuf/Cargo.toml +++ b/grpc-protobuf/Cargo.toml @@ -24,5 +24,10 @@ allowed_external_types = [ bytes = "1.11.1" grpc = { version = "0.10.0", path = "../grpc" } protobuf = "4.35.1-release" +protobuf-well-known-types = "4.35.1-release" [dev-dependencies] + +[build-dependencies] +grpc-protobuf-build = { version = "0.10.0", path = "../grpc-protobuf-build", default-features = false } +protobuf-well-known-types = "4.35.1-release" diff --git a/grpc-protobuf/build.rs b/grpc-protobuf/build.rs new file mode 100644 index 000000000..2a3d4a35a --- /dev/null +++ b/grpc-protobuf/build.rs @@ -0,0 +1,24 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + println!("cargo:rerun-if-env-changed=GRPC_RUST_REGENERATE_PROTO"); + if env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some() { + let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let dependencies = protobuf_well_known_types::get_dependency("protobuf_well_known_types") + .into_iter() + .map(|d| d.into()) + .collect(); + + grpc_protobuf_build::CodeGen::new() + .output_dir(manifest_dir.join("generated")) + .include(manifest_dir.join("third_party/googleapis")) + .inputs(["google/rpc/status.proto"]) + .dependencies(dependencies) + .client_only() + .compile() + .unwrap(); + } +} diff --git a/grpc-protobuf/generated/google/rpc/generated.rs b/grpc-protobuf/generated/google/rpc/generated.rs new file mode 100644 index 000000000..509f3282d --- /dev/null +++ b/grpc-protobuf/generated/google/rpc/generated.rs @@ -0,0 +1,17 @@ +#[path="status.u.pb.rs"] +#[allow(nonstandard_style, unused, unreachable_pub)] +#[doc(hidden)] +mod internal_do_not_use_google_srpc_sstatus; + +#[allow(nonstandard_style, unused)] +#[doc(inline)] +pub use internal_do_not_use_google_srpc_sstatus::*; +#[allow(nonstandard_style, unused)] +pub mod __unstable { +pub static GOOGLE_RPC_STATUS_DESCRIPTOR_INFO: ::protobuf::__internal::runtime::__unstable::DescriptorInfo = ::protobuf::__internal::runtime::__unstable::DescriptorInfo { + descriptor: b"\n\x17google/rpc/status.proto\x12\ngoogle.rpc\x1a\x19google/protobuf/any.proto\"N\n\x06Status\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\x12%\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyB^\n\x0e\x63om.google.rpcB\x0bStatusProtoP\x01Z7google.golang.org/genproto/googleapis/rpc/status;status\xa2\x02\x03RPCb\x06proto3", + deps: &[ + &::protobuf_well_known_types::__unstable::GOOGLE_PROTOBUF_ANY_DESCRIPTOR_INFO, + ], +}; +} diff --git a/grpc-protobuf/generated/google/rpc/status.u.pb.rs b/grpc-protobuf/generated/google/rpc/status.u.pb.rs new file mode 100644 index 000000000..fdb10160c --- /dev/null +++ b/grpc-protobuf/generated/google/rpc/status.u.pb.rs @@ -0,0 +1,523 @@ +const _: () = ::protobuf::__internal::assert_compatible_gencode_version("4.35.1-release"); +// This variable must not be referenced except by protobuf generated +// code. +pub(crate) static mut google__rpc__Status_msg_init: ::protobuf::__internal::runtime::MiniTableInitPtr = + ::protobuf::__internal::runtime::MiniTableInitPtr(::protobuf::__internal::runtime::MiniTablePtr::dangling()); +#[allow(non_camel_case_types)] +pub struct Status { + inner: ::protobuf::__internal::runtime::OwnedMessageInner +} + +impl ::protobuf::Message for Status { + type MessageView<'msg> = StatusView<'msg>; + type MessageMut<'msg> = StatusMut<'msg>; +} + +impl ::std::default::Default for Status { + fn default() -> Self { + Self::new() + } +} + +impl ::std::fmt::Debug for Status { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", ::protobuf::__internal::runtime::debug_string(self)) + } +} + +// SAFETY: +// - `Status` is `Sync` because it does not implement interior mutability. +// Neither does `StatusMut`. +unsafe impl ::std::marker::Sync for Status {} + +// SAFETY: +// - `Status` is `Send` because it uniquely owns its arena and does +// not use thread-local data. +unsafe impl ::std::marker::Send for Status {} + +impl ::protobuf::Proxied for Status { + type View<'msg> = StatusView<'msg>; +} + +impl ::protobuf::__internal::SealedInternal for Status {} + +impl ::protobuf::MutProxied for Status { + type Mut<'msg> = StatusMut<'msg>; +} + +#[derive(Copy, Clone)] +#[allow(dead_code)] +pub struct StatusView<'msg> { + inner: ::protobuf::__internal::runtime::MessageViewInner<'msg, Status>, +} + +impl<'msg> ::protobuf::__internal::SealedInternal for StatusView<'msg> {} + +impl<'msg> ::protobuf::MessageView<'msg> for StatusView<'msg> { + type Message = Status; +} + +impl ::std::fmt::Debug for StatusView<'_> { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", ::protobuf::__internal::runtime::debug_string(self)) + } +} + +impl ::std::default::Default for StatusView<'_> { + fn default() -> StatusView<'static> { + ::protobuf::__internal::runtime::MessageViewInner::default().into() + } +} + +impl<'msg> From<::protobuf::__internal::runtime::MessageViewInner<'msg, Status>> for StatusView<'msg> { + fn from(inner: ::protobuf::__internal::runtime::MessageViewInner<'msg, Status>) -> Self { + Self { inner } + } +} + +#[allow(dead_code)] +impl<'msg> StatusView<'msg> { + + pub fn to_owned(&self) -> Status { + ::protobuf::IntoProxied::into_proxied(*self, ::protobuf::__internal::Private) + } + + // code: optional int32 + pub fn code(self) -> i32 { + unsafe { + // TODO: b/361751487: This .into() and .try_into() is only + // here for the enum<->i32 case, we should avoid it for + // other primitives where the types naturally match + // perfectly (and do an unchecked conversion for + // i32->enum types, since even for closed enums we trust + // upb to only return one of the named values). + self.inner.ptr().get_i32_at_index( + 0, (0i32).into() + ).try_into().unwrap() + } + } + + // message: optional string + pub fn message(self) -> ::protobuf::View<'msg, ::protobuf::ProtoString> { + let str_view = unsafe { + self.inner.ptr().get_string_at_index( + 1, (b"").into() + ) + }; + ::protobuf::ProtoStr::from_utf8_unchecked(unsafe { str_view.as_ref() }) + } + + // details: repeated message google.protobuf.Any + pub fn details(self) -> ::protobuf::RepeatedView<'msg, ::protobuf_well_known_types::Any> { + unsafe { + self.inner.ptr().get_array_at_index( + 2 + ) + }.map_or_else( + ::protobuf::__internal::runtime::empty_array::<::protobuf_well_known_types::Any>, + |raw| unsafe { + ::protobuf::RepeatedView::from_raw(::protobuf::__internal::Private, raw) + } + ) + } + +} + +// SAFETY: +// - `StatusView` is `Sync` because it does not support mutation. +unsafe impl ::std::marker::Sync for StatusView<'_> {} + +// SAFETY: +// - `StatusView` is `Send` because while its alive a `StatusMut` cannot. +// - `StatusView` does not use thread-local data. +unsafe impl ::std::marker::Send for StatusView<'_> {} + +impl<'msg> ::protobuf::AsView for StatusView<'msg> { + type Proxied = Status; + fn as_view(&self) -> ::protobuf::View<'msg, Status> { + *self + } +} + +impl<'msg> ::protobuf::IntoView<'msg> for StatusView<'msg> { + fn into_view<'shorter>(self) -> StatusView<'shorter> + where + 'msg: 'shorter { + self + } +} + +impl<'msg> ::protobuf::IntoProxied for StatusView<'msg> { + fn into_proxied(self, _private: ::protobuf::__internal::Private) -> Status { + let mut dst = Status::new(); + assert!(unsafe { + dst.inner.ptr_mut().deep_copy(self.inner.ptr(), dst.inner.arena()) + }); + dst + } +} + +impl<'msg> ::protobuf::IntoProxied for StatusMut<'msg> { + fn into_proxied(self, _private: ::protobuf::__internal::Private) -> Status { + ::protobuf::IntoProxied::into_proxied(::protobuf::IntoView::into_view(self), _private) + } +} + +impl ::protobuf::__internal::EntityType for Status { + type Tag = ::protobuf::__internal::entity_tag::MessageTag; +} + +impl<'msg> ::protobuf::__internal::EntityType for StatusView<'msg> { + type Tag = ::protobuf::__internal::entity_tag::ViewProxyTag; +} + +impl<'msg> ::protobuf::__internal::EntityType for StatusMut<'msg> { + type Tag = ::protobuf::__internal::entity_tag::MutProxyTag; +} + +#[allow(dead_code)] +#[allow(non_camel_case_types)] +pub struct StatusMut<'msg> { + inner: ::protobuf::__internal::runtime::MessageMutInner<'msg, Status>, +} + +impl<'msg> ::protobuf::__internal::SealedInternal for StatusMut<'msg> {} + +impl<'msg> ::protobuf::MessageMut<'msg> for StatusMut<'msg> { + type Message = Status; +} + +impl ::std::fmt::Debug for StatusMut<'_> { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", ::protobuf::__internal::runtime::debug_string(self)) + } +} + +impl<'msg> From<::protobuf::__internal::runtime::MessageMutInner<'msg, Status>> for StatusMut<'msg> { + fn from(inner: ::protobuf::__internal::runtime::MessageMutInner<'msg, Status>) -> Self { + Self { inner } + } +} + +#[allow(dead_code)] +impl<'msg> StatusMut<'msg> { + + #[doc(hidden)] + pub fn as_message_mut_inner(&mut self, _private: ::protobuf::__internal::Private) + -> ::protobuf::__internal::runtime::MessageMutInner<'msg, Status> { + self.inner.reborrow() + } + + pub fn to_owned(&self) -> Status { + ::protobuf::AsView::as_view(self).to_owned() + } + + // code: optional int32 + pub fn code(&self) -> i32 { + unsafe { + // TODO: b/361751487: This .into() and .try_into() is only + // here for the enum<->i32 case, we should avoid it for + // other primitives where the types naturally match + // perfectly (and do an unchecked conversion for + // i32->enum types, since even for closed enums we trust + // upb to only return one of the named values). + self.inner.ptr().get_i32_at_index( + 0, (0i32).into() + ).try_into().unwrap() + } + } + pub fn set_code(&mut self, val: i32) { + unsafe { + // TODO: b/361751487: This .into() is only here + // here for the enum<->i32 case, we should avoid it for + // other primitives where the types naturally match + //perfectly. + self.inner.ptr_mut().set_base_field_i32_at_index( + 0, val.into() + ) + } + } + + // message: optional string + pub fn message(&self) -> ::protobuf::View<'_, ::protobuf::ProtoString> { + let str_view = unsafe { + self.inner.ptr().get_string_at_index( + 1, (b"").into() + ) + }; + ::protobuf::ProtoStr::from_utf8_unchecked(unsafe { str_view.as_ref() }) + } + pub fn set_message(&mut self, val: impl ::protobuf::IntoProxied<::protobuf::ProtoString>) { + unsafe { + ::protobuf::__internal::runtime::message_set_string_field( + ::protobuf::AsMut::as_mut(self).inner, + 1, + val); + } + } + + // details: repeated message google.protobuf.Any + pub fn details(&self) -> ::protobuf::RepeatedView<'_, ::protobuf_well_known_types::Any> { + unsafe { + self.inner.ptr().get_array_at_index( + 2 + ) + }.map_or_else( + ::protobuf::__internal::runtime::empty_array::<::protobuf_well_known_types::Any>, + |raw| unsafe { + ::protobuf::RepeatedView::from_raw(::protobuf::__internal::Private, raw) + } + ) + } + pub fn details_mut(&mut self) -> ::protobuf::RepeatedMut<'_, ::protobuf_well_known_types::Any> { + unsafe { + let raw_array = self.inner.ptr_mut().get_or_create_mutable_array_at_index( + 2, + self.inner.arena() + ).expect("alloc should not fail"); + ::protobuf::RepeatedMut::from_inner( + ::protobuf::__internal::Private, + ::protobuf::__internal::runtime::InnerRepeatedMut::new( + raw_array, self.inner.arena(), + ), + ) + } + } + pub fn set_details(&mut self, src: impl ::protobuf::IntoProxied<::protobuf::Repeated<::protobuf_well_known_types::Any>>) { + unsafe { + ::protobuf::__internal::runtime::message_set_repeated_field( + ::protobuf::AsMut::as_mut(self).inner, + 2, + src); + } + } + +} + +// SAFETY: +// - `StatusMut` does not perform any shared mutation. +unsafe impl ::std::marker::Send for StatusMut<'_> {} + +// SAFETY: +// - `StatusMut` does not perform any shared mutation. +unsafe impl ::std::marker::Sync for StatusMut<'_> {} + +impl<'msg> ::protobuf::AsView for StatusMut<'msg> { + type Proxied = Status; + fn as_view(&self) -> ::protobuf::View<'_, Status> { + self.inner.as_view().into() + } +} + +impl<'msg> ::protobuf::IntoView<'msg> for StatusMut<'msg> { + fn into_view<'shorter>(self) -> ::protobuf::View<'shorter, Status> + where + 'msg: 'shorter { + self.inner.as_view().into() + } +} + +impl<'msg> ::protobuf::AsMut for StatusMut<'msg> { + type MutProxied = Status; + fn as_mut(&mut self) -> StatusMut<'msg> { + self.inner.reborrow().into() + } +} + +impl<'msg> ::protobuf::IntoMut<'msg> for StatusMut<'msg> { + fn into_mut<'shorter>(self) -> StatusMut<'shorter> + where + 'msg: 'shorter { + self + } +} + +#[allow(dead_code)] +impl Status { + pub fn new() -> Self { + Self { inner: ::protobuf::__internal::runtime::OwnedMessageInner::::new() } + } + + + #[doc(hidden)] + pub fn as_message_mut_inner(&mut self, _private: ::protobuf::__internal::Private) -> ::protobuf::__internal::runtime::MessageMutInner<'_, Status> { + ::protobuf::__internal::runtime::MessageMutInner::mut_of_owned(&mut self.inner) + } + + pub fn as_view(&self) -> StatusView<'_> { + ::protobuf::__internal::runtime::MessageViewInner::view_of_owned(&self.inner).into() + } + + pub fn as_mut(&mut self) -> StatusMut<'_> { + ::protobuf::__internal::runtime::MessageMutInner::mut_of_owned(&mut self.inner).into() + } + + // code: optional int32 + pub fn code(&self) -> i32 { + unsafe { + // TODO: b/361751487: This .into() and .try_into() is only + // here for the enum<->i32 case, we should avoid it for + // other primitives where the types naturally match + // perfectly (and do an unchecked conversion for + // i32->enum types, since even for closed enums we trust + // upb to only return one of the named values). + self.inner.ptr().get_i32_at_index( + 0, (0i32).into() + ).try_into().unwrap() + } + } + pub fn set_code(&mut self, val: i32) { + unsafe { + // TODO: b/361751487: This .into() is only here + // here for the enum<->i32 case, we should avoid it for + // other primitives where the types naturally match + //perfectly. + self.inner.ptr_mut().set_base_field_i32_at_index( + 0, val.into() + ) + } + } + + // message: optional string + pub fn message(&self) -> ::protobuf::View<'_, ::protobuf::ProtoString> { + let str_view = unsafe { + self.inner.ptr().get_string_at_index( + 1, (b"").into() + ) + }; + ::protobuf::ProtoStr::from_utf8_unchecked(unsafe { str_view.as_ref() }) + } + pub fn set_message(&mut self, val: impl ::protobuf::IntoProxied<::protobuf::ProtoString>) { + unsafe { + ::protobuf::__internal::runtime::message_set_string_field( + ::protobuf::AsMut::as_mut(self).inner, + 1, + val); + } + } + + // details: repeated message google.protobuf.Any + pub fn details(&self) -> ::protobuf::RepeatedView<'_, ::protobuf_well_known_types::Any> { + unsafe { + self.inner.ptr().get_array_at_index( + 2 + ) + }.map_or_else( + ::protobuf::__internal::runtime::empty_array::<::protobuf_well_known_types::Any>, + |raw| unsafe { + ::protobuf::RepeatedView::from_raw(::protobuf::__internal::Private, raw) + } + ) + } + pub fn details_mut(&mut self) -> ::protobuf::RepeatedMut<'_, ::protobuf_well_known_types::Any> { + unsafe { + let raw_array = self.inner.ptr_mut().get_or_create_mutable_array_at_index( + 2, + self.inner.arena() + ).expect("alloc should not fail"); + ::protobuf::RepeatedMut::from_inner( + ::protobuf::__internal::Private, + ::protobuf::__internal::runtime::InnerRepeatedMut::new( + raw_array, self.inner.arena(), + ), + ) + } + } + pub fn set_details(&mut self, src: impl ::protobuf::IntoProxied<::protobuf::Repeated<::protobuf_well_known_types::Any>>) { + unsafe { + ::protobuf::__internal::runtime::message_set_repeated_field( + ::protobuf::AsMut::as_mut(self).inner, + 2, + src); + } + } + +} // impl Status + +impl ::std::ops::Drop for Status { + #[inline] + fn drop(&mut self) { + } +} + +impl ::std::clone::Clone for Status { + fn clone(&self) -> Self { + self.as_view().to_owned() + } +} + +impl ::protobuf::AsView for Status { + type Proxied = Self; + fn as_view(&self) -> StatusView<'_> { + self.as_view() + } +} + +impl ::protobuf::AsMut for Status { + type MutProxied = Self; + fn as_mut(&mut self) -> StatusMut<'_> { + self.as_mut() + } +} + +unsafe impl ::protobuf::__internal::runtime::AssociatedMiniTable for Status { + fn mini_table() -> ::protobuf::__internal::runtime::MiniTablePtr { + static ONCE_LOCK: ::std::sync::OnceLock<::protobuf::__internal::runtime::MiniTableInitPtr> = + ::std::sync::OnceLock::new(); + unsafe { + ONCE_LOCK.get_or_init(|| { + super::google__rpc__Status_msg_init.0 = + ::protobuf::__internal::runtime::build_mini_table("$(P1XG"); + ::protobuf::__internal::runtime::link_mini_table( + super::google__rpc__Status_msg_init.0, &[<::protobuf_well_known_types::Any as ::protobuf::__internal::runtime::AssociatedMiniTable>::mini_table(), + ], &[]); + ::protobuf::__internal::runtime::MiniTableInitPtr(super::google__rpc__Status_msg_init.0) + }).0 + } + } +} +unsafe impl ::protobuf::__internal::runtime::UpbGetArena for Status { + fn get_arena(&mut self, _private: ::protobuf::__internal::Private) -> &::protobuf::__internal::runtime::Arena { + self.inner.arena() + } +} + +unsafe impl ::protobuf::__internal::runtime::UpbGetMessagePtrMut for Status { + type Msg = Status; + fn get_ptr_mut(&mut self, _private: ::protobuf::__internal::Private) -> ::protobuf::__internal::runtime::MessagePtr { + self.inner.ptr_mut() + } +} +unsafe impl ::protobuf::__internal::runtime::UpbGetMessagePtr for Status { + type Msg = Status; + fn get_ptr(&self, _private: ::protobuf::__internal::Private) -> ::protobuf::__internal::runtime::MessagePtr { + self.inner.ptr() + } +} +unsafe impl ::protobuf::__internal::runtime::UpbGetMessagePtrMut for StatusMut<'_> { + type Msg = Status; + fn get_ptr_mut(&mut self, _private: ::protobuf::__internal::Private) -> ::protobuf::__internal::runtime::MessagePtr { + self.inner.ptr_mut() + } +} +unsafe impl ::protobuf::__internal::runtime::UpbGetMessagePtr for StatusMut<'_> { + type Msg = Status; + fn get_ptr(&self, _private: ::protobuf::__internal::Private) -> ::protobuf::__internal::runtime::MessagePtr { + self.inner.ptr() + } +} +unsafe impl ::protobuf::__internal::runtime::UpbGetMessagePtr for StatusView<'_> { + type Msg = Status; + fn get_ptr(&self, _private: ::protobuf::__internal::Private) -> ::protobuf::__internal::runtime::MessagePtr { + self.inner.ptr() + } +} + +unsafe impl ::protobuf::__internal::runtime::UpbGetArena for StatusMut<'_> { + fn get_arena(&mut self, _private: ::protobuf::__internal::Private) -> &::protobuf::__internal::runtime::Arena { + self.inner.arena() + } +} + + + diff --git a/grpc-protobuf/src/client/client_streaming.rs b/grpc-protobuf/src/client/client_streaming.rs index 4e0d6879a..b41f8d2d4 100644 --- a/grpc-protobuf/src/client/client_streaming.rs +++ b/grpc-protobuf/src/client/client_streaming.rs @@ -25,8 +25,9 @@ use std::marker::PhantomData; use std::pin::Pin; -use grpc::Status; -use grpc::StatusOr; +use crate::Status; +use crate::StatusOr; +use crate::trailers_conv::status_from_trailers; use grpc::client::CallOptions; use grpc::client::InvokeOnce; use grpc::client::RecvStream; @@ -169,7 +170,7 @@ where loop { let i = rx.recv(&mut res).await; if let ResponseStreamItem::Trailers(t) = i { - return t.into_status(); + return status_from_trailers(t); } } } diff --git a/grpc-protobuf/src/client/mod.rs b/grpc-protobuf/src/client/mod.rs index 935e5c151..a4b100374 100644 --- a/grpc-protobuf/src/client/mod.rs +++ b/grpc-protobuf/src/client/mod.rs @@ -26,8 +26,9 @@ use std::marker::PhantomData; use std::time::Duration; use std::time::Instant; +use crate::Status; +use crate::trailers_conv::status_from_trailers; use bytes::Buf; -use grpc::Status; use grpc::client::CallOptions; use grpc::client::InvokeOnce; use grpc::client::RecvStream as ClientRecvStream; @@ -147,7 +148,7 @@ where ResponseStreamItem::Headers(_) => unreachable!(), ResponseStreamItem::Message => Ok(()), ResponseStreamItem::Trailers(trailers) => { - self.status = Some(trailers.into_status()); + self.status = Some(status_from_trailers(trailers)); Err(()) } ResponseStreamItem::StreamClosed => Err(()), @@ -176,7 +177,7 @@ where loop { let i = self.rx.recv(&mut nop_msg).await; if let ResponseStreamItem::Trailers(t) = i { - return t.into_status(); + return status_from_trailers(t); } } } diff --git a/grpc-protobuf/src/client/unary.rs b/grpc-protobuf/src/client/unary.rs index 034aaa178..39b01c85b 100644 --- a/grpc-protobuf/src/client/unary.rs +++ b/grpc-protobuf/src/client/unary.rs @@ -25,8 +25,9 @@ use std::marker::PhantomData; use std::pin::Pin; -use grpc::Status; -use grpc::StatusError; +use crate::Status; +use crate::StatusError; +use crate::trailers_conv::status_from_trailers; use grpc::client::CallOptions; use grpc::client::InvokeOnce; use grpc::client::RecvStream as _; @@ -96,7 +97,7 @@ where loop { let i = rx.recv(&mut res).await; if let ResponseStreamItem::Trailers(t) = i { - return t.status().clone(); + return status_from_trailers(t); } } } diff --git a/grpc-protobuf/src/lib.rs b/grpc-protobuf/src/lib.rs index 704ee079a..70ffbd1f5 100644 --- a/grpc-protobuf/src/lib.rs +++ b/grpc-protobuf/src/lib.rs @@ -59,11 +59,14 @@ use protobuf::Proxied; use protobuf::Serialize; mod client; +mod status; +mod trailers_conv; pub use client::bidi::*; pub use client::client_streaming::*; pub use client::server_streaming::*; pub use client::unary::*; pub use client::*; +pub use status::*; /// Implements [`SendMessage`] for protobuf message views. pub struct ProtoSendMessage<'a, V: Proxied>(V::View<'a>); diff --git a/grpc-protobuf/src/status.rs b/grpc-protobuf/src/status.rs new file mode 100644 index 000000000..f6bc4e004 --- /dev/null +++ b/grpc-protobuf/src/status.rs @@ -0,0 +1,261 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use std::collections::BTreeMap; + +/// Represents a gRPC status code. This is expected to be replaced with absl::StatusCodeError when +/// it becomes available. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(i32)] +pub enum StatusCodeError { + /// The operation was cancelled, typically by the caller. + Cancelled = 1, + /// Unknown error. For example, this error may be returned when + /// a `Status` value received from another address space belongs to + /// an error space that is not known in this address space. Also + /// errors raised by APIs that do not return enough error information + /// may be converted to this error. + Unknown = 2, + /// The client specified an invalid argument. Note that this differs + /// from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments + /// that are problematic regardless of the state of the system + /// (e.g., a malformed file name). + InvalidArgument = 3, + /// The deadline expired before the operation could complete. For operations + /// that change the state of the system, this error may be returned + /// even if the operation has completed successfully. For example, a + /// successful response from a server could have been delayed long + /// enough for the deadline to expire. + DeadlineExceeded = 4, + /// Some requested entity (e.g., file or directory) was not found. + /// + /// Note to server developers: if a request is denied for an entire class + /// of users, such as gradual feature rollout or undocumented allowlist, + /// `NOT_FOUND` may be used. If a request is denied for some users within + /// a class of users, such as user-based access control, `PERMISSION_DENIED` + /// must be used. + NotFound = 5, + /// The entity that a client attempted to create (e.g., file or directory) + /// already exists. + AlreadyExists = 6, + /// The caller does not have permission to execute the specified + /// operation. `PERMISSION_DENIED` must not be used for rejections + /// caused by exhausting some resource (use `RESOURCE_EXHAUSTED` + /// instead for those errors). `PERMISSION_DENIED` must not be + /// used if the caller can not be identified (use `UNAUTHENTICATED` + /// instead for those errors). This error code does not imply the + /// request is valid or the requested entity exists or satisfies + /// other pre-conditions. + PermissionDenied = 7, + /// Some resource has been exhausted, perhaps a per-user quota, or + /// perhaps the entire file system is out of space. + ResourceExhausted = 8, + /// The operation was rejected because the system is not in a state + /// required for the operation's execution. For example, the directory + /// to be deleted is non-empty, an rmdir operation is applied to + /// a non-directory, etc. + /// + /// Service implementors can use the following guidelines to decide + /// between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: + /// (a) Use `UNAVAILABLE` if the client can retry just the failing call. + /// (b) Use `ABORTED` if the client should retry at a higher level. For + /// example, when a client-specified test-and-set fails, indicating the + /// client should restart a read-modify-write sequence. + /// (c) Use `FAILED_PRECONDITION` if the client should not retry until + /// the system state has been explicitly fixed. For example, if an "rmdir" + /// fails because the directory is non-empty, `FAILED_PRECONDITION` + /// should be returned since the client should not retry unless + /// the files are deleted from the directory. + FailedPrecondition = 9, + /// The operation was aborted, typically due to a concurrency issue such as + /// a sequencer check failure or transaction abort. + /// + /// See the guidelines above for deciding between `FAILED_PRECONDITION`, + /// `ABORTED`, and `UNAVAILABLE`. + Aborted = 10, + /// The operation was attempted past the valid range. E.g., seeking or + /// reading past end-of-file. + /// + /// Unlike `INVALID_ARGUMENT`, this error indicates a problem that may + /// be fixed if the system state changes. For example, a 32-bit file + /// system will generate `INVALID_ARGUMENT` if asked to read at an + /// offset that is not in the range [0,2^32-1], but it will generate + /// `OUT_OF_RANGE` if asked to read from an offset past the current + /// file size. + /// + /// There is a fair bit of overlap between `FAILED_PRECONDITION` and + /// `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific + /// error) when it applies so that callers who are iterating through + /// a space can easily look for an `OUT_OF_RANGE` error to detect when + /// they are done. + OutOfRange = 11, + /// The operation is not implemented or is not supported/enabled in this + /// service. + Unimplemented = 12, + /// Internal errors. This means that some invariants expected by the + /// underlying system have been broken. This error code is reserved + /// for serious errors. + Internal = 13, + /// The service is currently unavailable. This is most likely a + /// transient condition, which can be corrected by retrying with + /// a backoff. Note that it is not always safe to retry + /// non-idempotent operations. + /// + /// See the guidelines above for deciding between `FAILED_PRECONDITION`, + /// `ABORTED`, and `UNAVAILABLE`. + Unavailable = 14, + /// Unrecoverable data loss or corruption. + DataLoss = 15, + /// The request does not have valid authentication credentials for the + /// operation. + Unauthenticated = 16, +} + +impl From for StatusCodeError { + fn from(i: i32) -> Self { + match i { + 1 => StatusCodeError::Cancelled, + 2 => StatusCodeError::Unknown, + 3 => StatusCodeError::InvalidArgument, + 4 => StatusCodeError::DeadlineExceeded, + 5 => StatusCodeError::NotFound, + 6 => StatusCodeError::AlreadyExists, + 7 => StatusCodeError::PermissionDenied, + 8 => StatusCodeError::ResourceExhausted, + 9 => StatusCodeError::FailedPrecondition, + 10 => StatusCodeError::Aborted, + 11 => StatusCodeError::OutOfRange, + 12 => StatusCodeError::Unimplemented, + 13 => StatusCodeError::Internal, + 14 => StatusCodeError::Unavailable, + 15 => StatusCodeError::DataLoss, + 16 => StatusCodeError::Unauthenticated, + _ => StatusCodeError::Unknown, + } + } +} + +/// Represents either a failing gRPC status or a successful result containing `T`. This is expected +/// to be replaced with absl::StatusOr when it becomes available. +pub type StatusOr = Result; +/// Represents either a failing gRPC status or a successful result. This is expected to be replaced +/// with absl::Status when it becomes available. +pub type Status = StatusOr<()>; + +/// Represents a gRPC status. This is expected to be replaced with absl::StatusError when it becomes +/// available. +#[derive(Debug, Clone)] +pub struct StatusError { + code: StatusCodeError, + message: String, + payloads: BTreeMap, Vec>, +} + +impl StatusError { + /// Create a new [`StatusError`] with the given code and message. + pub fn new(code: StatusCodeError, message: impl Into) -> Self { + StatusError { + code: code, + message: message.into(), + payloads: BTreeMap::new(), + } + } + + /// Get the [`StatusCodeError`] of this [`StatusError`]. + pub fn code(&self) -> StatusCodeError { + self.code + } + + /// Get the message of this [`StatusError`]. + pub fn message(&self) -> &str { + &self.message + } + + /// Get the value for [`type_url`]. + pub fn get_payload<'a>(&'a self, type_url: &[u8]) -> Option<&'a [u8]> { + self.payloads.get(type_url).map(|v| v.as_slice()) + } + + /// Set the value for [`type_url`]. + pub fn set_payload(&mut self, type_url: &[u8], payload: &[u8]) { + self.payloads.insert(type_url.to_vec(), payload.to_vec()); + } + + pub(crate) fn has_payloads(&self) -> bool { + !self.payloads.is_empty() + } + + pub(crate) fn into_parts( + self, + ) -> ( + StatusCodeError, + String, + impl Iterator, Vec)>, + ) { + (self.code, self.message, self.payloads.into_iter()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_status_new() { + let status = StatusError::new(StatusCodeError::NotFound, "not ok"); + assert_eq!(status.code(), StatusCodeError::NotFound); + assert_eq!(status.message(), "not ok"); + } + + #[test] + fn test_status_debug() { + let status = StatusError::new(StatusCodeError::Cancelled, "not ok"); + let debug = format!("{:?}", status); + assert!(debug.contains("Status")); + assert!(debug.contains("Cancelled")); + assert!(debug.contains("not ok")); + } + + #[test] + fn test_status_error_payloads() { + let mut err = StatusError::new(StatusCodeError::NotFound, "Resource missing"); + assert_eq!(err.get_payload(b"type.googleapis.com/foo"), None); + + err.set_payload(b"type.googleapis.com/foo", b"payload_data"); + assert_eq!( + err.get_payload(b"type.googleapis.com/foo"), + Some(&b"payload_data"[..]) + ); + + let (_code, _msg, payloads) = err.into_parts(); + let items: Vec<_> = payloads.collect(); + assert_eq!( + items, + vec![( + b"type.googleapis.com/foo".to_vec(), + b"payload_data".to_vec() + )] + ); + } +} diff --git a/grpc-protobuf/src/trailers_conv.rs b/grpc-protobuf/src/trailers_conv.rs new file mode 100644 index 000000000..fe16e2925 --- /dev/null +++ b/grpc-protobuf/src/trailers_conv.rs @@ -0,0 +1,428 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use grpc::core::Trailers; +use protobuf::Parse; +use protobuf::Serialize; +use protobuf_well_known_types::Any; + +use crate::status::*; + +#[allow(dead_code)] +mod google_rpc { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/generated/google/rpc/generated.rs" + )); +} + +/// Converts grpc-status-details-bin from the trailer's metadata. If the rpc status code doesn't +/// match the result, the status will become an INTERNAL error. +pub(crate) fn status_from_trailers(mut t: Trailers) -> Status { + let bin_val = t.metadata_mut().remove_bin("grpc-status-details-bin"); + match t.into_status() { + Ok(()) => { + if bin_val.is_some() { + return Err(StatusError::new( + StatusCodeError::Internal, + "grpc-status-details-bin metadata cannot be present when gRPC status code is OK", + )); + } + Ok(()) + } + Err(grpc_err) => { + let (code, message) = grpc_err.into_parts(); + let expected_code = StatusCodeError::from(code as i32); + match bin_val { + None => Err(StatusError::new(expected_code, message)), + Some(meta_val) => { + let rpc_status_err = parse_rpc_status(meta_val.as_bytes())?; + if rpc_status_err.code() != expected_code { + return Err(StatusError::new( + StatusCodeError::Internal, + format!( + "RPC status code mismatch: gRPC code {:?}, RPC code {:?}", + expected_code, + rpc_status_err.code() + ), + )); + } + Err(rpc_status_err) + } + } + } + } +} + +fn parse_rpc_status(buf: &[u8]) -> StatusOr { + let rpc_status = google_rpc::Status::parse(buf).map_err(|e| { + StatusError::new( + StatusCodeError::Internal, + format!("Failed to parse grpc-status-details-bin: {}", e), + ) + })?; + let code_i32 = rpc_status.code(); + if code_i32 == 0 { + return Err(StatusError::new( + StatusCodeError::Internal, + "grpc-status-details-bin status code was OK, but should always be an error", + )); + } + let code = StatusCodeError::from(code_i32); + let message = rpc_status.message().to_string(); + let mut status_err = StatusError::new(code, message); + + for any in rpc_status.details() { + status_err.set_payload(any.type_url().as_bytes(), any.value()); + } + + Ok(status_err) +} + +/// Converts the status to trailers and inserts grpc-status-details-bin into the metadata. +#[allow(dead_code)] +pub(crate) fn trailers_from_status(s: Status) -> Trailers { + match s { + Ok(()) => Trailers::new(Ok(())), + Err(status_err) => { + let has_payloads = status_err.has_payloads(); + let (code, message, payloads) = status_err.into_parts(); + let mut m = grpc::metadata::MetadataMap::new(); + if has_payloads { + let code_i32 = code as i32; + let bytes = match encode_rpc_status(code_i32, &message, payloads) { + Ok(bytes) => bytes, + Err(err) => return Trailers::new(Err(err)), + }; + m.insert_bin( + "grpc-status-details-bin", + bytes::Bytes::from_owner(bytes) + .try_into() + .expect("Bytes to metadata value cannot fail"), + ); + } + let grpc_code = grpc::StatusCodeError::from(code as i32); + Trailers::new(Err(grpc::StatusError::new(grpc_code, message))).with_metadata(m) + } + } +} + +fn encode_rpc_status( + code: i32, + message: &str, + payloads: impl IntoIterator, Vec)>, +) -> grpc::Result> { + let mut rpc_status = google_rpc::Status::new(); + rpc_status.set_code(code); + rpc_status.set_message(message); + + for (type_url, payload) in payloads { + let Ok(type_url_str) = String::from_utf8(type_url) else { + continue; + }; + let mut any = Any::new(); + any.set_type_url(type_url_str); + any.set_value(payload); + rpc_status.details_mut().push(any); + } + + // TODO: reconsider this error handling; it will be sent to the client. But it may also never + // trigger. Note that `e` contains no details. + rpc_status.serialize().map_err(|e| { + grpc::StatusError::new( + grpc::StatusCodeError::Internal, + format!("Failed to serialize grpc-status-details-bin: {}", e), + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trailers_from_status_details_copied_to_grpc_status() { + let mut err = StatusError::new(StatusCodeError::NotFound, "not found detail"); + err.set_payload(b"type.googleapis.com/test", b"hello world"); + + let trailers = trailers_from_status(Err(err)); + let grpc_status_error = trailers.status().as_ref().unwrap_err(); + assert_eq!(grpc_status_error.code(), grpc::StatusCodeError::NotFound); + assert_eq!(grpc_status_error.message(), "not found detail"); + assert!( + trailers + .metadata() + .get_bin("grpc-status-details-bin") + .is_some() + ); + } + + #[test] + fn test_trailers_from_status_ok_copied_to_grpc_status() { + let trailers = trailers_from_status(Ok(())); + assert!(trailers.status().is_ok()); + assert!( + trailers + .metadata() + .get_bin("grpc-status-details-bin") + .is_none() + ); + } + + #[test] + fn test_trailers_from_status_empty_payload_skips_metadata() { + let err = StatusError::new( + StatusCodeError::NotFound, + "Resource missing without details", + ); + let status_or: Status = Err(err); + + let trailers = trailers_from_status(status_or); + assert!(trailers.status().is_err()); + assert!( + trailers + .metadata() + .get_bin("grpc-status-details-bin") + .is_none() + ); + } + + #[test] + fn test_roundtrip_payload() { + let mut og_err = StatusError::new(StatusCodeError::NotFound, "not found detail"); + og_err.set_payload(b"type.googleapis.com/foo", b"hello"); + og_err.set_payload(b"type.googleapis.com/bar", b"world"); + + let trailers = trailers_from_status(Err(og_err.clone())); + let rt_err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(rt_err.code(), og_err.code()); + assert_eq!(rt_err.message(), og_err.message()); + assert_eq!( + rt_err.get_payload(b"type.googleapis.com/foo"), + og_err.get_payload(b"type.googleapis.com/foo") + ); + assert_eq!( + rt_err.get_payload(b"type.googleapis.com/bar"), + og_err.get_payload(b"type.googleapis.com/bar") + ); + } + + #[test] + fn test_roundtrip_invalid_utf8_dropped() { + let mut og_err = StatusError::new(StatusCodeError::NotFound, "not found detail"); + og_err.set_payload(b"type.googleapis.com/foo", b"world"); + og_err.set_payload(b"type.googleapis.com/bar\x80", b"ain't gonna work"); + og_err.set_payload(b"type.googleapis.com/bar", b"hello"); + + let trailers = trailers_from_status(Err(og_err.clone())); + let rt_err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(rt_err.code(), og_err.code()); + assert_eq!(rt_err.message(), og_err.message()); + // Other payloads are preserved + assert_eq!( + rt_err.get_payload(b"type.googleapis.com/foo"), + og_err.get_payload(b"type.googleapis.com/foo") + ); + assert_eq!( + rt_err.get_payload(b"type.googleapis.com/bar"), + og_err.get_payload(b"type.googleapis.com/bar") + ); + // But not the one with invalid UTF-8 + assert!(rt_err.get_payload(b"type.googleapis.com/bar\x80").is_none()); + } + + #[test] + fn test_roundtrip_no_payload() { + let og_err = StatusError::new(StatusCodeError::NotFound, "not found detail"); + + let trailers = trailers_from_status(Err(og_err.clone())); + let rt_err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(rt_err.code(), og_err.code()); + assert_eq!(rt_err.message(), og_err.message()); + assert!(!rt_err.has_payloads()); + } + + #[test] + fn test_roundtrip_ok() { + let trailers = trailers_from_status(Ok(())); + let status = status_from_trailers(trailers); + assert!(status.is_ok()); + } + + #[test] + fn test_status_from_trailers_ok() { + let trailers = Trailers::new(Ok(())); + + let status = status_from_trailers(trailers); + assert!(status.is_ok()); + } + + #[test] + fn test_status_from_trailers_no_details_bin() { + let trailers = Trailers::new(Err(grpc::StatusError::new( + grpc::StatusCodeError::NotFound, + "Resource missing gRPC status", + ))); + + let restored_err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(restored_err.code(), StatusCodeError::NotFound); + assert_eq!(restored_err.message(), "Resource missing gRPC status"); + assert!(!restored_err.has_payloads()); + } + + #[test] + fn test_status_from_trailers_matching_code() { + let mut rpc_status = google_rpc::Status::new(); + rpc_status.set_code(StatusCodeError::NotFound as i32); + rpc_status.set_message("Resource missing RPC status"); + let rpc_status_bytes = rpc_status + .serialize() + .expect("rpc status serialization succeeds"); + + let mut m = grpc::metadata::MetadataMap::new(); + m.insert_bin( + "grpc-status-details-bin", + bytes::Bytes::from_owner(rpc_status_bytes) + .try_into() + .expect("Bytes to metadata value cannot fail"), + ); + let trailers = Trailers::new(Err(grpc::StatusError::new( + grpc::StatusCodeError::NotFound, + "Resource missing gRPC status", + ))) + .with_metadata(m); + + let restored_err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(restored_err.code(), StatusCodeError::NotFound); + assert_eq!(restored_err.message(), "Resource missing RPC status"); + } + + #[test] + fn test_status_from_trailers_mismatch_code() { + let mut rpc_status = google_rpc::Status::new(); + rpc_status.set_code(StatusCodeError::NotFound as i32); + rpc_status.set_message("Resource missing RPC status"); + let mut any = Any::new(); + any.set_type_url("the_type_url"); + any.set_value(b"the any value"); + rpc_status.details_mut().push(any); + let rpc_status_bytes = rpc_status + .serialize() + .expect("rpc status serialization succeeds"); + + let mut m = grpc::metadata::MetadataMap::new(); + m.insert_bin( + "grpc-status-details-bin", + bytes::Bytes::from_owner(rpc_status_bytes) + .try_into() + .expect("Bytes to metadata value cannot fail"), + ); + let trailers = Trailers::new(Err(grpc::StatusError::new( + grpc::StatusCodeError::PermissionDenied, + "Permission denied gRPC status", + ))) + .with_metadata(m); + + let err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(err.code(), StatusCodeError::Internal); + assert_eq!( + err.message(), + "RPC status code mismatch: gRPC code PermissionDenied, RPC code NotFound" + ); + } + + #[test] + fn test_status_from_trailers_mismatch_code_ok() { + // Empty message is has OK status + let rpc_status_bytes = b""; + let mut m = grpc::metadata::MetadataMap::new(); + m.insert_bin( + "grpc-status-details-bin", + bytes::Bytes::from_owner(rpc_status_bytes) + .try_into() + .expect("Bytes to metadata value cannot fail"), + ); + let trailers = Trailers::new(Err(grpc::StatusError::new( + grpc::StatusCodeError::PermissionDenied, + "Permission denied gRPC status", + ))) + .with_metadata(m); + + let err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(err.code(), StatusCodeError::Internal); + assert_eq!( + err.message(), + "grpc-status-details-bin status code was OK, but should always be an error" + ); + } + + #[test] + fn test_status_from_trailers_ok_with_details_bin() { + let mut rpc_status = google_rpc::Status::new(); + rpc_status.set_code(StatusCodeError::NotFound as i32); + rpc_status.set_message("Resource missing RPC status"); + let rpc_status_bytes = rpc_status + .serialize() + .expect("rpc status serialization succeeds"); + + let mut m = grpc::metadata::MetadataMap::new(); + m.insert_bin( + "grpc-status-details-bin", + bytes::Bytes::from_owner(rpc_status_bytes) + .try_into() + .expect("Bytes to metadata value cannot fail"), + ); + let trailers = Trailers::new(Ok(())).with_metadata(m); + + let err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(err.code(), StatusCodeError::Internal); + assert_eq!( + err.message(), + "grpc-status-details-bin metadata cannot be present when gRPC status code is OK" + ); + } + + #[test] + fn test_status_from_trailers_corrupt() { + let mut m = grpc::metadata::MetadataMap::new(); + m.insert_bin( + "grpc-status-details-bin", + bytes::Bytes::from_owner(b"not actually encoded proto") + .try_into() + .expect("Bytes to metadata value cannot fail"), + ); + let trailers = Trailers::new(Err(grpc::StatusError::new( + grpc::StatusCodeError::PermissionDenied, + "Permission denied gRPC status", + ))) + .with_metadata(m); + + let err = status_from_trailers(trailers).unwrap_err(); + assert_eq!(err.code(), StatusCodeError::Internal); + assert!( + err.message() + .contains("Failed to parse grpc-status-details-bin:") + ); + } +} diff --git a/grpc-protobuf/third_party/googleapis/LICENSE b/grpc-protobuf/third_party/googleapis/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/grpc-protobuf/third_party/googleapis/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/grpc-protobuf/third_party/googleapis/google/rpc/status.proto b/grpc-protobuf/third_party/googleapis/google/rpc/status.proto new file mode 100644 index 000000000..97f50b9d8 --- /dev/null +++ b/grpc-protobuf/third_party/googleapis/google/rpc/status.proto @@ -0,0 +1,48 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; +option java_multiple_files = true; +option java_outer_classname = "StatusProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +message Status { + // The status code, which should be an enum value of + // [google.rpc.Code][google.rpc.Code]. + int32 code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized + // by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + repeated google.protobuf.Any details = 3; +} diff --git a/grpc/src/client/transport/tonic/mod.rs b/grpc/src/client/transport/tonic/mod.rs index 556fb397a..8b9a9aad5 100644 --- a/grpc/src/client/transport/tonic/mod.rs +++ b/grpc/src/client/transport/tonic/mod.rs @@ -95,7 +95,6 @@ use crate::rt::UnixSocketOptions; use crate::rt::hyper_wrapper::HyperCompatExec; use crate::rt::hyper_wrapper::HyperCompatTimer; use crate::rt::hyper_wrapper::HyperStream; -use crate::status::Status; #[cfg(test)] mod test; @@ -201,7 +200,13 @@ impl Invoke for TonicTransport { } // Converts from a tonic status to a trailers stream item. -fn trailers_from_tonic_status(status: &TonicStatus, md: &TonicMeta) -> ResponseStreamItem { +fn trailers_from_tonic_status(status: &TonicStatus, mut md: TonicMeta) -> ResponseStreamItem { + if !status.details().is_empty() { + md.insert_bin( + "grpc-status-details-bin", + tonic::metadata::MetadataValue::from_bytes(status.details()), + ); + } let status_res = match status.code() { Code::Ok => Ok(()), code => Err(StatusError::new( @@ -209,11 +214,11 @@ fn trailers_from_tonic_status(status: &TonicStatus, md: &TonicMeta) -> ResponseS status.message(), )), }; - trailers_from_status(status_res, md) + trailers_from_status(status_res, &md) } // Builds a trailers with a status -fn trailers_from_status(status: Status, md: &TonicMeta) -> ResponseStreamItem { +fn trailers_from_status(status: crate::Result<()>, md: &TonicMeta) -> ResponseStreamItem { let trailers = match md.try_into() { Err(e) => Trailers::new(Err(StatusError::new( StatusCodeError::Internal, @@ -300,10 +305,11 @@ impl RecvStream for TonicRecvStream { Err(StatusError::new(StatusCodeError::Unknown, "Task cancelled")), &TonicMeta::default(), ), - Ok(Err(status)) => { + Ok(Err(mut status)) => { // In a Trailers-only response, the tonic status contains // the metadata. - trailers_from_tonic_status(&status, status.metadata()) + let md = std::mem::take(status.metadata_mut()); + trailers_from_tonic_status(&status, md) } }, StreamState::Streaming(mut stream) => match stream.message().await { @@ -331,7 +337,7 @@ impl RecvStream for TonicRecvStream { Err(status) => { let trailers = stream.trailers().await; let md = trailers.unwrap_or_default().unwrap_or_default(); - trailers_from_tonic_status(&status, &md) + trailers_from_tonic_status(&status, md) } Ok(None) => { let trailers = stream.trailers().await; diff --git a/grpc/src/core/mod.rs b/grpc/src/core/mod.rs index 02f970c8f..22b80ecc8 100644 --- a/grpc/src/core/mod.rs +++ b/grpc/src/core/mod.rs @@ -45,7 +45,6 @@ use std::any::TypeId; use bytes::Buf; use crate::metadata::MetadataMap; -use crate::status::Status; /// Represents a message sent by either a client or a server. #[allow(unused)] @@ -202,13 +201,13 @@ impl RequestHeaders { /// gRPC does not support request trailers. #[derive(Debug, Clone)] pub struct Trailers { - status: Status, + status: crate::Result<()>, metadata: MetadataMap, } impl Trailers { /// Returns a default [`Trailers`] instance. - pub fn new(status: Status) -> Self { + pub fn new(status: crate::Result<()>) -> Self { Self { status, metadata: MetadataMap::default(), @@ -216,13 +215,13 @@ impl Trailers { } /// Replaces the status of self with `status`. - pub fn with_status(mut self, status: Status) -> Self { + pub fn with_status(mut self, status: crate::Result<()>) -> Self { self.status = status; self } - /// Returns a reference to the [`Status`] contained in these trailers. - pub fn status(&self) -> &Status { + /// Returns a reference to the status contained in these trailers. + pub fn status(&self) -> &crate::Result<()> { &self.status } @@ -243,7 +242,7 @@ impl Trailers { } /// Returns the status in the [`Trailers`], consuming the entire status. - pub fn into_status(self) -> Status { + pub fn into_status(self) -> crate::Result<()> { self.status } } diff --git a/grpc/src/lib.rs b/grpc/src/lib.rs index eeae2df3c..36538d960 100644 --- a/grpc/src/lib.rs +++ b/grpc/src/lib.rs @@ -61,10 +61,9 @@ pub(crate) mod server; mod macros; mod status; -pub use status::Status; +pub use status::Result; pub use status::StatusCodeError; pub use status::StatusError; -pub use status::StatusOr; mod byte_str; mod rt; diff --git a/grpc/src/status.rs b/grpc/src/status.rs index 94b5dcfb0..fb30106a6 100644 --- a/grpc/src/status.rs +++ b/grpc/src/status.rs @@ -29,11 +29,7 @@ pub use status_code::StatusCodeError; /// Represents either a failing gRPC status or a successful result containing /// `T`. -pub type StatusOr = Result; - -/// The representation of a gRPC status. OK statuses may not contain a status -/// message, while error values may. -pub type Status = StatusOr<()>; +pub type Result = std::result::Result; /// Represents a gRPC status. #[derive(Debug, Clone)] @@ -61,6 +57,11 @@ impl StatusError { &self.message } + /// Consumes the [`StatusError`] and returns its constituent parts (code and message). + pub fn into_parts(self) -> (StatusCodeError, String) { + (self.code, self.message) + } + /// Returns whether the status includes a code restricted for control /// plane usage as defined by gRFC A54. pub(crate) fn is_restricted_control_plane_code(&self) -> bool { diff --git a/interop/src/client_protobuf.rs b/interop/src/client_protobuf.rs index e063be946..b63506874 100644 --- a/interop/src/client_protobuf.rs +++ b/interop/src/client_protobuf.rs @@ -22,8 +22,6 @@ * */ -use grpc::StatusCodeError; -use grpc::StatusOr; use grpc::client::Channel; use grpc::client::metadata_utils::AttachHeadersInterceptor; use grpc::client::metadata_utils::CaptureHeadersInterceptor; @@ -31,6 +29,8 @@ use grpc::client::metadata_utils::CaptureTrailersInterceptor; use grpc::metadata::MetadataMap; use grpc::metadata::MetadataValue; use grpc_protobuf::CallBuilder; +use grpc_protobuf::StatusCodeError; +use grpc_protobuf::StatusOr; use protobuf::message_eq; use protobuf::proto; use tonic::async_trait;