Fix the problem that some integrations overwritten HTTP headers. #793

This commit is contained in:
Sunli 2022-01-24 14:14:07 +08:00
parent b9feedc2ca
commit c2feefdf09
14 changed files with 102 additions and 74 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [3.0.25] 2022-1-24
- Fix the problem that some integrations overwritten HTTP headers. [#793](https://github.com/async-graphql/async-graphql/issues/793)
# [3.0.24] 2022-1-24
- Remove `'static` bound for `impl From<T> for Error`.

View File

@ -177,15 +177,14 @@ impl Responder for GraphQLResponse {
type Body = BoxBody;
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
let mut res = HttpResponse::build(StatusCode::OK);
let mut builder = HttpResponse::build(StatusCode::OK);
if self.0.is_ok() {
if let Some(cache_control) = self.0.cache_control().value() {
res.append_header((http::header::CACHE_CONTROL, cache_control));
builder.append_header((http::header::CACHE_CONTROL, cache_control));
}
}
for (name, value) in self.0.http_headers() {
res.append_header((name, value));
}
let accept = req
.headers()
.get(http::header::ACCEPT)
@ -209,7 +208,13 @@ impl Responder for GraphQLResponse {
},
),
};
res.content_type(ct);
res.body(body)
let mut resp = builder.content_type(ct).body(body);
for (name, value) in self.0.http_headers_iter() {
resp.headers_mut().append(name, value);
}
resp
}
}

View File

@ -65,6 +65,7 @@ where
{
/// Specify the initial subscription context data, usually you can get something from the
/// incoming request to create it.
#[must_use]
pub fn with_data(self, data: Data) -> Self {
Self { data, ..self }
}

View File

@ -1,6 +1,5 @@
use axum::body::{boxed, Body, BoxBody};
use axum::http;
use axum::http::header::HeaderName;
use axum::http::{HeaderValue, Response};
use axum::response::IntoResponse;
@ -38,14 +37,8 @@ impl IntoResponse for GraphQLResponse {
}
}
}
for (name, value) in self.0.http_headers() {
if let (Ok(name), Ok(value)) = (
HeaderName::try_from(name.as_bytes()),
HeaderValue::from_str(value),
) {
resp.headers_mut().insert(name, value);
}
}
resp.headers_mut().extend(self.0.http_headers());
resp
}
}

View File

@ -193,6 +193,7 @@ where
{
/// Specify the initial subscription context data, usually you can get something from the
/// incoming request to create it.
#[must_use]
pub fn with_data(self, data: Data) -> Self {
Self { data, ..self }
}

View File

@ -1,4 +1,3 @@
use poem::http::header::HeaderName;
use poem::web::Json;
use poem::{IntoResponse, Response};
@ -38,13 +37,7 @@ impl IntoResponse for GraphQLBatchResponse {
}
}
for (name, value) in self.0.http_headers() {
if let (Ok(name), Ok(value)) = (TryInto::<HeaderName>::try_into(name), value.try_into())
{
resp.headers_mut().append(name, value);
}
}
resp.headers_mut().extend(self.0.http_headers());
resp
}
}

View File

@ -183,6 +183,7 @@ where
{
/// Specify the initial subscription context data, usually you can get something from the
/// incoming request to create it.
#[must_use]
pub fn with_data(self, data: Data) -> Self {
Self { data, ..self }
}

View File

@ -112,6 +112,7 @@ impl GraphQLRequest {
}
/// Insert some data for this request.
#[must_use]
pub fn data<D: Any + Send + Sync>(mut self, data: D) -> Self {
self.0.data.insert(data);
self
@ -215,8 +216,11 @@ impl<'r> Responder<'r, 'static> for GraphQLResponse {
response.set_header(Header::new("cache-control", cache_control));
}
}
for (name, value) in self.0.http_headers() {
response.adjoin_header(Header::new(name.to_string(), value.to_string()));
for (name, value) in self.0.http_headers_iter() {
if let Ok(value) = value.to_str() {
response.adjoin_header(Header::new(name.as_str().to_string(), value.to_string()));
}
}
response.set_sized_body(body.len(), Cursor::new(body));

View File

@ -167,9 +167,13 @@ pub fn respond(resp: impl Into<async_graphql::BatchResponse>) -> tide::Result {
response.insert_header(headers::CACHE_CONTROL, cache_control);
}
}
for (name, value) in resp.http_headers() {
response.append_header(name, value);
for (name, value) in resp.http_headers_iter() {
if let Ok(value) = value.to_str() {
response.append_header(name.as_str(), value);
}
}
response.set_body(Body::from_json(&resp)?);
Ok(response)
}

View File

@ -4,7 +4,6 @@ use std::io::ErrorKind;
use async_graphql::http::MultipartOptions;
use async_graphql::{BatchRequest, ObjectType, Schema, SubscriptionType};
use futures_util::TryStreamExt;
use warp::hyper::header::HeaderName;
use warp::reply::Response as WarpResponse;
use warp::{Buf, Filter, Rejection, Reply};
@ -85,13 +84,8 @@ impl Reply for GraphQLBatchResponse {
}
}
}
for (name, value) in self.0.http_headers() {
if let (Ok(name), Ok(value)) = (TryInto::<HeaderName>::try_into(name), value.try_into())
{
resp.headers_mut().append(name, value);
}
}
resp.headers_mut().extend(self.0.http_headers());
resp
}
}

View File

@ -233,6 +233,7 @@ where
{
/// Specify the initial subscription context data, usually you can get something from the
/// incoming request to create it.
#[must_use]
pub fn with_data(self, data: Data) -> Self {
Self { data, ..self }
}

View File

@ -9,6 +9,7 @@ use std::sync::{Arc, Mutex};
use async_graphql_value::{Value as InputValue, Variables};
use fnv::FnvHashMap;
use http::header::{AsHeaderName, HeaderMap, IntoHeaderName};
use http::HeaderValue;
use serde::ser::{SerializeSeq, Serializer};
use serde::Serialize;
@ -242,7 +243,7 @@ pub struct QueryEnvInner {
pub uploads: Vec<UploadValue>,
pub session_data: Arc<Data>,
pub ctx_data: Arc<Data>,
pub http_headers: Mutex<HeaderMap<String>>,
pub http_headers: Mutex<HeaderMap>,
pub disable_introspection: bool,
pub errors: Mutex<Vec<ServerError>>,
}
@ -434,6 +435,7 @@ impl<'a, T> ContextBase<'a, T> {
/// ```no_run
/// use async_graphql::*;
/// use ::http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
/// use ::http::HeaderValue;
///
/// struct Query;
///
@ -453,7 +455,7 @@ impl<'a, T> ContextBase<'a, T> {
/// // 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()));
/// assert_eq!(was_in_headers, Some(HeaderValue::from_static("1234")));
///
/// String::from("Hello world")
/// }
@ -462,13 +464,17 @@ impl<'a, T> ContextBase<'a, T> {
pub fn insert_http_header(
&self,
name: impl IntoHeaderName,
value: impl Into<String>,
) -> Option<String> {
self.query_env
.http_headers
.lock()
.unwrap()
.insert(name, value.into())
value: impl TryInto<HeaderValue>,
) -> Option<HeaderValue> {
if let Ok(value) = value.try_into() {
self.query_env
.http_headers
.lock()
.unwrap()
.insert(name, value)
} else {
None
}
}
/// Sets a HTTP header to response.
@ -503,12 +509,20 @@ impl<'a, T> ContextBase<'a, T> {
/// }
/// }
/// ```
pub fn append_http_header(&self, name: impl IntoHeaderName, value: impl Into<String>) -> bool {
self.query_env
.http_headers
.lock()
.unwrap()
.append(name, value.into())
pub fn append_http_header(
&self,
name: impl IntoHeaderName,
value: impl TryInto<HeaderValue>,
) -> bool {
if let Ok(value) = value.try_into() {
self.query_env
.http_headers
.lock()
.unwrap()
.append(name, value)
} else {
false
}
}
fn var_value(&self, name: &str, pos: Pos) -> ServerResult<Value> {

View File

@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use http::header::HeaderMap;
use http::header::{HeaderMap, HeaderName};
use http::HeaderValue;
use serde::{Deserialize, Serialize};
use crate::{CacheControl, Result, ServerError, Value};
@ -26,7 +27,7 @@ pub struct Response {
/// HTTP headers
#[serde(skip)]
pub http_headers: HeaderMap<String>,
pub http_headers: HeaderMap,
}
impl Response {
@ -57,7 +58,7 @@ impl Response {
/// Set the http headers of the response.
#[must_use]
pub fn http_headers(self, http_headers: HeaderMap<String>) -> Self {
pub fn http_headers(self, http_headers: HeaderMap) -> Self {
Self {
http_headers,
..self
@ -128,25 +129,30 @@ impl BatchResponse {
}
}
/// Provides an iterator over all of the HTTP headers set on the response
pub fn http_headers(&self) -> impl Iterator<Item = (&str, &str)> {
let it: Box<dyn Iterator<Item = (&str, &str)>> = match self {
BatchResponse::Single(resp) => Box::new(
resp.http_headers
.iter()
.map(|(key, value)| (key.as_str(), value.as_str())),
),
BatchResponse::Batch(resp) => Box::new(
resp.iter()
.map(|r| {
r.http_headers
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
})
.flatten(),
),
};
it
/// Returns HTTP headers map.
pub fn http_headers(&self) -> HeaderMap {
match self {
BatchResponse::Single(resp) => resp.http_headers.clone(),
BatchResponse::Batch(resp) => resp.iter().fold(HeaderMap::new(), |mut acc, resp| {
acc.extend(resp.http_headers.clone());
acc
}),
}
}
/// Returns HTTP headers iterator.
pub fn http_headers_iter(&self) -> impl Iterator<Item = (HeaderName, HeaderValue)> {
let headers = self.http_headers();
let mut current_name = None;
headers.into_iter().filter_map(move |(name, value)| {
if let Some(name) = name {
current_name = Some(name);
}
current_name
.clone()
.map(|current_name| (current_name, value))
})
}
}

View File

@ -1,3 +1,4 @@
use ::http::HeaderValue;
use async_graphql::*;
#[tokio::test]
@ -37,8 +38,14 @@ pub async fn test_http_headers() {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let resp = schema.execute("{ value }").await;
assert_eq!(resp.http_headers.get("A").map(|s| &**s), Some("1"));
assert_eq!(
resp.http_headers.get("A"),
Some(&HeaderValue::from_static("1"))
);
let resp = schema.execute("{ err }").await;
assert_eq!(resp.http_headers.get("A").map(|s| &**s), Some("1"));
assert_eq!(
resp.http_headers.get("A"),
Some(&HeaderValue::from_static("1"))
);
}