use http::{header, Response, StatusCode};
use serde::Serialize;
use crate::{Invocation, JmapError};
#[derive(Serialize)]
struct ProblemDetails<'a> {
#[serde(rename = "type")]
type_urn: String,
status: u16,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<&'a str>,
}
pub fn error_invocation(call_id: &str, err: JmapError) -> Invocation {
let err_value = serde_json::to_value(&err).unwrap_or_else(
|_| serde_json::json!({"type": "serverFail", "description": "internal error"}),
);
("error".to_owned(), err_value, call_id.to_owned())
}
pub fn error_status(err: &JmapError) -> StatusCode {
match err.error_type.as_str() {
"notJSON" | "notRequest" | "limit" | "unknownCapability" | "invalidArguments"
| "requestTooLarge" => StatusCode::BAD_REQUEST,
"forbidden" => StatusCode::FORBIDDEN,
"serverFail" => StatusCode::INTERNAL_SERVER_ERROR,
"serverUnavailable" => StatusCode::SERVICE_UNAVAILABLE,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[derive(Debug)]
pub struct RequestError {
status: StatusCode,
err: JmapError,
}
impl RequestError {
pub fn into_response(self) -> Response<String> {
let status = self.status;
let err = self.err;
let (limit, detail) = if err.error_type == "limit" {
(Some(err.description.as_deref().unwrap_or("unknown")), None)
} else {
(None, err.description.as_deref())
};
let details = ProblemDetails {
type_urn: format!("urn:ietf:params:jmap:error:{}", err.error_type),
status: status.as_u16(),
limit,
detail,
};
let body = serde_json::to_string(&details).expect("ProblemDetails is infallible");
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/problem+json")
.body(body)
.expect("valid status code and Content-Type header")
}
}
pub fn request_error(err: JmapError) -> RequestError {
let status = error_status(&err);
RequestError { status, err }
}
impl From<JmapError> for RequestError {
fn from(err: JmapError) -> Self {
request_error(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Id;
use http::StatusCode;
#[test]
fn error_invocation_structure() {
let inv = error_invocation("c0", JmapError::unknown_method());
assert_eq!(inv.0, "error");
assert_eq!(inv.2, "c0");
}
#[test]
fn error_invocation_args_contains_type() {
let inv = error_invocation("c0", JmapError::unknown_method());
assert_eq!(inv.1["type"], "unknownMethod");
}
#[test]
fn error_invocation_server_fail() {
let inv = error_invocation("y", JmapError::server_fail("boom"));
assert_eq!(inv.1["type"], "serverFail");
assert_eq!(inv.1["description"], "boom");
}
#[test]
fn error_status_unknown_capability_is_400() {
let e: JmapError =
serde_json::from_value(serde_json::json!({"type": "unknownCapability"})).unwrap();
assert_eq!(error_status(&e), StatusCode::BAD_REQUEST);
}
#[test]
fn error_status_invalid_arguments_is_400() {
assert_eq!(
error_status(&JmapError::invalid_arguments("x")),
StatusCode::BAD_REQUEST
);
}
#[test]
fn error_status_request_too_large_is_400() {
assert_eq!(
error_status(&JmapError::request_too_large()),
StatusCode::BAD_REQUEST
);
}
#[test]
fn error_status_forbidden_is_403() {
assert_eq!(error_status(&JmapError::forbidden()), StatusCode::FORBIDDEN);
}
#[test]
fn error_status_account_not_found_is_500() {
assert_eq!(
error_status(&JmapError::account_not_found()),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn error_status_server_fail_is_500() {
assert_eq!(
error_status(&JmapError::server_fail("x")),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn error_status_unknown_type_is_500() {
let e: JmapError =
serde_json::from_value(serde_json::json!({"type": "totallyMadeUp"})).unwrap();
assert_eq!(error_status(&e), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn request_error_derives_status() {
let re = request_error(JmapError::invalid_arguments("bad"));
assert_eq!(re.into_response().status(), StatusCode::BAD_REQUEST);
}
#[test]
fn request_error_into_response_status_code() {
let re = request_error(JmapError::invalid_arguments("bad"));
let resp = re.into_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn request_error_content_type_is_problem_json() {
let re = request_error(JmapError::not_request());
let resp = re.into_response();
assert_eq!(
resp.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some("application/problem+json"),
"Content-Type must be application/problem+json per RFC 7807"
);
}
#[test]
fn request_error_type_is_full_urn() {
let body: serde_json::Value = serde_json::from_str(
&request_error(JmapError::not_request())
.into_response()
.into_body(),
)
.unwrap();
assert_eq!(
body["type"], "urn:ietf:params:jmap:error:notRequest",
"type must be full URN"
);
}
#[test]
fn request_error_status_field_matches_http_status() {
let body: serde_json::Value = serde_json::from_str(
&request_error(JmapError::not_request())
.into_response()
.into_body(),
)
.unwrap();
assert_eq!(body["status"], 400, "status field must match HTTP code");
}
#[test]
fn request_error_limit_includes_limit_property() {
let body: serde_json::Value = serde_json::from_str(
&request_error(JmapError::limit("maxCallsInRequest"))
.into_response()
.into_body(),
)
.unwrap();
assert_eq!(
body["limit"], "maxCallsInRequest",
"limit property must name the exceeded limit"
);
assert_eq!(body["type"], "urn:ietf:params:jmap:error:limit");
}
#[test]
fn jmap_error_all_constructors_serialize() {
let errors = vec![
JmapError::not_json(),
JmapError::not_request(),
JmapError::limit("maxCallsInRequest"),
JmapError::unknown_capability(),
JmapError::forbidden(),
JmapError::server_fail("test"),
JmapError::server_unavailable(),
JmapError::server_partial_fail(),
JmapError::unknown_method(),
JmapError::invalid_arguments("x"),
JmapError::invalid_result_reference(),
JmapError::not_found(),
JmapError::account_not_found(),
JmapError::account_not_supported_by_method(),
JmapError::account_read_only(),
JmapError::request_too_large(),
JmapError::singleton(),
JmapError::will_destroy(),
JmapError::invalid_patch(),
JmapError::invalid_properties(),
JmapError::too_large(),
JmapError::rate_limit(),
JmapError::over_quota(),
JmapError::state_mismatch(),
JmapError::cannot_calculate_changes(),
JmapError::anchor_not_found(),
JmapError::unsupported_sort(),
JmapError::unsupported_filter(),
JmapError::too_many_changes(),
JmapError::from_account_not_found(),
JmapError::from_account_not_supported_by_method(),
JmapError::already_exists(Id::from("existing-1")),
JmapError::custom("customErrorType"),
];
for err in &errors {
let v = serde_json::to_value(err);
assert!(v.is_ok(), "JmapError variant failed to serialize: {err:?}");
}
}
#[test]
fn from_jmap_error_matches_request_error() {
let via_from: RequestError = JmapError::invalid_arguments("x").into();
let via_fn = request_error(JmapError::invalid_arguments("x"));
assert_eq!(
via_from.into_response().status(),
via_fn.into_response().status()
);
}
}