twirp_rs/error.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
//! Implement [Twirp](https://twitchtv.github.io/twirp/) error responses
use std::collections::HashMap;
use axum::body::Body;
use axum::response::IntoResponse;
use http::header::{self, HeaderMap, HeaderValue};
use hyper::{Response, StatusCode};
use serde::{Deserialize, Serialize, Serializer};
/// Alias for a generic error
pub type GenericError = Box<dyn std::error::Error + Send + Sync>;
macro_rules! twirp_error_codes {
(
$(
$(#[$docs:meta])*
($konst:ident, $num:expr, $phrase:ident);
)+
) => {
/// A Twirp error code as defined by <https://twitchtv.github.io/twirp/docs/spec_v7.html>.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
#[non_exhaustive]
pub enum TwirpErrorCode {
$(
$(#[$docs])*
$konst,
)+
}
impl TwirpErrorCode {
pub fn http_status_code(&self) -> StatusCode {
match *self {
$(
TwirpErrorCode::$konst => $num,
)+
}
}
pub fn twirp_code(&self) -> &'static str {
match *self {
$(
TwirpErrorCode::$konst => stringify!($phrase),
)+
}
}
}
$(
pub fn $phrase<T: ToString>(msg: T) -> TwirpErrorResponse {
TwirpErrorResponse {
code: TwirpErrorCode::$konst,
msg: msg.to_string(),
meta: Default::default(),
}
}
)+
}
}
// Define all twirp errors.
twirp_error_codes! {
/// The operation was cancelled.
(Canceled, StatusCode::REQUEST_TIMEOUT, canceled);
/// An unknown error occurred. For example, this can be used when handling
/// errors raised by APIs that do not return any error information.
(Unknown, StatusCode::INTERNAL_SERVER_ERROR, unknown);
/// The client specified an invalid argument. This indicates arguments that
/// are invalid regardless of the state of the system (i.e. a malformed file
/// name, required argument, number out of range, etc.).
(InvalidArgument, StatusCode::BAD_REQUEST, invalid_argument);
/// The client sent a message which could not be decoded. This may mean that
/// the message was encoded improperly or that the client and server have
/// incompatible message definitions.
(Malformed, StatusCode::BAD_REQUEST, malformed);
/// Operation expired before completion. For operations that change the
/// state of the system, this error may be returned even if the operation
/// has completed successfully (timeout).
(DeadlineExceeded, StatusCode::REQUEST_TIMEOUT, deadline_exceeded);
/// Some requested entity was not found.
(NotFound, StatusCode::NOT_FOUND, not_found);
/// The requested URL path wasn't routable to a Twirp service and method.
/// This is returned by generated server code and should not be returned by
/// application code (use "not_found" or "unimplemented" instead).
(BadRoute, StatusCode::NOT_FOUND, bad_route);
/// An attempt to create an entity failed because one already exists.
(AlreadyExists, StatusCode::CONFLICT, already_exists);
// The caller does not have permission to execute the specified operation.
// It must not be used if the caller cannot be identified (use
// "unauthenticated" instead).
(PermissionDenied, StatusCode::FORBIDDEN, permission_denied);
// The request does not have valid authentication credentials for the
// operation.
(Unauthenticated, StatusCode::UNAUTHORIZED, unauthenticated);
/// Some resource has been exhausted or rate-limited, perhaps a per-user
/// quota, or perhaps the entire file system is out of space.
(ResourceExhausted, StatusCode::TOO_MANY_REQUESTS, resource_exhausted);
/// The operation was rejected because the system is not in a state required
/// for the operation's execution. For example, doing an rmdir operation on
/// a directory that is non-empty, or on a non-directory object, or when
/// having conflicting read-modify-write on the same resource.
(FailedPrecondition, StatusCode::PRECONDITION_FAILED, failed_precondition);
/// The operation was aborted, typically due to a concurrency issue like
/// sequencer check failures, transaction aborts, etc.
(Aborted, StatusCode::CONFLICT, aborted);
/// The operation was attempted past the valid range. For example, seeking
/// or reading past end of a paginated collection. Unlike
/// "invalid_argument", this error indicates a problem that may be fixed if
/// the system state changes (i.e. adding more items to the collection).
/// 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, StatusCode::BAD_REQUEST, out_of_range);
/// The operation is not implemented or not supported/enabled in this
/// service.
(Unimplemented, StatusCode::NOT_IMPLEMENTED, unimplemented);
/// When some invariants expected by the underlying system have been broken.
/// In other words, something bad happened in the library or backend
/// service. Twirp specific issues like wire and serialization problems are
/// also reported as "internal" errors.
(Internal, StatusCode::INTERNAL_SERVER_ERROR, internal);
/// The service is currently unavailable. This is most likely a transient
/// condition and may be corrected by retrying with a backoff.
(Unavailable, StatusCode::SERVICE_UNAVAILABLE, unavailable);
/// The operation resulted in unrecoverable data loss or corruption.
(Dataloss, StatusCode::INTERNAL_SERVER_ERROR, dataloss);
}
impl Serialize for TwirpErrorCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.twirp_code())
}
}
// Twirp error responses are always JSON
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct TwirpErrorResponse {
pub code: TwirpErrorCode,
pub msg: String,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
pub meta: HashMap<String, String>,
}
impl TwirpErrorResponse {
pub fn insert_meta(&mut self, key: String, value: String) -> Option<String> {
self.meta.insert(key, value)
}
}
impl IntoResponse for TwirpErrorResponse {
fn into_response(self) -> Response<Body> {
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let json =
serde_json::to_string(&self).expect("JSON serialization of an error should not fail");
(self.code.http_status_code(), headers, json).into_response()
}
}
#[cfg(test)]
mod test {
use crate::{TwirpErrorCode, TwirpErrorResponse};
#[test]
fn twirp_status_mapping() {
assert_code(TwirpErrorCode::Canceled, "canceled", 408);
assert_code(TwirpErrorCode::Unknown, "unknown", 500);
assert_code(TwirpErrorCode::InvalidArgument, "invalid_argument", 400);
assert_code(TwirpErrorCode::Malformed, "malformed", 400);
assert_code(TwirpErrorCode::Unauthenticated, "unauthenticated", 401);
assert_code(TwirpErrorCode::PermissionDenied, "permission_denied", 403);
assert_code(TwirpErrorCode::DeadlineExceeded, "deadline_exceeded", 408);
assert_code(TwirpErrorCode::NotFound, "not_found", 404);
assert_code(TwirpErrorCode::BadRoute, "bad_route", 404);
assert_code(TwirpErrorCode::Unimplemented, "unimplemented", 501);
assert_code(TwirpErrorCode::Internal, "internal", 500);
assert_code(TwirpErrorCode::Unavailable, "unavailable", 503);
}
fn assert_code(code: TwirpErrorCode, msg: &str, http: u16) {
assert_eq!(
code.http_status_code(),
http,
"expected http status code {} but got {}",
http,
code.http_status_code()
);
assert_eq!(
code.twirp_code(),
msg,
"expected error message '{}' but got '{}'",
msg,
code.twirp_code()
);
}
#[test]
fn twirp_error_response_serialization() {
let response = TwirpErrorResponse {
code: TwirpErrorCode::DeadlineExceeded,
msg: "test".to_string(),
meta: Default::default(),
};
let result = serde_json::to_string(&response).unwrap();
assert!(result.contains(r#""code":"deadline_exceeded""#));
assert!(result.contains(r#""msg":"test""#));
let result = serde_json::from_str(&result).unwrap();
assert_eq!(response, result);
}
}