Skip to content

Commit

Permalink
nginx: Clear Last-Modified if ETag is from store
Browse files Browse the repository at this point in the history
This is what I've suspected a while ago[1]:

> Heads-up everyone: After testing this in a few production instances,
> it seems that some browsers still get cache hits for new store paths
> (and changed contents) for some reason. I highly suspect that it might
> be due to the last-modified header (as mentioned in [2]).
>
> Going to test this with last-modified disabled for a little while and
> if this is the case I think we should improve that patch by disabling
> last-modified if serving from a store path.

Much earlier[2] when I reviewed the patch, I wrote this:

> Other than that, it looks good to me.
>
> However, I'm not sure what we should do with Last-Modified header.
> From RFC 2616, section 13.3.4:
>
> - If both an entity tag and a Last-Modified value have been
>   provided by the origin server, SHOULD use both validators in
>   cache-conditional requests. This allows both HTTP/1.0 and
>   HTTP/1.1 caches to respond appropriately.
>
> I'm a bit nervous about the SHOULD here, as user agents in the wild
> could possibly just use Last-Modified and use the cached content
> instead.

Unfortunately, I didn't pursue this any further back then because
@pbogdan noted[3] the following:

> Hmm, could they (assuming they are conforming):
>
>  * If an entity tag has been provided by the origin server, MUST
>    use that entity tag in any cache-conditional request (using If-
>    Match or If-None-Match).

Since running with this patch in some deployments, I found that both
Firefox and Chrome/Chromium do NOT re-validate against the ETag if the
Last-Modified header is still the same.

So I wrote a small NixOS VM test with Geckodriver to have a test case
which is closer to the real world and I indeed was able to reproduce
this.

Whether this is actually a bug in Chrome or Firefox is an entirely
different issue and even IF it is the fault of the browsers and it is
fixed at some point, we'd still need to handle this for older browser
versions.

Apart from clearing the header, I also recreated the patch by using a
plain "git diff" with a small description on top. This should make it
easier for future authors to work on that patch.

[1]: #48337 (comment)
[2]: #48337 (comment)
[3]: #48337 (comment)

Signed-off-by: aszlig <aszlig@nix.build>
  • Loading branch information
aszlig committed Dec 30, 2019
1 parent 646c641 commit ccf55be
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 13 deletions.
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ in
nfs4 = handleTest ./nfs { version = 4; };
nghttpx = handleTest ./nghttpx.nix {};
nginx = handleTest ./nginx.nix {};
nginx-etag = handleTest ./nginx-etag.nix {};
nginx-sso = handleTest ./nginx-sso.nix {};
nix-ssh-serve = handleTest ./nix-ssh-serve.nix {};
nixos-generate-config = handleTest ./nixos-generate-config.nix {};
Expand Down
89 changes: 89 additions & 0 deletions nixos/tests/nginx-etag.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import ./make-test-python.nix {
name = "nginx-etag";

nodes = {
server = { pkgs, lib, ... }: {
networking.firewall.enable = false;
services.nginx.enable = true;
services.nginx.virtualHosts.server = {
root = pkgs.runCommandLocal "testdir" {} ''
mkdir "$out"
cat > "$out/test.js" <<EOF
document.getElementById('foobar').setAttribute('foo', 'bar');
EOF
cat > "$out/index.html" <<EOF
<!DOCTYPE html>
<div id="foobar">test</div>
<script src="test.js"></script>
EOF
'';
};

nesting.clone = lib.singleton {
services.nginx.virtualHosts.server = {
root = lib.mkForce (pkgs.runCommandLocal "testdir2" {} ''
mkdir "$out"
cat > "$out/test.js" <<EOF
document.getElementById('foobar').setAttribute('foo', 'yay');
EOF
cat > "$out/index.html" <<EOF
<!DOCTYPE html>
<div id="foobar">test</div>
<script src="test.js"></script>
EOF
'');
};
};
};

client = { pkgs, lib, ... }: {
virtualisation.memorySize = 512;
environment.systemPackages = let
testRunner = pkgs.writers.writePython3Bin "test-runner" {
libraries = [ pkgs.python3Packages.selenium ];
} ''
import os
import time
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
options = Options()
options.add_argument('--headless')
driver = Firefox(options=options)
driver.implicitly_wait(20)
driver.get('http://server/')
driver.find_element_by_xpath('//div[@foo="bar"]')
open('/tmp/passed_stage1', 'w')
while not os.path.exists('/tmp/proceed'):
time.sleep(0.5)
driver.get('http://server/')
driver.find_element_by_xpath('//div[@foo="yay"]')
open('/tmp/passed', 'w')
'';
in [ pkgs.firefox-unwrapped pkgs.geckodriver testRunner ];
};
};

testScript = { nodes, ... }: let
inherit (nodes.server.config.system.build) toplevel;
newSystem = "${toplevel}/fine-tune/child-1";
in ''
start_all()
server.wait_for_unit("nginx.service")
client.wait_for_unit("multi-user.target")
client.execute("test-runner &")
client.wait_for_file("/tmp/passed_stage1")
server.succeed(
"${newSystem}/bin/switch-to-configuration test >&2"
)
client.succeed("touch /tmp/proceed")
client.wait_for_file("/tmp/passed")
'';
}
18 changes: 5 additions & 13 deletions pkgs/servers/http/nginx/nix-etag-1.15.4.patch
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
From f6a978f024d01202f954483423af1b2d5d5159a6 Mon Sep 17 00:00:00 2001
From: Yegor Timoshenko <yegortimoshenko@riseup.net>
Date: Fri, 28 Sep 2018 03:27:04 +0000
Subject: [PATCH] If root is in Nix store, set ETag to its path hash

---
src/http/ngx_http_core_module.c | 56 +++++++++++++++++++++++++++++----
1 file changed, 50 insertions(+), 6 deletions(-)
This patch makes it possible to serve static content from Nix store paths, by
using the hash of the store path for the ETag header.

diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index c57ec00c..b7992de2 100644
index cb49ef74..f88dc77c 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -1583,6 +1583,7 @@ ngx_http_set_etag(ngx_http_request_t *r)
Expand All @@ -19,7 +13,7 @@ index c57ec00c..b7992de2 100644

clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

@@ -1598,16 +1599,61 @@ ngx_http_set_etag(ngx_http_request_t *r)
@@ -1598,16 +1599,62 @@ ngx_http_set_etag(ngx_http_request_t *r)
etag->hash = 1;
ngx_str_set(&etag->key, "ETag");

Expand Down Expand Up @@ -68,6 +62,7 @@ index c57ec00c..b7992de2 100644
+ }
+
+ ngx_memcpy(etag->value.data, ptr1, etag->value.len);
+ ngx_http_clear_last_modified(r);
+ } else {
+ etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);
+
Expand All @@ -87,6 +82,3 @@ index c57ec00c..b7992de2 100644

r->headers_out.etag = etag;

--
2.19.0

0 comments on commit ccf55be

Please sign in to comment.