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

ServeDir & ServeFile last modified headers handling #145

Merged
merged 9 commits into from
Jan 17, 2022

Conversation

zenria
Copy link
Contributor

@zenria zenria commented Sep 17, 2021

Note: This PR also includes #137, I will modify it when it will be merged

Motivation

ServeDir & ServeFile should respect http caching best practice by setting Last-Modified response header and handling If-Modified_since & If-Unmodified-Since requests headers.

Solution

The Last-Modified header is added on responses of ServeFile & ServeDir.

If-Modified_since & If-Unmodified-Since requests headers are handled so the service will respond NOT_MODIFIED or PRECONDITION_FAILED depending on values set into the headers.

@zenria zenria force-pushed the servedir-lastmod-handling branch from 8e83667 to 6b737a4 Compare September 18, 2021 15:05
@davidpdrsn
Copy link
Member

@zenria do you merge/rebase the latest master? Seems some things have changed making this hard to review.

@zenria zenria force-pushed the servedir-lastmod-handling branch from 6b737a4 to d64c656 Compare November 8, 2021 16:02
@zenria
Copy link
Contributor Author

zenria commented Nov 8, 2021

@zenria do you merge/rebase the latest master? Seems some things have changed making this hard to review.

I've just rebased my branch on master :)

@davidpdrsn davidpdrsn added this to the 0.2.0 milestone Nov 8, 2021
@davidpdrsn
Copy link
Member

Thank you! Given the size of this change I'm pushing it to 0.2 as I would like to get 0.1.2 our relatively soon.

tower-http/Cargo.toml Outdated Show resolved Hide resolved
@davidpdrsn
Copy link
Member

We made some more changes to CI end of last week so you'll have to rebase master again to get CI working.

Just a heads up there are also some other PRs landing this week which touch these files so you'll probably get some merge conflicts soon.

@zenria zenria force-pushed the servedir-lastmod-handling branch 2 times, most recently from a837668 to cef7a29 Compare November 16, 2021 17:10
@zenria
Copy link
Contributor Author

zenria commented Nov 16, 2021

I've rebased my branch, you were right, there was some merge conflicts ;)

I've got rid of headers crate, but I was forced to introduce a dependency on httpdate to parse and generate headers.

@davidpdrsn
Copy link
Member

Thanks! I'll take a look some time this week.

@davidpdrsn
Copy link
Member

Just a heads up. I'm waiting on #173 being merged before diving into this.

@davidpdrsn davidpdrsn removed this from the 0.2.0 milestone Nov 25, 2021
@davidpdrsn
Copy link
Member

Removing this from the 0.2 milestone. It wont require breaking changes so we can always ship it in 0.2.1

@shepmaster
Copy link
Contributor

Thank you for your work on this; I'm excited to have this functionality in tower-http!

Copy link
Member

@davidpdrsn davidpdrsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall! Using httpdate for parsing is fine. Hyper is also using it.

I'm sorry but it looks like you'll have to rebase master again 😞 At least ServeFile has been simplified quite a bit which should make this simpler as well.

tower-http/src/services/fs/serve_dir.rs Outdated Show resolved Hide resolved
@zenria zenria force-pushed the servedir-lastmod-handling branch from cef7a29 to cc21606 Compare January 7, 2022 09:49
@zenria
Copy link
Contributor Author

zenria commented Jan 7, 2022

I've rebased my branch :)

Copy link
Contributor

@shepmaster shepmaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comments don’t carry any weight in this repository, just actively reading through to see what changed.

fn from_header_value(value: &http::header::HeaderValue) -> Option<IfModifiedSince> {
String::from_utf8(value.as_bytes().to_vec())
.ok()
.map(|value| httpdate::parse_http_date(&value).ok())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map followed by flatten should be flat_map

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! you're right. (btw this is an option, as there is no flat_map on that type, I always forget I can use and_then to do that.)

String::from_utf8(value.as_bytes().to_vec())
.ok()
.map(|value| httpdate::parse_http_date(&value).ok())
.flatten()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

}

fn check_modified_headers(
modified: &Option<LastModified>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually you don’t want references to options, but an optional reference. Option::as_ref Can be used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case this does not matter, but I agree.

.headers()
.get(header::IF_MODIFIED_SINCE)
.map(IfModifiedSince::from_header_value)
.flatten();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here x2

@@ -424,7 +459,7 @@ fn append_slash_on_path(uri: Uri) -> Uri {
enum Output {
File(FileRequest),
Redirect(HeaderValue),
NotFound,
StatusCode(StatusCode),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels unfortunate that this went from one possibility to hundreds when it seemingly only needed two possibilities.

Copy link
Contributor Author

@zenria zenria Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the check_modified_headers can output 2 different status codes: 304 Not Modified and 412 Precondition Failed this is why I'm not using 2 more enum variants.

@@ -749,7 +787,6 @@ mod tests {
.unwrap();
let res = svc.oneshot(request).await.unwrap();

assert_eq!(res.headers()["content-type"], "text/plain");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why’s this get removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this is a merge error!

@@ -1,8 +1,7 @@
//! Service that serves a file.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why’s this get removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this too :(

Copy link
Collaborator

@Nehliin Nehliin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done! Thanks for the PR! I have some comments regarding the tests but those extra checks should still pass with this implementation. Also have a comment regarding some unnecessary heap allocations.

I will approve this since I personally think my comments can be easily fixed in a follow up pr by either you or me and this has been in the works for some time now. But it's up to @davidpdrsn if he wants to block this on my comments.

Comment on lines 237 to 230
String::from_utf8(value.as_bytes().to_vec())
.ok()
.and_then(|value| httpdate::parse_http_date(&value).ok())
.map(|time| IfModifiedSince(time.into()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can avoid heap allocation here

Suggested change
String::from_utf8(value.as_bytes().to_vec())
.ok()
.and_then(|value| httpdate::parse_http_date(&value).ok())
.map(|time| IfModifiedSince(time.into()))
std::str::from_utf8(value.as_bytes()))
.ok()
.and_then(|value| httpdate::parse_http_date(value).ok())
.map(|time| IfModifiedSince(time.into()))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I was not aware of this function, thanks a lot!

Comment on lines 252 to 245
String::from_utf8(value.as_bytes().to_vec())
.ok()
.and_then(|value| httpdate::parse_http_date(&value).ok())
.map(|time| IfUnmodifiedSince(time.into()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
String::from_utf8(value.as_bytes().to_vec())
.ok()
.and_then(|value| httpdate::parse_http_date(&value).ok())
.map(|time| IfUnmodifiedSince(time.into()))
std::str::from_utf8(value.as_bytes()))
.ok()
.and_then(|value| httpdate::parse_http_date(value).ok())
.map(|time| IfUnmodifiedSince(time.into()))

.unwrap();

let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please also check that the body is empty here like the spec states?

.unwrap();

let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check that the file is actually part of the body here as a sanity check.

.unwrap();

let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

.unwrap();

let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this shouldn't return the body.

@@ -1124,4 +1160,63 @@ mod tests {
&format!("bytes */{}", file_contents.len())
)
}
#[tokio::test]
async fn last_modified() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments here like in the tests for serve_file

@Nehliin
Copy link
Collaborator

Nehliin commented Jan 14, 2022

Ci issue should be fixed by this: #206

@zenria
Copy link
Contributor Author

zenria commented Jan 17, 2022

Ok, thank you @Nehliin I'll update the PR with the CI & suggested fixes :)

@zenria zenria force-pushed the servedir-lastmod-handling branch from 19f2f8d to a373bcd Compare January 17, 2022 08:01
@Nehliin Nehliin merged commit 83b1f24 into tower-rs:master Jan 17, 2022
@photino
Copy link

photino commented Sep 23, 2023

I cannot find the Last-Modified header in the response for tower-http 0.4.4.

@davidpdrsn
Copy link
Member

Please open a new issue instead of resurrecting such and old PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants