Server Push and Caching

Mark Nottingham <> Wed, 24 August 2016 04:56 UTC

Return-Path: <>
Received: from localhost (localhost []) by (Postfix) with ESMTP id A467612D0B6 for <>; Tue, 23 Aug 2016 21:56:03 -0700 (PDT)
X-Virus-Scanned: amavisd-new at
X-Spam-Flag: NO
X-Spam-Score: -7.469
X-Spam-Status: No, score=-7.469 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, HEADER_FROM_DIFFERENT_DOMAINS=0.001, RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RP_MATCHES_RCVD=-0.548, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001] autolearn=ham autolearn_force=no
Received: from ([]) by localhost ( []) (amavisd-new, port 10024) with ESMTP id qgnqPVjLwqOV for <>; Tue, 23 Aug 2016 21:56:01 -0700 (PDT)
Received: from ( []) (using TLSv1.2 with cipher DHE-RSA-AES128-SHA (128/128 bits)) (No client certificate requested) by (Postfix) with ESMTPS id D964112B01C for <>; Tue, 23 Aug 2016 21:56:01 -0700 (PDT)
Received: from lists by with local (Exim 4.80) (envelope-from <>) id 1bcQ9q-0004FT-WC for; Wed, 24 Aug 2016 04:51:23 +0000
Resent-Date: Wed, 24 Aug 2016 04:51:22 +0000
Resent-Message-Id: <>
Received: from ([]) by with esmtps (TLS1.2:DHE_RSA_AES_128_CBC_SHA1:128) (Exim 4.80) (envelope-from <>) id 1bcQ9k-0004EV-57 for; Wed, 24 Aug 2016 04:51:16 +0000
Received: from ([]) by with esmtps (TLS1.2:DHE_RSA_AES_256_CBC_SHA256:256) (Exim 4.80) (envelope-from <>) id 1bcQ9h-0001PX-PB for; Wed, 24 Aug 2016 04:51:15 +0000
Received: from [] (unknown []) (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) (No client certificate requested) by (Postfix) with ESMTPSA id D02BC22E256 for <>; Wed, 24 Aug 2016 00:50:49 -0400 (EDT)
From: Mark Nottingham <>
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: quoted-printable
Message-Id: <>
Date: Wed, 24 Aug 2016 14:50:47 +1000
To: HTTP Working Group <>
Mime-Version: 1.0 (Mac OS X Mail 9.3 \(3124\))
X-Mailer: Apple Mail (2.3124)
Received-SPF: pass client-ip=;;
X-W3C-Hub-Spam-Status: No, score=-8.5
X-W3C-Hub-Spam-Report: AWL=1.081, BAYES_00=-1.9, RCVD_IN_DNSWL_LOW=-0.7, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001, W3C_AA=-1, W3C_DB=-1, W3C_IRA=-1, W3C_IRR=-3, W3C_WL=-1
X-W3C-Scan-Sig: 1bcQ9h-0001PX-PB 7585309a64d12bc01054b4134f344e75
Subject: Server Push and Caching
Archived-At: <>
X-Mailing-List: <> archive/latest/32341
Precedence: list
List-Id: <>
List-Help: <>
List-Post: <>
List-Unsubscribe: <>

Exploring how Server Push interacts with HTTP caching below. Feedback welcome.

# Pushing Uncacheable Content

RFC7540, Section 8.2 says:

> Pushed responses that are not cacheable MUST NOT be stored by any HTTP cache. They MAY be made available to the application separately.

As a result, any pushed response that cannot be stored as per the rules in RFC7234, Section 3 <> cannot be stored by a receiving HTTP cache.

"Being made available to the application separately" could mean many things. It could be that a truly uncacheable response (e.g., with `Cache-Control: no-store`) would bypass the HTTP cache but then be stored by the application in anticipation of a future request, but this might lead to some surprising results for Web developers, because it's effectively specifying yet another kind of browser caching (see separate thread).

However, they might still be usable if a browser API for Server Push emerges. See <>.

Do we think that an uncacheable response should have any side effects on a client other than such a dedicated push-focused API (e.g., triggering an event)? See also thread on "Scope of Server Push."

# Pushing and Freshness

RFC7540, Section 8.2 says:

> Pushed responses are considered successfully validated on the origin server (e.g., if the "no-cache" cache response directive is present (RFC7234, Section 5.2.2)) while the stream identified by the promised stream ID is still open.

This implies that, while that stream is open, the pushed response can be used by the cache, even when it contains any (or all) of the following cache directives:

* max-age=0
* no-cache
* s-maxage=0 (for shared caches)

The underlying principle here is that while the response stream is still open, it's semantically equivalent to a "normal" response to a just-issued request; it would be senseless to require it to be immediately revalidated before handing it to the application for use. 

The cache can also store the response, but once the stream is closed, if that response is stale -- either because of the presence of one of the directives above, or some combination of `Expires`, `Age`, `Date`, and `Cache-Control`, it will need to be revalidated before use.

Interestingly, cache freshness (as defined in RFC7234, Section 4.2) is also related to the concept of "successful validation on the origin server":

> A response's age is the time that has passed since it was generated by, or successfully validated with, the origin server.

We could decide to read this to assume that the pushed response has `Age: 0` no matter what comes across the wire. 

So, in summary -- any pushed response can be passed on to the application through a Server Push API. If it's cacheable, it can be stored in the cache, and even if it's stale or it requires validation, it can be served from cache while the response stream is still open (with some wiggle room).

Note that HTTP does not put constraints on _how_ the application uses that response after it comes through the API or the cache; it might use it multiple times (e.g., an image might occur more than once on a page, or more than one downstream client might have made the request). It's just that this reuse isn't in the context of a HTTP cache's operation.

Does this agree with other people's understanding?

# Pushing and Invalidation

When a server wants to remove the contents of a client's cache for a given URL, but doesn't know what it's to be replaced with yet, it needs to invalidate. The only native HTTP mechanism for cache invalidation is described in RFC7234, Section 4.4:

> A cache MUST invalidate the effective Request URI (Section 5.5 of RFC7230) as well as the URI(s) in the Location and Content-Location response header fields (if present) when a non-error status code is received in response to an unsafe request method.

Since it is triggered by unsafe request methods (like POST), this can't be used in Server Push.

We _could_ use this loophole a bit further down:

> A cache MUST invalidate the effective request URI (Section 5.5 of RFC7230) when it receives a non-error response to a request with a method whose safety is unknown.

... by defining a method that is defined to have a method whose safety is unknown (since if it's defined, it either won't be pushable, or won't trigger invalidation). E.g.

:scheme: https
:path: /thing

However, doing that might cause problems with IANA, since we'd have to pick a safety value to register.

Another approach would be to push a 404 (Not Found) or 410 (Gone) to trigger invalidation. However, such a push would need to be uncacheable (e.g,. with `Cache-Control: no-store`) to assure that the error response wasn't returned; however, this falls afoul of HTTP/2's requirement that uncacheable responses not interact with the HTTP cache.

So, we've kind of built ourselves a nice trap here.

If invalidation is an important use case, we'll need to change one of these specifications, or invent a new protocol mechanism. Maybe a `CACHE_INVALIDATE` frame?

Mark Nottingham