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

Provide a helper to add Etag headers to responses #41

Open
tmattio opened this issue Apr 21, 2021 · 4 comments
Open

Provide a helper to add Etag headers to responses #41

tmattio opened this issue Apr 21, 2021 · 4 comments

Comments

@tmattio
Copy link
Contributor

tmattio commented Apr 21, 2021

It would be really useful to have a helper val with_etag : request -> response -> response (or similar) that computes a hash of the response body and compare it with the request's Etag. It would transform the response to a 304 Not modified if they match, or add the Etag header with the computed hash if they don't

@aantron
Copy link
Owner

aantron commented Apr 21, 2021

Thanks. I plan to look at ETag (and other cache control topics) in a few days, after finishing the last of the bulk doc writing. I also want to expose non-cryptographic hash functions that Dream already uses internally, IIRC MD5 and SHA-1, which could be useful for writing this, or similar helpers in user code.

@aantron
Copy link
Owner

aantron commented Apr 21, 2021

@tmattio It seems that this will work only for small files (so typical assets and pages, which is fine). Is that right? So the helper would check that the response body is not a stream. If it is a stream, it would leave the request alone. Otherwise, it seems that it would have to buffer the entire stream, which could be disastrous.

@tmattio
Copy link
Contributor Author

tmattio commented Apr 21, 2021

That sounds good, for larger files, if they are embedded with ocaml-crunch, the hash could be computed at compile time and the header could be added even for Stream responses. I created an issue for that: mirage/ocaml-crunch#53

But having Etag for small files and pages would be great already!

@yawaramin
Copy link
Contributor

yawaramin commented Jan 1, 2025

I think looking at Ruby on Rails' support for Conditional GET requests would be instructive: https://guides.rubyonrails.org/caching_with_rails.html#conditional-get-support

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |wants|
        # ... normal response processing
      end
    end
  end
end

One way to adapt this might be:

let show req =
  let product = ... in

  if stale req product.etag then
    (* Send a 200 OK response rendering the product. Remember to add the new
       ETag value to the response headers. *)
  else
    Dream.empty `Not_Modified

We can perhaps help the user a bit more:

let etag product = product.etag

let show req =
  let product = ... in

  if_stale req etag (fun () ->
    (* Render the product again *)
  )

And if_stale could handle all the logic of checking the ETag and either sending a 200 OK with the full rendering of the new product, and a new ETag, or sending 304 Not Modified, and also taking care to add the proper response headers ie Cache-Control: no-cache and Vary: ETag.

let etag str = {|"|} ^ str ^ {|"|}

let if_stale req hash refresh =
  let new_etag = etag hash in
  let resp = match Dream.header req "If-None-Match" with
    | Some old_etag when old_etag = new_etag -> Dream.empty `Not_Modified
    | _ ->
      let resp = refresh () in
      Dream.add_header resp "ETag" new_etag;
      resp
  in
  Dream.add_header resp "Cache-Control" "no-cache";
  Dream.add_header resp "Vary" "ETag";
  resp

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

3 participants