Rocket
Rocket copied to clipboard
Alllow customizing HTTP headers for `NamedFile`, `StaticFiles`
This has been converted into a tracking issue for "Alllow customizing HTTP headers for NamedFile, StaticFiles". Original text after the fold.
This tracking issue is a merger of the following:
- Static files should be sent with Last-Modified header #989
- add support for Last-Modified and If-Modified-Since headers #1376
- RFC for Attachments #791
- Default content-type for StaticFiles #1397
- Cache-Control and other headers for static served files #1522
In short, NamedFile, and by implication StaticFiles, should support options to compute and set more response headers. In summary, these are:
- [ ]
Last-Modified - [ ]
If-Modified-Since - [ ]
ETag - [ ]
If-Match - [ ]
Content-Disposition - [ ]
Cache-Control - [ ]
Content-Range,If-Range,Range - [ ] Fallback
Content-Type
Hi,
happy new year with successfull Rust projects!
I'm building a little personal project with Rust and Rocket. I have to serve static pages (js, images) and using the Firefox Inspector (to see how fast is this little app) I saw that static files served with NamedFile are not cached by the browser... as the only HTTP headers are Content-Type, Date, Server and Transfert-Encoding.
I thing that at least the header Last-Modified should be added (maybe also Expires and Cache-Control).
I'm still new to Rust and do not know how to derive/override the standard NamedFile to add the header using Response.set_header(). Looking at NamedFile source, it seems that adding new header could be done inside the original code.
Thanks for your project.
@eliovir: In the meantime i found a 'dirty' and maybe expensive workarround to fulfill our need.
#[get("/static/<file..>")]
fn files<'a>(file: PathBuf) -> Response<'a>
{
let result_read: io::Result<NamedFile> = NamedFile::open(Path::new("static/").join(file));
let result_namefile1 = result_read.ok();
let mut response_build = Response::build();
match result_namefile1
{
None => { }
Some(mut nfile) => {
let nfile_result: Result<Response,Status> = nfile.respond();
match nfile_result {
Ok(nfile_result_response) => {
response_build = Response::build_from(nfile_result_response);
response_build.raw_header("Cache-control","max-age=86400"); // 24h (24*60*60)
},
Err(nfile_result_status) => {
response_build.raw_status(nfile_result_status.code, nfile_result_status.reason);
},
}
}
}
response_build.finalize()
}
The code from @majkcramer is now obsolete, I've found this way to do it with current Rocket:
struct CachedFile(NamedFile);
impl<'r> Responder<'r> for CachedFile {
fn respond_to(self, req: &Request) -> response::Result<'r> {
Response::build_from(self.0.respond_to(req)?)
.raw_header("Cache-control", "max-age=86400") // 24h (24*60*60)
.ok()
}
}
#[get("/resources/<file..>")]
fn files(file: PathBuf) -> Option<CachedFile> {
NamedFile::open(Path::new("resources/").join(file)).ok().map(|nf| CachedFile(nf))
}
any progress on this, it has been nearly 5 years
For anybody wanting to take a stab at this, I encourage doing so, but please note that several PR attempts haven't made it through for a variety of reasons. For example, from https://github.com/SergioBenitez/Rocket/pull/1802:
I don't think the API or implementation here are ones that I'm in favor of. Specifically:
- This API does not leave room for growth into headers which are not simply flags, that is, which require a value like
Cache-Control.- This API reuses
OptionsfromStaticFilesforNamedFilewhen only one bitfield actually applies.- This implementation introduces a dependency on
headerfor two types.- The constructor approach (
with_last_modified_date()) results in lengthy invocations.- The
Responderimpl indexes into a vector without checking it contains a value and thenunwrap()s. This can trivially lead to panics.- There are a many
vec![],to_string()allocations. This functionality should require none.Before revisting an implementation for this and #95 in general, I would like to agree on an API design and implementation strategy that has none of these drawbacks. Until then, I am closing this pull request.
From https://github.com/SergioBenitez/Rocket/pull/1802#issuecomment-1491097856:
Unfortunately, I cannot accept the PR as is because it reimplements functionality that already exists in Rocket. I do not understand why a new responder type
StaticFileis being added when Rocket has (and has had)NamedFile. I also don't understand how these changes apply to anything existing, includingFileServerandNamedFile. It feels to me that this is a dump of an external library into Rocket's core, which is generally not something we'd like to do.Since this can live outside of Rocket and doesn't integrate into any of Rocket's existing infrastructure (and duplicates much of it), I'm closing this out. If you'd like to pursue this further, please submit a PR that begins with a design idea and perhaps a minimal implementation of it. For example, an implementation that includes two headers, one which has a value and one which doesn't, and an example enabling those headers for
FileServerandNamedFile.
A successful PR would:
- Suggest an API that is as uniform as possible across
NamedFileandFileServer. - Allow setting and unsetting the headers outlined in the issue.
- Allows setting headers with values and without values.
- Integrates well and ideally follows existing motifs in Rocket.
I imagine something like the following:
let headers = ETag | LastModified | Cache::MaxAge(3.days()) | Cache::MustRevalidate;
NamedFile::open("foo").await.headers(headers.clone());
FileServer::from("/foo").headers(headers);
I'm not sure how to get this particular API (the | chaining in headers), but notably, it's 1) possible to create a set of headers the you can apply to both NamedFile and FileServer without chaining a bunch of methods, and 2) the API to apply them is identical. I'd be highly in favor of an API of this nature.
Here's an alternative workaround solution which still makes use of the validations in FileServer by creating a thin layer around it:
#[derive(Debug, Clone)]
struct CachedFileServer {
root: PathBuf,
server: FileServer,
}
impl CachedFileServer {
/// The default rank use by `FileServer` routes.
const DEFAULT_RANK: isize = 10;
#[track_caller]
pub fn from<P: AsRef<Path>>(path: P) -> Self {
CachedFileServer { root: path.as_ref().into(), server: FileServer::from(path) }
}
}
impl From<CachedFileServer> for Vec<Route> {
fn from(server: CachedFileServer) -> Self {
let source = figment::Source::File(server.root.clone());
let mut route = Route::ranked(CachedFileServer::DEFAULT_RANK, Method::Get, "/<path..>", server);
route.name = Some(format!("CachedFileServer: {}", source).into());
vec![route]
}
}
#[async_trait::async_trait]
impl Handler for CachedFileServer {
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
match self.server.handle(req, data).await {
Outcome::Success(mut resp) => {
resp.set_raw_header("Cache-control", "max-age=86400"); // 24h (24 * 60 * 60)
Outcome::Success(resp)
},
i => i,
}
}
}