use foundation_errstacks::ErrorTrace;
use foundation_netio::shared::http::{
SendSafeBody, SimpleHeader, SimpleHeaders, SimpleIncomingRequest, SimpleMethod,
SimpleOutgoingResponse, Status,
};
use crate::shared::error::{Code, ConnectError, ConnectResult};
use crate::shared::envelope::EnvelopeWriter;
use crate::shared::protocol::grpc_web::{
self, build_status_trailers, render_trailer_frame_body, Base64StreamEncoder,
};
use crate::shared::protocol::{connect, grpc, parse_connect_content_type};
#[derive(Debug, Default, Clone, Copy)]
pub struct ErrorWriter;
impl ErrorWriter {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn is_supported(&self, request: &SimpleIncomingRequest) -> bool {
request_kind(request).is_some()
}
pub fn write(
&self,
response: &mut SimpleOutgoingResponse,
request: &SimpleIncomingRequest,
error: &ErrorTrace<ConnectError>,
) -> ConnectResult<()> {
if error.current_context().is_not_modified() && request.method == SimpleMethod::GET {
response.status = Status::NotModified;
response.body = None;
return Ok(());
}
match request_kind(request) {
Some(RequestKind::ConnectUnary) => self.write_connect_unary(response, error),
Some(RequestKind::ConnectStreaming { codec }) => {
self.write_connect_streaming(response, &codec, error)
}
Some(RequestKind::GrpcWeb { codec, text }) => {
self.write_grpc_web(response, &codec, text, error)
}
Some(RequestKind::Grpc { codec }) => self.write_grpc(response, &codec, error),
None => self.write_connect_unary(response, error),
}
}
fn write_connect_unary(
&self,
response: &mut SimpleOutgoingResponse,
error: &ErrorTrace<ConnectError>,
) -> ConnectResult<()> {
response.status = status_from_code(error.current_context().code());
set_content_type(response, connect::ERROR_CONTENT_TYPE);
response.body = Some(SendSafeBody::Bytes(connect::unary_error_body(error)?));
Ok(())
}
fn write_connect_streaming(
&self,
response: &mut SimpleOutgoingResponse,
codec: &str,
error: &ErrorTrace<ConnectError>,
) -> ConnectResult<()> {
response.status = Status::OK; set_content_type(response, &connect::streaming_content_type(codec));
let writer = EnvelopeWriter::new(None, 0, 0);
let frame = writer.write_end_stream(Some(error), &SimpleHeaders::new())?;
response.body = Some(SendSafeBody::Bytes(frame));
Ok(())
}
fn write_grpc_web(
&self,
response: &mut SimpleOutgoingResponse,
codec: &str,
text: bool,
error: &ErrorTrace<ConnectError>,
) -> ConnectResult<()> {
response.status = Status::OK; set_content_type(response, &grpc_web::content_type(codec, text));
let trailers = build_status_trailers(Some(error), &SimpleHeaders::new());
let body = render_trailer_frame_body(&trailers);
let mut frame = Vec::with_capacity(5 + body.len());
frame.push(grpc_web::constants::TRAILER_FLAG);
frame.extend_from_slice(&(body.len() as u32).to_be_bytes());
frame.extend_from_slice(&body);
let out = if text {
let mut enc = Base64StreamEncoder::default();
let mut encoded = enc.feed(&frame);
encoded.extend(enc.finish());
encoded
} else {
frame
};
response.body = Some(SendSafeBody::Bytes(out));
Ok(())
}
fn write_grpc(
&self,
response: &mut SimpleOutgoingResponse,
codec: &str,
error: &ErrorTrace<ConnectError>,
) -> ConnectResult<()> {
response.status = Status::OK;
set_content_type(response, &grpc::content_type(codec));
response.trailers = build_status_trailers(Some(error), &SimpleHeaders::new());
response.body = None;
Ok(())
}
}
enum RequestKind {
ConnectUnary,
ConnectStreaming { codec: String },
GrpcWeb { codec: String, text: bool },
Grpc { codec: String },
}
fn request_kind(request: &SimpleIncomingRequest) -> Option<RequestKind> {
let ct = request
.headers
.get(&SimpleHeader::CONTENT_TYPE)
.and_then(|v| v.first())?;
if let Some((codec, text)) = grpc_web::parse_content_type(ct) {
return Some(RequestKind::GrpcWeb { codec, text });
}
if let Some(codec) = grpc::parse_content_type(ct) {
return Some(RequestKind::Grpc { codec });
}
match parse_connect_content_type(ct)? {
(codec, true) => Some(RequestKind::ConnectStreaming { codec }),
(_, false) => Some(RequestKind::ConnectUnary),
}
}
fn set_content_type(response: &mut SimpleOutgoingResponse, value: &str) {
response
.headers
.insert(SimpleHeader::CONTENT_TYPE, vec![value.to_string()]);
}
#[must_use]
pub fn status_from_code(code: Code) -> Status {
match code {
Code::Canceled => Status::InternalServerError, Code::Unknown | Code::DataLoss | Code::Internal => Status::InternalServerError,
Code::InvalidArgument | Code::FailedPrecondition | Code::OutOfRange => Status::BadRequest,
Code::DeadlineExceeded => Status::GatewayTimeout,
Code::NotFound => Status::NotFound,
Code::AlreadyExists | Code::Aborted => Status::Conflict,
Code::PermissionDenied => Status::Forbidden,
Code::ResourceExhausted => Status::TooManyRequests,
Code::Unimplemented => Status::NotImplemented,
Code::Unavailable => Status::ServiceUnavailable,
Code::Unauthenticated => Status::Unauthorized,
}
}