Merge pull request #784 from dungeonfog/actix-cbor-support

Actix integration: cbor response support + error handling improvements
This commit is contained in:
Sunli 2022-01-18 10:42:48 +08:00 committed by GitHub
commit e5141b0f64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 101 additions and 4 deletions

View File

@ -24,7 +24,13 @@ serde_json = "1.0.64"
serde_urlencoded = "0.7.0"
futures-channel = "0.3.13"
thiserror = "1.0.30"
serde_cbor = { version = "0.11.2", optional = true }
[features]
default = []
cbor = ["serde_cbor"]
[dev-dependencies]
actix-rt = "2.2.0"
async-mutex = "1.4.0"
serde = { version = "1", features = ["derive"] }

View File

@ -1,4 +1,5 @@
use actix_http::body::BoxBody;
use actix_web::error::JsonPayloadError;
use std::future::Future;
use std::io::{self, ErrorKind};
use std::pin::Pin;
@ -153,20 +154,62 @@ impl From<async_graphql::BatchResponse> for GraphQLResponse {
}
}
#[cfg(feature = "cbor")]
mod cbor {
use actix_web::{http::StatusCode, ResponseError};
use core::fmt;
#[derive(Debug)]
pub struct Error(pub serde_cbor::Error);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
impl Responder for GraphQLResponse {
type Body = BoxBody;
fn respond_to(self, _req: &HttpRequest) -> HttpResponse {
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
let mut res = HttpResponse::build(StatusCode::OK);
res.content_type("application/json");
if self.0.is_ok() {
if let Some(cache_control) = self.0.cache_control().value() {
res.append_header(("cache-control", cache_control));
res.append_header((http::header::CACHE_CONTROL, cache_control));
}
}
for (name, value) in self.0.http_headers() {
res.append_header((name, value));
}
res.body(serde_json::to_string(&self.0).unwrap())
let accept = req
.headers()
.get(http::header::ACCEPT)
.and_then(|val| val.to_str().ok());
let (ct, body) = match accept {
// optional cbor support
#[cfg(feature = "cbor")]
// this avoids copy-pasting the mime type
Some(ct @ "application/cbor") => (
ct,
match serde_cbor::to_vec(&self.0) {
Ok(body) => body,
Err(e) => return HttpResponse::from_error(cbor::Error(e)),
},
),
_ => (
"application/json",
match serde_json::to_vec(&self.0) {
Ok(body) => body,
Err(e) => return HttpResponse::from_error(JsonPayloadError::Serialize(e)),
},
),
};
res.content_type(ct);
res.body(body)
}
}

View File

@ -220,3 +220,51 @@ async fn test_count() {
.into_bytes()
);
}
#[cfg(feature = "cbor")]
#[actix_rt::test]
async fn test_cbor() {
let srv = test::init_service(
App::new()
.app_data(Data::new(Schema::new(
AddQueryRoot,
EmptyMutation,
EmptySubscription,
)))
.service(
web::resource("/")
.guard(guard::Post())
.to(gql_handle_schema::<AddQueryRoot, EmptyMutation, EmptySubscription>),
),
)
.await;
let response = srv
.call(
test::TestRequest::with_uri("/")
.method(Method::POST)
.set_payload(r#"{"query":"{ add(a: 10, b: 20) }"}"#)
.insert_header((actix_http::header::ACCEPT, "application/cbor"))
.to_request(),
)
.await
.unwrap();
assert!(response.status().is_success());
#[derive(Debug, serde::Deserialize, PartialEq)]
struct Response {
data: ResponseInner,
}
#[derive(Debug, serde::Deserialize, PartialEq)]
struct ResponseInner {
add: i32,
}
let body = actix_web::body::to_bytes(response.into_body())
.await
.unwrap();
let response: Response = serde_cbor::from_slice(&body).unwrap();
assert_eq!(
response,
Response {
data: ResponseInner { add: 30 }
}
);
}