How to make sprockets (or rack, or nginx?) motivate browsers to cache fonts and correctly return 304? - ruby-on-rails

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.

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 :)

Related

rails 4 http caching returning 200 iso 304, even with the same ETag and last_modified

I'm quite new to caching so I've been trying some different ways of caching my website. I've settled on HTTP caching now, because it's the most appropriate with sporadic updates and lots of users perusing the same pages over and over.
I'm struggling to get it working however. The site shows different content based on whether you're logged in or not, so I have to invalidate cache based on current_user as well as the latest update on the collection of models.
If I look in chrome inspect the ETag and the modified_since are the same, but the server returns a 200 instead of a 304. My code works in development environment, so I'm lost in how to troubleshoot it. Also a different page that only invalidates based on the collection of models (similar on latest update), does work as expected.
Code from the controller:
def index
...#some code
# HTTTP caching:
last_mod = #scraps.order("updated_at").last.updated_at
user = current_user ? current_user.id : 0
fresh_when etag: user.to_s, last_modified: last_mod, public: false
end
Output from chrome inspect
Response Headers:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Status: 200 OK
Last-Modified: Sun, 23 Jul 2017 20:40:53 GMT
Cache-Control: max-age=0, private, must-revalidate
ETag: W/"6e92592bdb6c3cf610020e2b076e64b4"
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Runtime: 3.187090
X-Request-Id: c698c0c6-8a0d-44ba-8ca9-3f162b766478
Date: Mon, 24 Jul 2017 14:49:38 GMT
Set-Cookie: ... [edited out]; path=/; HttpOnly
X-Powered-By: Phusion Passenger 5.0.30
Server: nginx/1.10.1 + Phusion Passenger 5.0.30
Content-Encoding: gzip
Request Headers:
GET /scraps?page=3&price_max=100&price_min=0&producer=silk+scraps HTTP/1.1
Host: www.picture-scraps.com
Connection: keep-alive
Accept: text/html, application/xhtml+xml, application/xml
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
X-XHR-Referer: https://www.picture-scraps.com/scraps?page=4&price_max=100&price_min=0&producer=silk+scraps
Referer: https://www.picture-scraps.com/scraps?page=4&price_max=100&price_min=0&producer=silk+scraps
Accept-Encoding: gzip, deflate, br
Accept-Language: nl-NL,nl;q=0.8,en-US;q=0.6,en;q=0.4,af;q=0.2
Cookie: ... [edited out]
If-None-Match: W/"6e92592bdb6c3cf610020e2b076e64b4"
If-Modified-Since: Sun, 23 Jul 2017 20:40:53 GMT
I can imagine some additional information is needed, so please request and I'll add to the question.
Figured it out today. This post provides the answer. I saw the server used weak etags while in the dev environment strong etags were used. The latter is as expected as weak etags were only introduced from rails 5 forward.
If you use Nginx with rails 4 you might experience the same problem. Installing rails_weak_etags gem solved it for me.

How to force nginx to include Content-Length header on HEAD requests

How do you configure nginx to return a valid Content-Length header when responding to a HTTP HEAD request? Currently my server returns this:
curl --head http://example.com/myfile.xml
HTTP/1.1 200 OK
Date: Tue, 23 Aug 2016 13:49:46 GMT
Content-Type: text/xml
Set-Cookie: __cfduid=da2daeaa59809916192f7ac0645d3a3e91471960186; expires=Wed, 23-Aug-17 13:49:46 GMT; path=/; domain=.example.com; HttpOnly
Last-Modified: Mon, 22 Aug 2016 16:20:26 GMT
Vary: Accept-Encoding
ETag: W/"57bb264a-5442b26a"
Expires: Thu, 31 Dec 2037 23:55:55 GMT
Cache-Control: max-age=315360000
Access-Control-Allow-Origin: *
Access-Control-Request-Method: *
Cache-Control: public
Server: cloudflare-nginx
CF-RAY: 9dac40-LHR
X-Cache: MISS from Squid
Via: 1.1 Squid (squid/3.2.14)
I must send the Content-Length header with the response to HEAD (if I don't, the service that checks that URL will never see that the file was changed, and will not download the new version). How do you set it up?
I might be wrong, but nginx probably isn't showing the content-length because of nature of dynamic content generated by Rails's application server.
You can ask Rails to send that in response header. In your Rails application's config/application.rb add the following middleware:
config.middleware.use "Rack::ContentLength"
This should return the content-length header in response.

enabling rails page caching causes http header charset to disappears

I need charset to be utf-8, which seem to be the case by default. Recently I enabled page caching for a few static pages:
caches_page :about
The caching works fine, and I see the corresponding about.html and contact.html pages generated in my /public folder, except when the page renders, it's no longer in utf-8.
After googling for a bit I tried looking at the http headers with wget, before and after caching:
first time:
$wget --server-response http://localhost:3000/about
HTTP request sent, awaiting response...
1 HTTP/1.1 200 OK
2 X-Ua-Compatible: IE=Edge
3 Etag: "f7b0b4dea015140f3b5ad90c3a392bef"
4 Connection: Keep-Alive
5 Content-Type: text/html; charset=utf-8
6 Date: Sun, 12 Jun 2011 03:44:22 GMT
7 Server: WEBrick/1.3.1 (Ruby/1.8.7/2009-06-12)
8 X-Runtime: 0.235347
9 Content-Length: 5520
10 Cache-Control: max-age=0, private, must-revalidate
cached:
$wget --server-response http://localhost:3000/about
Resolving localhost... 127.0.0.1
Connecting to localhost[127.0.0.1]:3000... connected.
HTTP request sent, awaiting response...
1 HTTP/1.1 200 OK
2 Last-Modified: Sun, 12 Jun 2011 03:34:42 GMT
3 Connection: Keep-Alive
4 Content-Type: text/html
5 Date: Sun, 12 Jun 2011 03:39:53 GMT
6 Server: WEBrick/1.3.1 (Ruby/1.8.7/2009-06-12)
7 Content-Length: 5783
as a result the page displays in ISO-8859-1 and I get a bunch of garbled text. Does anyone know how I can prevent this undesirable result? Thank you.
The solution will depend on the server used.
When you use page cache, the servers reads the server directly, so the rails stack does not provide encoding information to the server. Then the server default apply.
If you're using apache with passenger, add to the configuration:
AddDefaultCharset UTF-8
If you need specific charsets, use a solution like the one in http://www.philsergi.com/2007/06/rails-page-caching-and-mime-types.html
<LocationMatch \/(rss)\/?>
ForceType text/xml;charset=utf-8
</LocationMatch>
<LocationMatch \/(ical)\/?>
ForceType text/calendar;charset=utf-8
</LocationMatch>

BlackBerry Browser (4.5) refuses to cache CSS. Is there a workaround?

I develop an internal web application for my company. It is used by our field technicians, all of whom carry a BlackBerry 8330 running 4.5. I would consider myself fortunate to have such a consistent target platform, if it wasn't BB 4.5...
I've noticed a lot of request overhead in loading the site, and know that if only my CSS resources were cached, the load time would be cut dramatically. The BB always requests a full copy of the CSS file, no matter if the Expires, Cache-Control, or Last-Modified headers are set. It doesn't hit its cache, it doesn't send an If-Modified-Since, nothing.
Anyone run into this or know what I can do to workaround it? I'd really like to avoid inlining my CSS if I don't have to.
EDIT: I just noticed that it is always requesting the page twice. Below are diffs between to 2 requests
GET /css/bb.css HTTP/1.1 |GET /css/bb.css HTTP/1.1
User-Agent: BlackBerry8330/4.5.0.77|User-Agent: BlackBerry8330/4.5.0.77
profile: http://www.blackberry.net/|profile: http://www.blackberry.net/
-----------------------------------|Accept: application/vnd.rim.html,te
-----------------------------------|Connection: close
Referer: http://10.7.2.167/page.php|Referer: http://10.7.2.167/page.php
-----------------------------------|Accept-Charset: ISO-8859-1,UTF-8,US
Host: 10.7.2.167 |Host: 10.7.2.167
-----------------------------------|Accept-Language: en-US,en;q=0.5
-----------------------------------|x-wap-profile: "http://www.blackber
Cookie: PHPSESSID=xxxxxxxx; token=x|Cookie: PHPSESSID=xxxxxxxx; token=x
-----------------------------------|Via: MDS_5.0.0.86
|
HTTP/1.1 200 OK |HTTP/1.1 200 OK
Date: Sun, 05 Sep 2010 09:34:52 GMT|Date: Sun, 05 Sep 2010 09:34:54 GMT
Server: Apache/2.2.15 (Debian) |Server: Apache/2.2.15 (Debian)
Last-Modified: Sat, 04 Sep 2010 02:|Last-Modified: Sat, 04 Sep 2010 02:
ETag: "10426-d64-48f65ab39bf80" |ETag: "10426-d64-48f65ab39bf80"
Accept-Ranges: bytes |Accept-Ranges: bytes
Content-Length: 3428 |Content-Length: 3428
Cache-Control: max-age=12960000 |Cache-Control: max-age=12960000
Expires: Wed, 02 Feb 2011 09:34:52 |Expires: Wed, 02 Feb 2011 09:34:54
Vary: Accept-Encoding |Vary: Accept-Encoding
-----------------------------------|Connection: close
Content-Type: text/css |Content-Type: text/css

Passenger/Apache: Can't set expire headers for versioned resources (rewrite rule not recognized)

I'm trying to set the expire headers for Rails' auto-versioned resources, like whatever.css?1234567890 . (I don't want to set the expire headers for unversioned resources.) The only method I could find online involved two steps: 1) rewrite all urls that end in 10 digits to load from /public/add_expires_header instead of from /public, where add_expires_header is a symlink that points to /public 2) Add an expiry date to all files from add_expires_header.
Seems like a good idea -- but passenger doesn't seem to recognize the rewrite rule, as indicated by the below curl results.
(Note: a lot of people seemed to think they could accomplish my goal using FilesMatch, but I read elsewhere that FilesMatch can't see the query string.)
#from sites_enabled/sitename in the tags
...
RewriteCond %{QUERY_STRING} ^[0-9]{10}$
RewriteRule ^(.*)$ /add_expires_header%{REQUEST_URI} [QSA]
ExpiresActive On
ExpiresDefault "access plus 1 years"
...
-----
#curl indicates that rewrite rule isn't taking effect
manu#Blade-Server:~$ curl -I -L "http://x.com/stylesheets/style.css?1249092148"
HTTP/1.1 200 OK
Date: Tue, 11 Aug 2009 04:07:49 GMT
Server: Apache/2.2.11 (Ubuntu) Phusion_Passenger/2.2.4 PHP/5.2.6-3ubuntu4.1 with Suhosin-Patch
Last-Modified: Sat, 01 Aug 2009 02:02:28 GMT
ETag: "455b-2fbb-4700aedc5f500"
Accept-Ranges: bytes
Content-Length: 12219
Vary: Accept-Encoding
Content-Type: text/css
manu#Blade-Server:~$ curl -I -L "http://x.com/add_expires_header/stylesheets/style.css?1249092148"
HTTP/1.1 200 OK
Date: Tue, 11 Aug 2009 04:07:55 GMT
Server: Apache/2.2.11 (Ubuntu) Phusion_Passenger/2.2.4 PHP/5.2.6-3ubuntu4.1 with Suhosin-Patch
Last-Modified: Sat, 01 Aug 2009 02:02:28 GMT
ETag: "455b-2fbb-4700aedc5f500"
Accept-Ranges: bytes
Content-Length: 12219
Cache-Control: max-age=31536000
Expires: Wed, 11 Aug 2010 04:07:55 GMT
Vary: Accept-Encoding
Content-Type: text/css
I've also tried including the rewrite stuff in apache2.conf, httpd.conf, and public/.htacess
I prefer to do this by combining it with using an assets host on a separate subdomain to avoid the whole rewrite issue. That way you can set the expire headers for everything on that subdomain. You can activate this in rails in environments/production.rb.
If you don't want to go with a separate subdomain I think the code below should do it, although I have not tested it myself:
ExpiresActive On
<FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
ExpiresDefault "access plus 1 year"
</FilesMatch>

Resources