Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle GET request query parameters. #429

Merged
merged 22 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5a1d073
Plugin utils.
o0Ignition0o Feb 9, 2022
4b3cfe2
review comments
o0Ignition0o Feb 10, 2022
adc457e
remove the service_mock feature gate
o0Ignition0o Feb 10, 2022
d9fcb56
flatten the plugin_utils directory + address TypedBuilder comments
o0Ignition0o Feb 10, 2022
9ad45a4
Handle GET request query parameters.
o0Ignition0o Feb 9, 2022
3384a10
remove unfortunate unwrap leftovers
o0Ignition0o Feb 9, 2022
756b479
revert apq.rs, it belongs in an other pr
o0Ignition0o Feb 9, 2022
7b7aee6
provide more context as to why we perform a string replace on the rec…
o0Ignition0o Feb 10, 2022
84f5056
wip: prevent mutations from http get requests
o0Ignition0o Feb 10, 2022
1a76a76
bump federation dependency so we can check for the operation type
o0Ignition0o Feb 10, 2022
9a3fffc
ForbitHttpGetMutations layer
o0Ignition0o Feb 10, 2022
b20c893
update get mutations error message so it matches the nodejs one. it's…
o0Ignition0o Feb 11, 2022
d2a8b95
fix conflicts
o0Ignition0o Feb 11, 2022
ea2b84f
remove comments
o0Ignition0o Feb 11, 2022
8c588c1
servicebuilder.layer() instead of the service fold
o0Ignition0o Feb 11, 2022
9553e02
nit fixup
o0Ignition0o Feb 14, 2022
dbc17d2
clippy fixes
o0Ignition0o Feb 14, 2022
90a47ba
Merge branch 'geal-all-along-the-tower-service' into igni/http_get_su…
o0Ignition0o Feb 14, 2022
2d5ce64
Request::default + use same router-bridge reference as in main
o0Ignition0o Feb 14, 2022
9165f64
looks even better now, thanks Gary
o0Ignition0o Feb 15, 2022
7e92d56
Merge branch 'geal-all-along-the-tower-service' into igni/http_get_su…
o0Ignition0o Feb 15, 2022
ad7226f
add subscription variant
o0Ignition0o Feb 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apollo-router-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ router-bridge = { git = "https://github.com/apollographql/federation-rs.git", re
serde = { version = "1.0.136", features = ["derive", "rc"] }
serde_json = { version = "1.0.79", features = ["preserve_order"] }
serde_json_bytes = { version = "0.2.0", features = ["preserve_order"] }
serde_urlencoded = "0.7.1"
static_assertions = "1.1.0"
thiserror = "1.0.30"
tokio = { version = "1.16.1", features = ["rt", "sync"] }
Expand All @@ -45,6 +46,7 @@ tracing = "0.1.30"
tracing-futures = "0.2.5"
typed-builder = "0.9.1"
url = "2.2.2"
urlencoding = "2.1.0"

[dev-dependencies]
insta = "1.12.0"
Expand Down
228 changes: 228 additions & 0 deletions apollo-router-core/src/layers/forbid_http_get_mutations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use crate::{plugin_utils, ExecutionRequest, ExecutionResponse};
use futures::future::BoxFuture;
use http::{Method, StatusCode};
use std::task::Poll;
use tower::{BoxError, Layer, Service};

#[derive(Clone, Default)]
pub struct ForbidHttpGetMutations {}

pub struct ForbidHttpGetMutationsService<S>
where
S: Service<ExecutionRequest>,
{
service: S,
_forbid_http_get_mutations: ForbidHttpGetMutations,
}

impl<S> ForbidHttpGetMutationsService<S>
where
S: Service<ExecutionRequest>,
{
pub fn new(service: S) -> Self {
Self {
service,
_forbid_http_get_mutations: ForbidHttpGetMutations {},
}
}
}

impl<S> Layer<S> for ForbidHttpGetMutations
where
S: Service<ExecutionRequest, Response = ExecutionResponse, Error = BoxError>,
{
type Service = ForbidHttpGetMutationsService<S>;

fn layer(&self, service: S) -> Self::Service {
ForbidHttpGetMutationsService {
service,
_forbid_http_get_mutations: self.clone(),
}
}
}

impl<S> Service<ExecutionRequest> for ForbidHttpGetMutationsService<S>
where
S: Service<ExecutionRequest, Response = ExecutionResponse, Error = BoxError>,
S::Future: Send + 'static,
{
type Response = <S as Service<ExecutionRequest>>::Response;

type Error = <S as Service<ExecutionRequest>>::Error;

type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}

fn call(&mut self, req: ExecutionRequest) -> Self::Future {
if req.context.request.method() == Method::GET && req.query_plan.contains_mutations() {
let res = plugin_utils::ExecutionResponse::builder()
.errors(vec![crate::Error {
message: "GET supports only query operation".to_string(),
locations: Default::default(),
path: Default::default(),
extensions: Default::default(),
}])
.status(StatusCode::METHOD_NOT_ALLOWED)
.headers(vec![("Allow".to_string(), "POST".to_string())])
.context(req.context)
.build()
.into();

Box::pin(async { Ok(res) })
} else {
Box::pin(self.service.call(req))
}
}
}

#[cfg(test)]
mod forbid_http_get_mutations_tests {
use std::sync::Arc;

use super::*;
use crate::query_planner::fetch::OperationKind;
use crate::{
plugin_utils::{ExecutionRequest, ExecutionResponse, MockExecutionService},
Context, QueryPlan,
};
use http::{Request, StatusCode};
use serde_json::json;
use tower::ServiceExt;

#[tokio::test]
async fn it_lets_http_post_queries_pass_through() {
let mut mock_service = plugin_utils::MockExecutionService::new();

mock_service
.expect_call()
.times(1)
.returning(move |_| Ok(plugin_utils::ExecutionResponse::builder().build().into()));

let mock = mock_service.build();

let mut service_stack = ForbidHttpGetMutations {}.layer(mock);

let http_post_query_plan_request = create_request(Method::POST, OperationKind::Query);

let services = service_stack.ready().await.unwrap();
services.call(http_post_query_plan_request).await.unwrap();
}

#[tokio::test]
async fn it_lets_http_post_mutations_pass_through() {
let mut mock_service = MockExecutionService::new();

mock_service
.expect_call()
.times(1)
.returning(move |_| Ok(plugin_utils::ExecutionResponse::builder().build().into()));

let mock = mock_service.build();

let mut service_stack = ForbidHttpGetMutations {}.layer(mock);

let http_post_query_plan_request = create_request(Method::POST, OperationKind::Mutation);

let services = service_stack.ready().await.unwrap();
services.call(http_post_query_plan_request).await.unwrap();
}

#[tokio::test]
async fn it_lets_http_get_queries_pass_through() {
let mut mock_service = MockExecutionService::new();

mock_service
.expect_call()
.times(1)
.returning(move |_| Ok(ExecutionResponse::builder().build().into()));

let mock = mock_service.build();

let mut service_stack = ForbidHttpGetMutations {}.layer(mock);

let http_post_query_plan_request = create_request(Method::GET, OperationKind::Query);

let services = service_stack.ready().await.unwrap();
services.call(http_post_query_plan_request).await.unwrap();
}

#[tokio::test]
async fn it_doesnt_let_http_get_mutations_pass_through() {
let expected_error = crate::Error {
message: "GET supports only query operation".to_string(),
locations: Default::default(),
path: Default::default(),
extensions: Default::default(),
};
let expected_status = StatusCode::METHOD_NOT_ALLOWED;
let expected_allow_header = "POST";

let mock = MockExecutionService::new().build();
let mut service_stack = ForbidHttpGetMutations {}.layer(mock);

let http_post_query_plan_request = create_request(Method::GET, OperationKind::Mutation);

let services = service_stack.ready().await.unwrap();
let actual_error = services.call(http_post_query_plan_request).await.unwrap();

assert_eq!(expected_status, actual_error.response.status());
assert_eq!(
expected_allow_header,
actual_error.response.headers().get("Allow").unwrap()
);
assert_error_matches(&expected_error, actual_error);
}

fn assert_error_matches(expected_error: &crate::Error, response: crate::ExecutionResponse) {
assert_eq!(&response.response.body().errors[0], expected_error);
}

fn create_request(method: Method, operation_kind: OperationKind) -> crate::ExecutionRequest {
let root = if operation_kind == OperationKind::Mutation {
serde_json::from_value(json!({
"kind": "Sequence",
"nodes": [
{
"kind": "Fetch",
"serviceName": "product",
"variableUsages": [],
"operation": "{__typename}",
"operationKind": "mutation"
},
]
}))
.unwrap()
} else {
serde_json::from_value(json!({
"kind": "Sequence",
"nodes": [
{
"kind": "Fetch",
"serviceName": "product",
"variableUsages": [],
"operation": "{__typename}",
"operationKind": "query"
},
]
}))
.unwrap()
};

ExecutionRequest::builder()
.query_plan(Arc::new(QueryPlan { root }))
.context(
Context::new().with_request(Arc::new(
Request::builder()
.method(method)
.body(crate::Request::builder().query("").build())
.unwrap()
.into(),
)),
)
.build()
.into()
}
}
1 change: 1 addition & 0 deletions apollo-router-core/src/layers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod cache;
pub mod forbid_http_get_mutations;
pub mod header_manipulation;
2 changes: 1 addition & 1 deletion apollo-router-core/src/plugin_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ pub mod structures;
pub use service::{
MockExecutionService, MockQueryPlanningService, MockRouterService, MockSubgraphService,
};
pub use structures::{RouterRequest, RouterResponse};
pub use structures::{ExecutionRequest, ExecutionResponse, RouterRequest, RouterResponse};
1 change: 1 addition & 0 deletions apollo-router-core/src/plugin_utils/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
ExecutionRequest, ExecutionResponse, QueryPlannerRequest, QueryPlannerResponse, RouterRequest,
RouterResponse, SubgraphRequest,
};

#[macro_export]
macro_rules! mock_service {
($name:ident, $request_type:ty, $response_type:ty) => {
Expand Down
80 changes: 68 additions & 12 deletions apollo-router-core/src/plugin_utils/structures.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Context, Error, Object, Path};
use crate::{Context, Error, Object, Path, QueryPlan};
use http::{Request, Response, StatusCode};
use serde_json_bytes::{ByteString, Value};
use std::sync::Arc;
Expand Down Expand Up @@ -71,17 +71,9 @@ impl RouterResponse {
)
.expect("crate::Response implements Serialize; qed")
.into(),
context: this.context.unwrap_or_else(|| {
Context::new().with_request(Arc::new(
Request::new(crate::Request {
query: Default::default(),
operation_name: Default::default(),
variables: Default::default(),
extensions: Default::default(),
})
.into(),
))
}),
context: this
.context
.unwrap_or_else(|| Context::new().with_request(Arc::new(Default::default()))),
}
}
}
Expand All @@ -92,3 +84,67 @@ fn from_names_and_values(extensions: Vec<(&str, Value)>) -> Object {
.map(|(name, value)| (ByteString::from(name.to_string()), value))
.collect()
}

#[derive(Default, Clone, TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option)))]
pub struct ExecutionRequest {
query_plan: Option<Arc<QueryPlan>>,
context: Option<Context<CompatRequest>>,
}

impl From<ExecutionRequest> for crate::ExecutionRequest {
fn from(execution_request: ExecutionRequest) -> Self {
Self {
query_plan: execution_request.query_plan.unwrap_or_default(),
context: execution_request
.context
.unwrap_or_else(|| Context::new().with_request(Arc::new(Default::default()))),
}
}
}

#[derive(Default, Clone, TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option)))]
pub struct ExecutionResponse {
label: Option<String>,
data: Option<Value>,
path: Option<Path>,
has_next: Option<bool>,
#[builder(setter(!strip_option))]
errors: Vec<Error>,
#[builder(default, setter(!strip_option, transform = |extensions: Vec<(&str, Value)>| Some(from_names_and_values(extensions))))]
extensions: Option<Object>,
#[builder(default = StatusCode::OK, setter(!strip_option))]
status: StatusCode,
#[builder(default, setter(!strip_option))]
headers: Vec<(String, String)>,
context: Option<Context<CompatRequest>>,
}

impl From<ExecutionResponse> for crate::ExecutionResponse {
fn from(execution_response: ExecutionResponse) -> Self {
let mut response_builder = Response::builder().status(execution_response.status);

for (name, value) in execution_response.headers {
response_builder = response_builder.header(name, value);
}
let response = response_builder
.body(crate::Response {
label: execution_response.label,
data: execution_response.data.unwrap_or_default(),
path: execution_response.path,
has_next: execution_response.has_next,
errors: execution_response.errors,
extensions: execution_response.extensions.unwrap_or_default(),
})
.expect("crate::Response implements Serialize; qed")
.into();

Self {
response,
context: execution_response
.context
.unwrap_or_else(|| Context::new().with_request(Arc::new(Default::default()))),
}
}
}
Loading