Easily add extra headers to an endpoint
I have some endpoints like this clipped example:
impl Index {
#[get("/")]
fn index(&self) -> impl Future<Item = FileResponse, Error = io::Error> + Send {}
#[get("/help")]
fn help(&self) -> impl Future<Item = FileResponse, Error = io::Error> + Send {}
}
impl Assets {
#[get("/assets/*asset")]
fn asset(&self, asset: PathBuf) -> impl Future<Item = FileResponse, Error = io::Error> + Send {}
}
I'd like to set the Cache-Control header to 1 hour for index / help and 1 year for asset.
It'd be nice to have some amount of reusable code for this, instead of manually mucking about with the http::Response in each endpoint.
Some ideas I had:
-
Add a higher-order service:
ServiceBuilder::new() .resource(Cache::new(one_hour, Index::new(config.root.clone()))) .resource(Cache::new(one_year, Assets::new(config.root))) -
Some kind of sugar (unclear on how to implement this)
#[get("/")] #[web(cache = one_hour)] fn index(&self) -> impl Future<Item = FileResponse, Error = io::Error> + Send {}
Ideas and suggested direction would be appreciated.
One thing that makes me lean towards the higher-order service is that it's similar to warp, similar to middleware, just regular Rust code, and probably more capable. For example, I probably want to add a "guess the content type based on the file extension" bit of code as well.
Here's my implementation for caching.
mod cache {
use std::time::Duration;
use tower_web::{self, routing::{IntoResource, RouteSet, Resource, RouteMatch}, util::BufStream};
use http::{self, header::HeaderValue, status::StatusCode};
use tokio::prelude::Poll;
use futures::{Future, Async};
#[derive(Debug, Clone)]
pub struct Cache<R> {
inner: R,
cache: HeaderValue,
}
impl<R> Cache<R> {
pub fn new(inner: R, time: Duration) -> Self {
use http::HttpTryFrom;
let x = format!("public, max-age={}", time.as_secs());
let cache = HeaderValue::try_from(x).expect("nah, dawg");
Self { inner, cache }
}
}
impl<R, S, RequestBody> IntoResource<S, RequestBody> for Cache<R>
where
R: IntoResource<S, RequestBody>,
S: ::tower_web::response::Serializer,
RequestBody: BufStream,
{
type Destination = R::Destination;
type Resource = CacheResource<R::Resource>;
fn routes(&self) -> RouteSet<Self::Destination> {
self.inner.routes()
}
fn into_resource(self, serializer: S) -> Self::Resource {
let Self { inner, cache } = self;
let inner = inner.into_resource(serializer);
CacheResource { inner, cache }
}
}
#[derive(Debug, Clone)]
pub struct CacheResource<R> {
inner: R,
cache: HeaderValue,
}
impl<R> Resource for CacheResource<R>
where
R: Resource,
{
type Destination = R::Destination;
type RequestBody = R::RequestBody;
type Buf = R::Buf;
type Body = R::Body;
type Future = CacheFuture<R::Future>;
fn dispatch(
&mut self,
destination: Self::Destination,
route_match: &RouteMatch,
body: Self::RequestBody
) -> Self::Future {
let inner = self.inner.dispatch(destination, route_match, body);
let cache = self.cache.clone();
CacheFuture { inner, cache: Some(cache) }
}
}
pub struct CacheFuture<F> {
inner: F,
cache: Option<HeaderValue>,
}
impl<F, B> Future for CacheFuture<F>
where
F: Future<Item = http::Response<B>, Error = tower_web::Error>
{
type Item = http::Response<B>;
type Error = tower_web::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let mut resp = try_ready!(self.inner.poll());
if let Some(cache) = self.cache.take() {
let status = resp.status();
// TESTME
if status == StatusCode::OK || status == StatusCode::NOT_MODIFIED {
resp.headers_mut().insert(http::header::CACHE_CONTROL, cache);
}
}
Ok(Async::Ready(resp))
}
}
}
@carllerche do you think something like this would live in tower-web?
add a "guess the content type based on the file extension"
Unfortunately, this cannot be done because Resource::dispatch does not provide access to the incoming URI.
It should provide access to the URI in the route match.
anyway, I agree with your intuition. The higer order service is the way to go here.
I just wonder if at this point the best course of action is to merge with warp. Is there much else for us to learn first?
the best course of action is to merge with warp.
I think there's strong benefit to such a path. For example, someone is already adding CORS support to warp, which is potentially duplicated work.
Is there much else for us to learn first?
Always a tough question because we don't know whaat we don't know. 😇 That being said, it feels like it's possible to continue learning in a combined framework.