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

Setting Cache-Control header with serve_static #103

Open
ross-byrne opened this issue Nov 12, 2024 · 14 comments
Open

Setting Cache-Control header with serve_static #103

ross-byrne opened this issue Nov 12, 2024 · 14 comments

Comments

@ross-byrne
Copy link

Problem

Serving static files using wisp.serve_static doesn't give the option of setting a cache-control header. This means static files served this way are not cached by the browser.

I noticed this while serving my static content using the following:

use <- wisp.serve_static(req, under: "/static", from: ctx.asset_path)

It seems this code is responsible:

case simplifile.is_file(path) {
  Ok(True) ->
    response.new(200)
    |> response.set_header("content-type", content_type)
    |> response.set_body(File(path))
  _ -> handler()
}

Possible Solution

I don't have any strong opinions on the best solution. I doubt just throwing in an extra header here is a good idea as it would change the behavior.

Maybe we could add a new function that can take some kind of config?

For context my use case is, I'm serving a Svelte SPA from the backend. It would be nice to have the js files cached without having to re-implement my own version of wisp.serve_static :)

Happy to help with implementation if this is something you'd like to add.

Thanks!

@lpil
Copy link
Collaborator

lpil commented Nov 12, 2024

Thank you! I think this would be great to have, though I'm not entirely sure what the API and behaviour might be. How long would the cache be valid for? What about etags? Should the programmer be able to configure this? etc.

@ross-byrne
Copy link
Author

ross-byrne commented Nov 12, 2024

I'm not entirely sure what the API and behaviour might be

Yeah same honestly. My guess is that there would be too much nuance for there to be one good default. I'd like it to cache but others might not depending on the situation. So I'd lean towards having some way to set it.

Maybe making it less about caching specifically and more about being able to set response headers, could be good? I'm not sure if that level of flexibility is useful for others?

@ross-byrne
Copy link
Author

I get that this could also be a non issue if most people just use a CDN. So I'm happy to just implement my own version of this for now.

@lpil
Copy link
Collaborator

lpil commented Nov 15, 2024

I'm sure there is a good default out there! Maybe we can look at what other frameworks do and copy them.

While using a CDN is common I think there's a lot of value in supporting non-cloud deployments too, so let's figure that out.

@ross-byrne
Copy link
Author

OK leave it with me for now. I had a quick look but I think I need to do some more reading on what the different options are.

It seems the two Go based web frameworks I checked, Echo and Chi both leave it up to the developer to set the headers. By just wrapping the handler with another handler to set the response headers.

I looked at Ruby on Rails and they seem to set it for you, but they have a lot of options for caching. So I'd have to dig a bit more to find out what it's doing.

Either way, interesting!

@ross-byrne
Copy link
Author

ross-byrne commented Nov 20, 2024

@lpil so I've done a bit more reading on the topic and I'm curious about what your take on this is.

The quick summary is: HTML files should either not be cached or have a private, very short lived cache eg. 5 or 10 minutes.

Cache-Control: max-age:300, private

JS and CSS should ideally have versioned names to aid cache invalidation or "cache busting". If versioned, you can set the max-age to a year (1 year is the max a cache can be set).

Cache-Control: max-age=31536000, immutable

Un-versioned files are trickier. Those would need ETags to avoid issues with cached resources going stale. ETags is how Rails handles caching assets with it's asset pipeline but I believe it's off by default. While ETags would be a good feature, the file hashing required would add overhead. So I think that would be a separate thing a user opts into.

My Thoughts

Caching seems to be very context specific but if we're going to set a default value, the cache should probably be private to avoid accidentally leaking sensitive information (see: MDN). We could go with something like: Cache-Control: private, max-age=604800 which is valid for 7 days.

My gut feeling is, I'd like to be able to set the response header and/or a config to control how this works. So maybe it's better to leave serve_static as it is for now and look at adding an additional function that can take a config? Or something that returns the response so the user can customise it as needed (similar to Go based frameworks). Curious what your thoughts are on this and your appetite for either changing the existing api or adding new functions.

I'll be looking at tackling caching in my own backend soon, maybe next week. So I can prototype some things and share them here if that would be useful? Maybe we can start narrowing down what the API could look like?

Links: 1 2

@lpil
Copy link
Collaborator

lpil commented Nov 20, 2024

That sounds sensible, but it would be good to be able to support the common options people would want. Did you look at what other frameworks do?

@ross-byrne
Copy link
Author

Yeah sorry, I should have compiled a bit of a list. So I looked through some popular web frameworks and this is how they handle caching:

  • Express.js - uses etags but has default max-age set to 0. So files aren't cached. Takes options to make it easy to set. link
  • Laravel - No caching by default
  • Rails - Does asset fingerprinting by hashing the file contents - link
  • Echo - No caching by default, needs to be configured
  • Chi - No caching by default, needs to be configured
  • Next.js - Doesn't cache static assets by default. Needs to be configured. link
  • Hono.js - Doesn't cache by default. Has middleware with config. link
  • Spring - Sets cache for 1 year by default. Allows for configuration

Unless I've missed an obvious one, it seems most don't actually do caching by default but have middleware for configuring it.

@ross-byrne
Copy link
Author

Something like the express api would be nice

var options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now())
  }
}

app.use(express.static('public', options))

@lpil
Copy link
Collaborator

lpil commented Nov 25, 2024

That's great, thank you Ross. I think it would be good to have this middleware in a new package so we can experiment with APIs and approaches, and then if we are very happy with the design we could potentially move it into wisp.

@ross-byrne
Copy link
Author

Sounds good to me. Any ideas on what the package should be called? I'm not familiar with what an idiomatic name would be for a gleam package.

I know in the rust world, it's common to have <package>_mod_<my-thing>. So we could go with wisp_mod_caching or something? That would leave wisp_caching free if you wanted to take it over as a separate package instead of upstreaming it straight into wisp.

@lpil
Copy link
Collaborator

lpil commented Nov 26, 2024

It's best to avoid using the name of a project as the prefix as it implies it is maintained by the same people, but otherwise not particular convention.

@ross-byrne
Copy link
Author

Cheers, thanks for the input and guidance here. I don't want to take up too much of your time, so I'll check in again once I have something I've tested and I'm using myself.

@lpil
Copy link
Collaborator

lpil commented Nov 26, 2024

Love that approach. Extracting libraries from applications is a great way to design code using real-world experience

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

No branches or pull requests

2 participants