use crate::client_contract::{ClientContractError, FailureCode, FailureDetail};
pub type RpcResult<T> = Result<T, RpcFailure>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("RPC failed with code `{}`", code.as_str())]
#[non_exhaustive]
pub struct RpcFailure {
code: FailureCode,
retryable: bool,
}
impl RpcFailure {
pub fn application(
code: impl Into<String>,
retryable: bool,
) -> Result<Self, ClientContractError> {
Ok(Self {
code: FailureCode::new(code)?,
retryable,
})
}
pub fn code(&self) -> &str {
self.code.as_str()
}
pub const fn retryable(&self) -> bool {
self.retryable
}
pub(crate) fn framework(code: &'static str, retryable: bool) -> Self {
Self {
code: FailureCode::framework(code),
retryable,
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn from_detail(detail: &FailureDetail) -> Self {
Self {
code: detail.code().clone(),
retryable: detail.retryable(),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn detail(&self) -> FailureDetail {
FailureDetail::new(self.code.clone(), self.retryable)
}
}
#[cfg(not(target_arch = "wasm32"))]
mod server {
use super::{RpcFailure, RpcResult};
use crate::Router;
use crate::client_contract::{
CURRENT_CLIENT_CONTRACT_VERSION, ClientContractError, ClientContractPolicy, RequestId,
ServerFailure, ServerResponse,
};
use axum::body::{Body, to_bytes};
use axum::extract::Request;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::future::Future;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn route<H, T>(path: &'static str, handler: H) -> Router
where
T: 'static,
H: axum::handler::Handler<T, ()>,
{
Router::new().route(path, axum::routing::post(handler))
}
pub async fn handle_request<Req, Output, Invoke, InvokeFuture>(
request: Request,
invoke: Invoke,
) -> Response
where
Req: DeserializeOwned + Send + 'static,
Output: Serialize,
Invoke: FnOnce(Req) -> InvokeFuture,
InvokeFuture: Future<Output = RpcResult<Output>>,
{
let content_type = request
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
if !is_json_content_type(content_type) {
return failure_response(
StatusCode::UNSUPPORTED_MEDIA_TYPE,
server_request_id(),
RpcFailure::framework("rpc.content_type_required", false),
);
}
let (_, body) = request.into_parts();
let policy = ClientContractPolicy::default();
let body = match to_bytes(body, policy.max_body_bytes()).await {
Ok(body) => body,
Err(_) => {
return failure_response(
StatusCode::PAYLOAD_TOO_LARGE,
server_request_id(),
RpcFailure::framework("rpc.request_too_large", false),
);
}
};
let envelope = match policy.decode_request::<Req>(&body) {
Ok(envelope) => envelope,
Err(error) => {
let (status, failure) = request_contract_failure(&error);
return failure_response(status, server_request_id(), failure);
}
};
let version = envelope.version();
let request_id = envelope.request_id().clone();
match invoke(envelope.into_payload()).await {
Ok(data) => {
let response =
ServerResponse::new(version, request_id.clone(), now_epoch_ms(), data);
match policy.encode_response(&response) {
Ok(body) => json_response(StatusCode::OK, body),
Err(_) => failure_response(
StatusCode::INTERNAL_SERVER_ERROR,
request_id,
RpcFailure::framework("rpc.response_encoding", false),
),
}
}
Err(failure) => failure_response(StatusCode::UNPROCESSABLE_ENTITY, request_id, failure),
}
}
fn request_contract_failure(error: &ClientContractError) -> (StatusCode, RpcFailure) {
match error {
ClientContractError::BodyTooLarge { .. } => (
StatusCode::PAYLOAD_TOO_LARGE,
RpcFailure::framework("rpc.request_too_large", false),
),
ClientContractError::UnsupportedVersion { .. } => (
StatusCode::CONFLICT,
RpcFailure::framework("rpc.version_unsupported", false),
),
_ => (
StatusCode::BAD_REQUEST,
RpcFailure::framework("rpc.request_invalid", false),
),
}
}
fn failure_response(
status: StatusCode,
request_id: RequestId,
failure: RpcFailure,
) -> Response {
let policy = ClientContractPolicy::default();
let response = ServerFailure::new(
CURRENT_CLIENT_CONTRACT_VERSION,
request_id,
now_epoch_ms(),
failure.detail(),
);
match policy.encode_failure(&response) {
Ok(body) => json_response(status, body),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, "application/json")],
Body::from(
r#"{"contract":"rullst.client","version":1,"error":{"code":"rpc.internal","retryable":false}}"#,
),
)
.into_response(),
}
}
fn json_response(status: StatusCode, body: Vec<u8>) -> Response {
(status, [(header::CONTENT_TYPE, "application/json")], body).into_response()
}
fn server_request_id() -> RequestId {
let value = format!("rpc_{}", uuid::Uuid::new_v4().simple());
RequestId::framework(value)
}
fn now_epoch_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.and_then(|duration| u64::try_from(duration.as_millis()).ok())
.unwrap_or(0)
}
fn is_json_content_type(content_type: &str) -> bool {
content_type
.split(';')
.next()
.is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case("application/json"))
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use server::{handle_request, route};