diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 8d6135ab..bff1080c 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -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"] } diff --git a/integrations/actix-web/src/request.rs b/integrations/actix-web/src/request.rs index 70cdc7a5..b4639b67 100644 --- a/integrations/actix-web/src/request.rs +++ b/integrations/actix-web/src/request.rs @@ -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 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) } } diff --git a/integrations/actix-web/tests/graphql.rs b/integrations/actix-web/tests/graphql.rs index 820d80ed..3f5c9690 100644 --- a/integrations/actix-web/tests/graphql.rs +++ b/integrations/actix-web/tests/graphql.rs @@ -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::), + ), + ) + .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 } + } + ); +}