I have a node gateway right now and I was looking to move to Apollo Router. The only functionality that doesn’t come out of the box for Router that I need to port over from my gateway is to return headers to the client that were set by a subgraph. I took a stab at a custom plugin for this but the headers set by my subgraphs are still not being passed through to the client. Here’s what my plugin looks like:
use apollo_router_core::{
register_plugin, Plugin, RouterRequest, RouterResponse, SubgraphResponse, SubgraphRequest,
};
use http::HeaderValue;
use tower::{util::BoxService, BoxError, ServiceBuilder, ServiceExt};
use cookie::{Cookie, SameSite};
use schemars::JsonSchema;
use serde::Deserialize;
use once_cell::sync::OnceCell;
static SET_COOKIE_HEADER: &str = "set-cookie";
static DOMAIN: OnceCell<String> = OnceCell::new();
#[derive(Debug, Default, Deserialize, JsonSchema)]
struct Conf {
allow_local_host: bool,
cookie_domain: String,
cookie_same_site: String,
use_secure_cookie: bool,
}
// We are storing the configuration, but not using it. Hence the allow dead code.
#[derive(Debug, Default)]
struct CookieSetter {
allow_local_host: bool,
cookie_same_site: Option<SameSite>,
use_secure_cookie: bool,
}
#[async_trait::async_trait]
impl Plugin for CookieSetter {
type Config = Conf;
async fn new(configuration: Self::Config) -> Result<Self, BoxError> {
let same_site = configuration.cookie_same_site.clone().to_lowercase();
let cookie_same_site = match same_site.as_str() {
"strict" => SameSite::Strict,
"lax" => SameSite::Lax,
_ => SameSite::None,
};
let domain = configuration.cookie_domain.clone();
DOMAIN.set(domain).unwrap();
return Ok(Self {
allow_local_host: configuration.allow_local_host,
cookie_same_site: Some(cookie_same_site),
use_secure_cookie: configuration.use_secure_cookie,
})
}
fn router_service(
&mut self,
service: BoxService<RouterRequest, RouterResponse, BoxError>,
) -> BoxService<RouterRequest, RouterResponse, BoxError> {
// `ServiceBuilder` provides us with `map_request` and `map_response` methods.
//
// These allow basic interception and transformation of request and response messages.
let same_site = self.cookie_same_site.clone();
let allow_local_host = self.allow_local_host.clone();
let use_secure_cookie = self.use_secure_cookie.clone();
ServiceBuilder::new()
.service(service)
.map_response(move |mut resp: RouterResponse| {
let headers = resp.response.headers_mut();
for (key, val) in headers.clone() {
match key {
Some(name) => {
let name_lower = name.as_str().to_lowercase();
if name_lower == SET_COOKIE_HEADER {
let cookie_string = val.to_str().unwrap().to_owned();
let result = Cookie::parse(cookie_string);
match result {
Ok(mut cookie) => {
cookie.set_same_site(same_site);
cookie.set_secure(use_secure_cookie);
if allow_local_host {
cookie.unset_domain();
} else {
cookie.set_domain(DOMAIN.get().unwrap());
}
let new_header = cookie.to_string();
let header_value = HeaderValue::from_str(new_header.as_str()).unwrap();
headers.insert(name, header_value);
},
Err(_) => (),
}
}
},
None => (),
}
}
resp
})
.boxed()
}
fn subgraph_service(
&mut self,
_nam: &str,
service: BoxService<SubgraphRequest, SubgraphResponse, BoxError>,
) -> BoxService<SubgraphRequest, SubgraphResponse, BoxError> {
// `ServiceBuilder` provides us with `map_request` and `map_response` methods.
//
// These allow basic interception and transformation of request and response messages.
let same_site = self.cookie_same_site.clone();
let allow_local_host = self.allow_local_host.clone();
let use_secure_cookie = self.use_secure_cookie.clone();
ServiceBuilder::new()
.service(service)
.map_response(move |mut resp: SubgraphResponse| {
let headers = resp.response.headers_mut();
for (key, val) in headers.clone() {
match key {
Some(name) => {
let name_lower = name.as_str().to_lowercase();
if name_lower == SET_COOKIE_HEADER {
let cookie_string = val.to_str().unwrap().to_owned();
let result = Cookie::parse(cookie_string);
match result {
Ok(mut cookie) => {
cookie.set_same_site(same_site);
cookie.set_secure(use_secure_cookie);
if allow_local_host {
cookie.unset_domain();
} else {
cookie.set_domain(DOMAIN.get().unwrap());
}
let new_header = cookie.to_string();
let header_value = HeaderValue::from_str(new_header.as_str()).unwrap();
headers.insert(name, header_value);
},
Err(_) => (),
}
}
},
None => (),
}
}
resp
})
.boxed()
}
}
register_plugin!("plugins", "cookie", CookieSetter);