use std::pin::Pin;
use std::task::{ready, Context, Poll};
use bytes::{BufMut, Bytes, BytesMut};
use http_body::{Frame, SizeHint};
use pin_project::pin_project;
use tonic::codegen::http::{header::CONTENT_TYPE, HeaderMap, HeaderName, Request, Response};
use tonic::codegen::{Body, Service};
use tonic::Status;
use tower::Layer;
const GRPC_WEB_TRAILERS_BIT: u8 = 0x80;
const FRAME_HEADER_SIZE: usize = 5;
fn is_framing_header(name: &HeaderName) -> bool {
name == CONTENT_TYPE || name == tonic::codegen::http::header::CONTENT_LENGTH
}
fn is_grpc_web_trailers_only(headers: &HeaderMap) -> bool {
let is_grpc_web = headers
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.is_some_and(|ct| ct.starts_with("application/grpc-web"));
is_grpc_web && headers.contains_key(Status::GRPC_STATUS)
}
fn trailers_frame(headers: &HeaderMap) -> Bytes {
let mut block = Vec::new();
for (name, value) in headers.iter() {
if is_framing_header(name) {
continue;
}
block.put_slice(name.as_ref());
block.push(b':');
block.put_slice(value.as_bytes());
block.put_slice(b"\r\n");
}
let mut frame = BytesMut::with_capacity(FRAME_HEADER_SIZE + block.len());
frame.put_u8(GRPC_WEB_TRAILERS_BIT);
frame.put_u32(block.len() as u32);
frame.put_slice(&block);
frame.freeze()
}
#[derive(Debug, Clone, Default)]
pub struct GrpcWebTrailersLayer;
impl GrpcWebTrailersLayer {
pub fn new() -> Self {
Self
}
}
impl<S> Layer<S> for GrpcWebTrailersLayer {
type Service = GrpcWebTrailers<S>;
fn layer(&self, inner: S) -> Self::Service {
GrpcWebTrailers { inner }
}
}
#[derive(Debug, Clone)]
pub struct GrpcWebTrailers<S> {
inner: S,
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for GrpcWebTrailers<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
ResBody: Body<Data = Bytes>,
{
type Response = Response<RepairedBody<ResBody>>;
type Error = S::Error;
type Future = ResponseFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
ResponseFuture {
inner: self.inner.call(req),
}
}
}
#[pin_project]
pub struct ResponseFuture<F> {
#[pin]
inner: F,
}
impl<F, ResBody, E> std::future::Future for ResponseFuture<F>
where
F: std::future::Future<Output = Result<Response<ResBody>, E>>,
ResBody: Body<Data = Bytes>,
{
type Output = Result<Response<RepairedBody<ResBody>>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let response = ready!(self.project().inner.poll(cx))?;
Poll::Ready(Ok(repair(response)))
}
}
fn repair<B>(response: Response<B>) -> Response<RepairedBody<B>>
where
B: Body<Data = Bytes>,
{
let (mut parts, body) = response.into_parts();
if !is_grpc_web_trailers_only(&parts.headers) {
return Response::from_parts(parts, RepairedBody::passthrough(body));
}
let frame = trailers_frame(&parts.headers);
let mut kept = HeaderMap::with_capacity(parts.headers.len());
for (name, value) in parts.headers.drain() {
if let Some(name) = name {
if is_framing_header(&name) {
kept.insert(name, value);
}
}
}
kept.remove(tonic::codegen::http::header::CONTENT_LENGTH);
parts.headers = kept;
Response::from_parts(parts, RepairedBody::trailers_frame(frame))
}
#[pin_project(project = RepairedBodyProj)]
pub enum RepairedBody<B> {
Passthrough(#[pin] B),
TrailersFrame(Option<Bytes>),
}
impl<B> RepairedBody<B> {
fn passthrough(body: B) -> Self {
RepairedBody::Passthrough(body)
}
fn trailers_frame(frame: Bytes) -> Self {
RepairedBody::TrailersFrame(Some(frame))
}
}
impl<B> Body for RepairedBody<B>
where
B: Body<Data = Bytes>,
{
type Data = Bytes;
type Error = B::Error;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
match self.project() {
RepairedBodyProj::Passthrough(body) => body.poll_frame(cx),
RepairedBodyProj::TrailersFrame(frame) => {
Poll::Ready(frame.take().map(|f| Ok(Frame::data(f))))
}
}
}
fn is_end_stream(&self) -> bool {
match self {
RepairedBody::Passthrough(body) => body.is_end_stream(),
RepairedBody::TrailersFrame(frame) => frame.is_none(),
}
}
fn size_hint(&self) -> SizeHint {
match self {
RepairedBody::Passthrough(body) => body.size_hint(),
RepairedBody::TrailersFrame(Some(frame)) => SizeHint::with_exact(frame.len() as u64),
RepairedBody::TrailersFrame(None) => SizeHint::with_exact(0),
}
}
}