2

In a Rails6 app with webpacker replaced by sprockets, I do not manage to let sprockets make my browser cache fonts. Edit: my browser does cache the font, but google complains and curl shows how the App responds (not as expected with a 304, see below).

Update

It seems that a 304 is only returned when you tell the server (via If-Modified-Since-headers) that you know exactly the last modified version. While I Mozillas Dev Resources do not state that this should clearly be the case (and I am not in RFC-reading mood), it might make sens:

  • your server serves the asset on 2020-01-01 (appreviated date for simplicity)
  • a browser visits you and stores the asset alongside its date
  • next day same browser revisits, asks server for asset and tells it the last known date (2020-01-01 via If-Modified-Since-header)
    • server answes 304: You know that stuff already
  • next day a mistake happens and a dev-asset is served by the server
  • browser revisits, gets new (but wrong asset with Last modified date of 2020-01-03) and stores it alongside that date
  • server admins remove the wrong dev asset
  • next day, browser visits and tells server "I know the thing from yesterday"
    • server tells browser: no, forgot that, the correct payload is this, and this is the timestamp: 2020-01-01.

In my tests below, I used If-Modified-Since headers that did not correspond to the last (production) asset Timestamp. Thanks @bliof for help figuring that out.

As my ultimate goal was to make googles speed insight happy (now that I know that this 304- response works if all players behave well) I will follow Rails 5+ path of config.public_file_server.headers (https://blog.bigbinary.com/2015/10/31/rails-5-allows-setting-custom-http-headers-for-assets.html). The Rails guides also point out how you would usually let your webserver (or CDN) handle the situation (https://guides.rubyonrails.org/asset_pipeline.html#in-production), but my stack works somewhat different.

Original follows

The fonts are in e.g. app/assets/fonts/OTF/SourceSansPro-BoldIt.otf and correctly put in public/assets/OTF/...fingerprint... (accompanied by a .gz variant). They are referenced via a SCSS font-face rule, pointing to a file with the respective fingerprint in it (using font-url()).

When curling these, I seem to never get a HTTP/1.1 304 Not Modified, but a 200 with the given payload. With the other (JS, CSS) assets it works as expected.

I did not modify config/initializers/assets.rb, as all the subdirectories and files should already be picked up (and the assets:precompile output and content of public/assets shows that it works).

Digging into the sprockets code at https://github.com/rails/sprockets/blob/9909da64595ddcfa1e7ee40ed1d99738961288ec/lib/sprockets/server.rb#L73 seems to indicate that maybe an etag is not set correctly or something like that, but I do not really grock that code.

The application is deployed with dokku (basically a heroku) with a pretty standard nginx-configuration as far as I can tell: https://github.com/dokku/dokku/blob/master/plugins/nginx-vhosts/templates/nginx.conf.sigil . The app serves the assets itself (like in heroku).

What do I have to do such that sprockets adds the relevant headers / responds "correctly" with a 304? Any ideas how to debug that issue?

The relevant "debugging" parts

The initial request for CSS

curl -v https://...application-3d...c76c3.css \
  -H 'Accept: text/css,*/*;q=0.1'\
  -H 'Accept-Language: en-US,en;q=0.5'\
  --compressed # omitted: ... User-Agent, DNT, ...
# omitted: TLS handshake etc
> GET /assets/application-3d...c76c3.css HTTP/1.1
> Host: #the host
> Accept-Encoding: deflate, gzip
> User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0
> Accept: text/css,*/*;q=0.1
> Accept-Language: en-US,en;q=0.5
> Referer: #the host
> DNT: 1
> Connection: keep-alive
> Cookie: #a cookie
> 
< HTTP/1.1 200 OK
< Server: nginx
< Date: Tue, 21 Apr 2020 15:39:47 GMT
< Content-Type: text/css
< Content-Length: 41256
< Connection: keep-alive
< Last-Modified: Mon, 06 Apr 2020 11:59:56 GMT
< Content-Encoding: gzip
< Vary: Accept-Encoding
< 
# payload

Subsequent fetch of CSS

(The relevant parts, other params and output omitted). Note that a If-Modified-Since: Mon, 06 Apr 2020 11:59:56 GMT header is sent along.

curl -v 'https://.../assets/application-3d...c76c3.css' \
  -H 'If-Modified-Since: Mon, 06 Apr 2020 11:59:56 GMT'\
  -H 'Cache-Control: max-age=0'
> If-Modified-Since: Mon, 06 Apr 2020 11:59:56 GMT
> Cache-Control: max-age=0
> 
< HTTP/1.1 304 Not Modified
< Server: nginx
< Date: Tue, 21 Apr 2020 15:50:52 GMT
< Connection: keep-alive

(Thats what I want: A 304 Not Modified.

The initial request for the font asset

curl -v 'https://.../assets/WOFF2/TTF/SourceSansPro-Light.ttf-32...d9.woff2' \
  -H 'Accept: application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8'\
  -H 'Accept-Language: en-US,en;q=0.5'\
  --compressed \
  -H 'Referer: https://...assets/application-3d....c76c3.css'
  # ommitted: User Agent, Cookies, ....
> GET /assets/WOFF2/TTF/SourceSansPro-Light.ttf-32...d9.woff2 HTTP/1.1
> Host: #the host
> Accept-Encoding: deflate, gzip
> User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0
> Accept: application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8
> Accept-Language: en-US,en;q=0.5
> DNT: 1
> Connection: keep-alive
> Referer: https://.../assets/application-3d...c76c3.css
# cookie etc
> 
< HTTP/1.1 200 OK
< Server: nginx
< Date: Tue, 21 Apr 2020 15:45:34 GMT
< Content-Type: application/font-woff2
< Content-Length: 88732
< Connection: keep-alive
< Last-Modified: Wed, 25 Mar 2020 20:09:14 GMT
< 
# payload

Subsequent fetch of Font

curl -v 'https://.../assets/WOFF2/TTF/SourceSansPro-Light.ttf-32...ed9.woff2' \
  -H 'Referer: https://.../assets/application-3d...c76c3.css'\
  -H 'If-Modified-Since: Mon, 06 Apr 2020 11:59:56 GMT'
  -H 'Cache-Control: max-age=0'
# ....
> If-Modified-Since: Mon, 06 Apr 2020 11:59:56 GMT
> Cache-Control: max-age=0
> 
< HTTP/1.1 200 OK
< Server: nginx
< Date: Tue, 21 Apr 2020 15:53:46 GMT
< Content-Type: application/font-woff2
< Content-Length: 88732
< Connection: keep-alive
< Last-Modified: Wed, 25 Mar 2020 20:09:14 GMT
# payload

What I find interesting, that the server actually sends a Last-Modified which is way before the If-Modified-Since. I guess clever browsers stop the conversation there, but I really want to see a well-behaved 304.

Felix
  • 4,510
  • 2
  • 31
  • 46

1 Answers1

1

Here are few notes/findings:


It seems that it returns 304 when you match the timestamp.

In your example if you do the curl to the font with

-H 'If-Modified-Since: Wed, 25 Mar 2020 20:09:14 GMT'

You'll get the HTTP/1.1 304 Not Modified

Same thing for the .css if you don't exactly match the date, you'll get 200.


I've tried changing sprockets locally to add some puts calls and also to change the default log level of sprockets itself but nothing happens.

TBO I don't believe the Sprokets::Server#call is getting called.


I've tried with puma and with thin, both return 304 only when the dates match.


curl --compressed -H 'Cache-Control: max-age=0' -H 'If-Modified-Since: Thu, 23 Apr 2020 21:34:30 GMT' -v http://localhost:3000/assets/OTF/SpaceMeatball-d61519ff17fadd38b57e3698067894c0e75fcb6031ee91034f5f7d6f2daa4d4b.otf

> Cache-Control: max-age=0
> If-Modified-Since: Thu, 23 Apr 2020 21:34:30 GMT
>
< HTTP/1.1 200 OK
< Last-Modified: Thu, 23 Apr 2020 21:34:29 GMT

curl --compressed -H 'Cache-Control: max-age=0' -H 'If-Modified-Since: Thu, 23 Apr 2020 21:34:29 GMT' -v http://localhost:3000/assets/OTF/SpaceMeatball-d61519ff17fadd38b57e3698067894c0e75fcb6031ee91034f5f7d6f2daa4d4b.otf

> Cache-Control: max-age=0
> If-Modified-Since: Thu, 23 Apr 2020 21:34:29 GMT
>
< HTTP/1.1 304 Not Modified

I am running rails like this:

RAILS_SERVE_STATIC_FILES=1 RAILS_ENV=production ./bin/rails s
or 
RAILS_SERVE_STATIC_FILES=1 RAILS_ENV=production bundle exec thin start

todo - find what exactly is returning the response :)

bliof
  • 2,957
  • 2
  • 23
  • 39
  • Yeah I tried puts-debugging at various call methods in sprockets, too. STDOUT is likely suppressed, so I also `File.write('/tmp/debug.txt', 'mymsg')`-debugged and I agree: it seems that other middleware is handling the request (rack itself?). – Felix Apr 24 '20 at 10:26
  • But do you agree that there should be a 304 if last-modified is "older" than "if-modified-since"? There is a bug, right? Or do I completely misunderstand something? Ah, thinking about it it makes sense that the query is done with the exact timestamp. I will investigate, but most likely this will take some time. – Felix Apr 24 '20 at 10:31
  • Here you go with the bounty. While the rails guides say it is the Sprocket Middleware, I think it is `ActionDispatch::Static` (maybe thats actually the same thing, didnt check). As I might conquer more territory, I will not yet mark your answer as accepted but might make a write-up from what I edited to the question. Thanks again! – Felix Apr 28 '20 at 15:09
  • 1
    Sorry, I've been busy lately. I'll try at some point to double check / update my answer ⏳ - it is quite an interesting question :) Otherwise there is a since in the name :D and the rfc has "modification date being more recent than the date provided" and so on. https://tools.ietf.org/html/rfc7232#section-3.3 tbo the current situation feels like a hacky way to accomplish 80-90% of the task. – bliof Apr 29 '20 at 16:38
  • Not sure if you have seen that I also updated my question. At least the PageSpeed Insight Google thing is happy if you set the headers, but nonetheless, I am also confused and hope to find the time to revisit the situation, also just to understand where the response is actually generated. Thanks so far. – Felix Apr 29 '20 at 21:01