diff --git a/Cargo.toml b/Cargo.toml index ef294527..c12b8ca1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ serde_json = "1.0.48" spin = "0.7.0" thiserror = "1.0.21" static_assertions = "1.1.0" +http = "0.2.3" # Feature optional dependencies bson = { version = "1.0.0", optional = true } diff --git a/integrations/rocket/src/lib.rs b/integrations/rocket/src/lib.rs index 62114fff..dc281ac9 100644 --- a/integrations/rocket/src/lib.rs +++ b/integrations/rocket/src/lib.rs @@ -169,17 +169,17 @@ impl<'r> Responder<'r, 'static> for Response { let body = serde_json::to_string(&self.0).unwrap(); let mut response = rocket::Response::new(); + response.set_header(ContentType::new("application", "json")); if self.0.is_ok() { if let Some(cache_control) = self.0.cache_control().value() { response.set_header(Header::new("cache-control", cache_control)); } for (name, value) in self.0.http_headers() { - response.set_header(Header::new(name.to_string(), value.to_string())); + response.adjoin_header(Header::new(name.to_string(), value.to_string())); } } - response.set_header(ContentType::new("application", "json")); response.set_sized_body(body.len(), Cursor::new(body)); Ok(response) diff --git a/integrations/tide/src/lib.rs b/integrations/tide/src/lib.rs index 69f57383..2df9acf2 100644 --- a/integrations/tide/src/lib.rs +++ b/integrations/tide/src/lib.rs @@ -164,7 +164,7 @@ pub fn respond(resp: impl Into) -> tide::Result { response.insert_header(headers::CACHE_CONTROL, cache_control); } for (name, value) in resp.http_headers() { - response.insert_header(name, value); + response.append_header(name, value); } } response.set_body(Body::from_json(&resp)?); diff --git a/integrations/warp/src/batch_request.rs b/integrations/warp/src/batch_request.rs index 1412ef6f..035a2fb1 100644 --- a/integrations/warp/src/batch_request.rs +++ b/integrations/warp/src/batch_request.rs @@ -86,7 +86,7 @@ impl Reply for BatchResponse { if let (Ok(name), Ok(value)) = (TryInto::::try_into(name), value.try_into()) { - resp.headers_mut().insert(name, value); + resp.headers_mut().append(name, value); } } } diff --git a/src/context.rs b/src/context.rs index 0020e296..bb62ed46 100644 --- a/src/context.rs +++ b/src/context.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use async_graphql_value::Value as InputValue; use fnv::FnvHashMap; +use http::header::{AsHeaderName, HeaderMap, IntoHeaderName}; use serde::de::{Deserialize, Deserializer}; use serde::ser::{SerializeSeq, Serializer}; use serde::Serialize; @@ -320,7 +321,7 @@ pub struct QueryEnvInner { pub fragments: HashMap>, pub uploads: Vec, pub ctx_data: Arc, - pub http_headers: spin::Mutex>, + pub http_headers: spin::Mutex>, } #[doc(hidden)] @@ -443,12 +444,124 @@ impl<'a, T> ContextBase<'a, T> { .and_then(|d| d.downcast_ref::()) } - /// Sets an HTTP header to response. - pub fn set_http_header(&self, name: impl Into, value: impl Into) { + /// Returns whether the HTTP header `key` is currently set on the response + /// + /// # Examples + /// + /// ```no_run + /// use async_graphql::*; + /// use ::http::header::ACCESS_CONTROL_ALLOW_ORIGIN; + /// + /// struct Query; + /// + /// #[Object] + /// impl Query { + /// async fn greet(&self, ctx: &Context<'_>) -> String { + /// + /// let header_exists = ctx.http_header_contains("Access-Control-Allow-Origin"); + /// assert!(!header_exists); + /// + /// ctx.insert_http_header(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + /// + /// let header_exists = ctx.http_header_contains("Access-Control-Allow-Origin"); + /// assert!(header_exists); + /// + /// String::from("Hello world") + /// } + /// } + /// ``` + pub fn http_header_contains(&self, key: impl AsHeaderName) -> bool { + self.query_env.http_headers.lock().contains_key(key) + } + + /// Sets a HTTP header to response. + /// + /// If the header was not currently set on the response, then `None` is returned. + /// + /// If the response already contained this header then the new value is associated with this key + /// and __all the previous values are removed__, however only a the first previous + /// value is returned. + /// + /// See [`http::HeaderMap`] for more details on the underlying implementation + /// + /// # Examples + /// + /// ```no_run + /// use async_graphql::*; + /// use ::http::header::ACCESS_CONTROL_ALLOW_ORIGIN; + /// + /// struct Query; + /// + /// #[Object] + /// impl Query { + /// async fn greet(&self, ctx: &Context<'_>) -> String { + /// + /// // Headers can be inserted using the `http` constants + /// let was_in_headers = ctx.insert_http_header(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + /// assert_eq!(was_in_headers, None); + /// + /// // They can also be inserted using &str + /// let was_in_headers = ctx.insert_http_header("Custom-Header", "1234"); + /// assert_eq!(was_in_headers, None); + /// + /// // If multiple headers with the same key are `inserted` then the most recent + /// // one overwrites the previous. If you want multiple headers for the same key, use + /// // `append_http_header` for subsequent headers + /// let was_in_headers = ctx.insert_http_header("Custom-Header", "Hello World"); + /// assert_eq!(was_in_headers, Some("1234".to_string())); + /// + /// String::from("Hello world") + /// } + /// } + /// ``` + pub fn insert_http_header( + &self, + name: impl IntoHeaderName, + value: impl Into, + ) -> Option { self.query_env .http_headers .lock() - .insert(name.into(), value.into()); + .insert(name, value.into()) + } + + /// Sets a HTTP header to response. + /// + /// If the header was not currently set on the response, then `false` is returned. + /// + /// If the response did have this header then the new value is appended to the end of the + /// list of values currently associated with the key, however the key is not updated + /// _(which is important for types that can be `==` without being identical)_. + /// + /// See [`http::HeaderMap`] for more details on the underlying implementation + /// + /// # Examples + /// + /// ```no_run + /// use async_graphql::*; + /// use ::http::header::SET_COOKIE; + /// + /// struct Query; + /// + /// #[Object] + /// impl Query { + /// async fn greet(&self, ctx: &Context<'_>) -> String { + /// // Insert the first instance of the header + /// ctx.insert_http_header(SET_COOKIE, "Chocolate Chip"); + /// + /// // Subsequent values should be appended + /// let header_already_exists = ctx.append_http_header("Set-Cookie", "Macadamia"); + /// assert!(header_already_exists); + /// + /// String::from("Hello world") + /// } + /// } + /// ``` + pub fn append_http_header(&self, name: impl IntoHeaderName, value: impl Into) -> bool { + self.query_env + .http_headers + .lock() + .append(name, value.into()) } fn var_value(&self, name: &str, pos: Pos) -> ServerResult { diff --git a/src/extensions/logger.rs b/src/extensions/logger.rs index 0ac644ed..4a9823e0 100644 --- a/src/extensions/logger.rs +++ b/src/extensions/logger.rs @@ -70,7 +70,7 @@ impl Extension for LoggerExtension { struct DisplayError<'a> { log: &'a LoggerExtension, e: &'a ServerError, - }; + } impl<'a> Display for DisplayError<'a> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "[Error] ")?; diff --git a/src/response.rs b/src/response.rs index 966c39de..78e60828 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use http::header::HeaderMap; use serde::{Deserialize, Serialize}; @@ -25,7 +25,7 @@ pub struct Response { /// HTTP headers #[serde(skip)] - pub http_headers: HashMap, + pub http_headers: HeaderMap, } impl Response { @@ -116,20 +116,20 @@ impl BatchResponse { } } - /// Gets HTTP headers + /// Provides an iterator over all of the HTTP headers set on the response pub fn http_headers(&self) -> impl Iterator { let it: Box> = match self { BatchResponse::Single(resp) => Box::new( resp.http_headers .iter() - .map(|item| (item.0.as_str(), item.1.as_str())), + .map(|(key, value)| (key.as_str(), value.as_str())), ), BatchResponse::Batch(resp) => Box::new( resp.iter() .map(|r| { r.http_headers .iter() - .map(|item| (item.0.as_str(), item.1.as_str())) + .map(|(key, value)| (key.as_str(), value.as_str())) }) .flatten(), ),