Reference - Cache backends - file


App: regression/cache_file - Page: a_304_carries_the_etag_its_200_carries
PHP
a_304_carries_the_etag_its_200_carries.php
HTML
a_304_carries_the_etag_its_200_carries.pad
Result
<?php

// A 304 carries the ETag the 200 to the same request carries. The stable page, fetched at
// a fresh address: the miss goes out as it was made, the hit gzipped as the cache keeps it,
// with the tag of the gzip body ("...-gzip"), and asked again with that tag the answer is
// a 304 with the same one - it said the identity tag, which a cache holding the gzip copy
// may not take for the one it holds.

function tagOf ( $answer ) {

foreach (
$answer ['headers'] ?? [] as $name => $value )
if (
strtolower ( $name ) == 'etag' )
return
trim ( $value );

return
'';

}

$tagUrl = $padHost . 'regression/cache_file/?stable&fresh=' . bin2hex ( random_bytes ( 6 ) );
$tagMiss = padCurl ( $tagUrl );
$tagHit = padCurl ( $tagUrl );
$tagAsk = padCurl ( [ 'url' => $tagUrl, 'headers' => [ 'If-None-Match' => tagOf ( $tagHit ) ] ] );

$tagResult = 'miss: ' . ( str_ends_with ( tagOf ( $tagMiss ), '-gzip"' ) ? 'gzip tag' : 'plain tag' )
.
', hit: ' . ( str_ends_with ( tagOf ( $tagHit ), '-gzip"' ) ? 'gzip tag' : 'plain tag' )
.
', asked with it: ' . $tagAsk ['result'] . ' ' . ( tagOf ( $tagAsk ) === tagOf ( $tagHit ) ? 'the same tag' : 'another tag' );

?>
{meta cache=0}{$tagResult}


App: regression/cache_file - Page: a_public_answer_among_several_locales_varies_on_the_cookie
PHP
a_public_answer_among_several_locales_varies_on_the_cookie.php
HTML
a_public_answer_among_several_locales_varies_on_the_cookie.pad
Result
<?php

// A page a shared cache may keep, of an application with several $padLocales, says Vary:
// Cookie on its public answer as well as Accept-Language: a padLang cookie chooses the
// locale (lib/locale.php), and the answer to that visitor is private - but a shared cache
// that kept the guest's English answer, stored under the address and Accept-Language
// alone, handed it to the next visitor whose padLang=nl cookie chose Dutch, the server
// never asked (RFC 9111 4.1: only the headers Vary names are matched). Apache joins the
// Vary names with a comma, php -S with a comma and a blank: the blank is taken out before
// they are compared.

$varyAnswer = padCurl ( [ 'url' => $padHost . 'regression/cache_file/?proxied&padInclude&fresh=' . bin2hex ( random_bytes ( 6 ) ),
'headers' => [ 'Accept-Language' => 'en' ] ] );

$varyControl = '';
$varyList = '';

foreach (
$varyAnswer ['headers'] ?? [] as $varyName => $varyValue )
if (
strtolower ( $varyName ) == 'cache-control' )
$varyControl = $varyValue;
elseif (
strtolower ( $varyName ) == 'vary' )
$varyList = preg_replace ( '/\s*,\s*/', ',', $varyValue );

$varyResult = trim ( (string) ( $varyAnswer ['data'] ?? '' ) ) . ' '
. ( preg_match ( '/^(public|private)\b/', $varyControl, $varyMatch ) ? $varyMatch [1] : "'$varyControl'" )
.
', Vary: ' . $varyList;

?>
{meta cache=0}{$varyResult}


App: regression/cache_file - Page: a_public_answer_of_one_locale_varies_on_neither_the_language_nor_the_cookie
PHP
a_public_answer_of_one_locale_varies_on_neither_the_language_nor_the_cookie.php
HTML
a_public_answer_of_one_locale_varies_on_neither_the_language_nor_the_cookie.pad
Result
<?php

// A page a shared cache may keep, of an application with one locale in $padLocales: neither
// Accept-Language nor a padLang cookie can choose another, so the answer to a visitor with
// that cookie is public and Vary names neither - where Vary said Accept-Language, Cookie
// and a shared cache kept a copy per Cookie header, and the padLang visitor was answered
// private. The blank after Vary's commas is taken out, as Apache and php -S join them
// differently.

$varyAnswer = padCurl ( [ 'url' => $padHost . 'regression/cache_file/?proxiedone&padInclude&fresh=' . bin2hex ( random_bytes ( 6 ) ),
'headers' => [ 'Accept-Language' => 'en', 'Cookie' => 'padLang=en' ] ] );

$varyControl = '';
$varyList = '';

foreach (
$varyAnswer ['headers'] ?? [] as $varyName => $varyValue )
if (
strtolower ( $varyName ) == 'cache-control' )
$varyControl = $varyValue;
elseif (
strtolower ( $varyName ) == 'vary' )
$varyList = preg_replace ( '/\s*,\s*/', ',', $varyValue );

$varyResult = trim ( (string) ( $varyAnswer ['data'] ?? '' ) ) . ' '
. ( preg_match ( '/^(public|private)\b/', $varyControl, $varyMatch ) ? $varyMatch [1] : "'$varyControl'" )
.
', Vary: ' . $varyList;

?>
{meta cache=0}{$varyResult}


App: regression/cache_file - Page: a_push_in_a_nocache_part_is_made_on_every_fetch
PHP
a_push_in_a_nocache_part_is_made_on_every_fetch.php
HTML
a_push_in_a_nocache_part_is_made_on_every_fetch.pad
Result
<?php

// A {push} inside a {nocache} part of a page the cache would keep pushes what this fetch
// renders: nocachepush, fetched twice at a fresh address, prints a new part and a new push
// both times. The page was kept with its stack filled in - the part rendered anew on every
// hit, its push showed the first fetch's text for the entry's whole life.

$pushUrl = $padHost . 'regression/cache_file/?nocachepush&padInclude&fresh=' . bin2hex ( random_bytes ( 6 ) );

preg_match ( '/<b>(\w+)<\/b>.*\[(\w+)\]/s', padCurl ( $pushUrl ) ['data'], $pushOne );
preg_match ( '/<b>(\w+)<\/b>.*\[(\w+)\]/s', padCurl ( $pushUrl ) ['data'], $pushTwo );

$pushResult = 'part new: ' . ( ( $pushOne [2] ?? 1 ) !== ( $pushTwo [2] ?? 1 ) ? 'yes' : 'no' )
.
', push new: ' . ( ( $pushOne [1] ?? 1 ) !== ( $pushTwo [1] ?? 1 ) ? 'yes' : 'no' );

?>
{meta cache=0}{$pushResult}


App: regression/cache_file - Page: a_visitor_with_an_identity_is_never_answered_public
PHP
a_visitor_with_an_identity_is_never_answered_public.php
HTML
a_visitor_with_an_identity_is_never_answered_public.pad
Result
<?php

// A page a shared cache may keep ($padCacheProxyAge) is public to a guest only. Asked with
// a cookie of the visitor's own, or with an Authorization header, it is that visitor's -
// private, as the page cache already treats such a request: it went out public with an
// s-maxage, and a proxy kept "Hello Alice" for everyone after her (RFC 9111 3.5 lets a
// shared cache reuse an answer to an Authorization request that says public). So is the
// answer whose locale a padLang cookie chose: a shared cache keys it on the address and the
// Accept-Language alone, and handed the Dutch page to the next English visitor.

function proxyType ( $extra ) {

global
$padHost;

$answer = padCurl ( [ 'url' => $padHost . 'regression/cache_file/?proxied&padInclude&fresh=' . bin2hex ( random_bytes ( 6 ) ) ] + $extra );

$control = '';

foreach (
$answer ['headers'] ?? [] as $name => $value )
if (
strtolower ( $name ) == 'cache-control' )
$control = $value;

return
trim ( $answer ['data'] ) . ' ' . ( preg_match ( '/^(public|private)\b/', $control, $match ) ? $match [1] : "'$control'" );

}

$proxyResult = 'guest: ' . proxyType ( [] )
.
', cookie: ' . proxyType ( [ 'cookies' => [ 'viewer' => 'Alice' ] ] )
.
', authorization: ' . proxyType ( [ 'headers' => [ 'Authorization' => 'Basic ' . base64_encode ( 'Alice:secret' ) ] ] )
.
', language cookie: ' . proxyType ( [ 'cookies' => [ 'padLang' => 'nl' ], 'headers' => [ 'Accept-Language' => 'en' ] ] );

?>
{meta cache=0}{$proxyResult}


App: regression/cache_file - Page: an_exposing_page_says_vary_accept_on_every_fetch
PHP
an_exposing_page_says_vary_accept_on_every_fetch.php
HTML
an_exposing_page_says_vary_accept_on_every_fetch.pad
Result
<?php

// A page that exposes its data answers JSON to an Accept header that asks for it, so every
// answer of it says Vary: Accept - the second fetch too. The page cache kept it, and a hit,
// which runs no page PHP, said Vary: Accept-Encoding only: a browser holding the HTML
// answered a fetch of the page's JSON with it. Apache joins the Vary names with a comma,
// php -S with a comma and a blank: the blank is taken out before they are compared.

$exposedUrl = $padHost . 'regression/cache_file/?exposed&padInclude&fresh=' . bin2hex ( random_bytes ( 6 ) );

function
exposedVary ( $url ) {

foreach (
padCurl ( $url ) ['headers'] ?? [] as $name => $value )
if (
strtolower ( $name ) == 'vary' )
return
preg_replace ( '/\s*,\s*/', ',', $value );

return
'-';

}

$exposedResult = 'first: ' . exposedVary ( $exposedUrl ) . ', second: ' . exposedVary ( $exposedUrl );

?>
{meta cache=0}{$exposedResult}


App: regression/cache_file - Page: debug
PHP
debug.php
HTML
debug.pad
Result
<?php

// A page that shows this machine something it shows nobody else - a {debug} - is not
// stored: built for a local request it went into the cache, and every visitor after it
// was answered with the value {debug} keeps from them. The second fetch says it was
// forwarded, as a visitor's request through a proxy on this machine does. The address is
// new on every run: the visitor's own copy, rightly stored, would answer the next run's
// local fetch.

$url = $padHost . 'regression/cache_file/?debugged&padInclude&debug=' . padRandomString ();

$local = padCurl ( $url );
$remote = padCurl ( [ 'url' => $url, 'headers' => [ 'X-Forwarded-For' => '203.0.113.5' ] ] );

$debugResult = 'local sees it: ' . ( str_contains ( $local ['data'], 'for this machine only' ) ? 'yes' : 'no' )
.
', visitor sees it: ' . ( str_contains ( $remote ['data'], 'for this machine only' ) ? 'YES' : 'no' );

?>
{$debugResult}


App: regression/cache_file - Page: debugged
PHP
debugged.php
HTML
debugged.pad
Result
<?php

// The fixture of debug: a page with a {debug} in it, which shows its value to this
// machine's own requests only.

$debugValue = 'for this machine only';

?>
<p>page</p>{debug $debugValue}


App: regression/cache_file - Page: etaglist
PHP
etaglist.php
HTML
etaglist.pad
Result
<?php

// On a cache miss the 304 decision reads the whole If-None-Match list: the stable page,
// fetched at a fresh address each time so the cache never holds it, answers 304 to a list
// holding its tag second, and to *, and 200 to a tag it does not have. Only the first tag
// of the list was compared there - the cache's own path already read them all.

function etagAsk ( $tags ) {

global
$padHost;

$input = [ 'url' => $padHost . 'regression/cache_file/?stable&fresh=' . bin2hex ( random_bytes ( 6 ) ) ];

if (
$tags !== '' )
$input ['headers'] = [ 'If-None-Match' => $tags ];

return
padCurl ( $input );

}

$etagFirst = etagAsk ( '' );
$etagTag = '';

foreach (
$etagFirst ['headers'] ?? [] as $etagName => $etagValue )
if (
strtolower ( $etagName ) == 'etag' )
$etagTag = trim ( $etagValue );

$etagResult = 'tag: ' . ( $etagTag !== '' ? 'sent' : 'none' )
.
' | list: ' . etagAsk ( '"AAAAAAAAAAAAAAAAAAAAAA", ' . $etagTag ) ['result']
.
' | star: ' . etagAsk ( '*' ) ['result']
.
' | other: ' . etagAsk ( '"AAAAAAAAAAAAAAAAAAAAAA"' ) ['result'];

?>
{$etagResult}


App: regression/cache_file - Page: exposed
PHP
exposed.php
HTML
exposed.pad
Result
<?php

// A page that exposes its data (lib/expose.php): its answer depends on the Accept header.

$rows = [ [ 'n' => 1 ], [ 'n' => 2 ] ];
$padExpose = [ 'rows' ];

?>
{rows}{$n} {/rows}


App: regression/cache_file - Page: flush
PHP
flush.php
HTML
flush.pad
Result
<?php

// A flushed page goes without an ETag - {flush} gives the ETag up with the whole body it
// describes - where the headers that went out with the first part carried Etag: "", one
// tag for every flushed page of the application.

$flushAnswer = padCurl ( $padHost . 'regression/cache_file/?flushed&padInclude&flush=' . padRandomString () );

$flushResult = 'body: ' . trim ( $flushAnswer ['data'] ) . ', etag: '
. ( array_key_exists ( 'Etag', $flushAnswer ['headers'] ) || array_key_exists ( 'ETag', $flushAnswer ['headers'] ) ? 'sent' : 'none' );

?>
{!flushResult}


App: regression/cache_file - Page: flushed
PHP
HTML
flushed.pad
Result
<p>head</p>{flush}<p>rest</p>


App: regression/cache_file - Page: fragment
PHP
fragment.php
HTML
fragment.pad
Result
<?php

// A page whose PHP picks a response fragment - from an HX-Request header, which the page
// cache's key does not hold - is answered fresh for such a request and its fragment is
// never stored: the HTMX swap went into the cache under the page's address, and every
// browser after it got the bare fragment for the whole page; or the stored whole page
// went into the swap. Two runs in a row each see both answers right; the address is new
// on every run.

$url = $padHost . 'regression/cache_file/?fragmented&padInclude&fragment=' . padRandomString ();

$swap = padCurl ( [ 'url' => $url, 'headers' => [ 'HX-Request' => 'true' ] ] );
$page = padCurl ( $url );
$swap2 = padCurl ( [ 'url' => $url, 'headers' => [ 'HX-Request' => 'true' ] ] );

$fragmentResult = 'swap: ' . trim ( $swap ['data'] ) . ' | page: ' . trim ( $page ['data'] ) . ' | swap again: ' . trim ( $swap2 ['data'] );

?>
{!fragmentResult}


App: regression/cache_file - Page: fragmented
PHP
fragmented.php
HTML
fragmented.pad
Result
<?php

// The fixture of fragment: a page that answers its fragment alone to an HTMX request - the
// HX-Request header - and the whole page to everyone else, as lib/respond.php suggests.

if ( isset ( $_SERVER ['HTTP_HX_REQUEST'] ) )
$padFragmentOnly = 'part';

?>
<p>full</p>{fragment 'part'}<p>part</p>{/fragment}


App: regression/cache_file - Page: guarded
PHP
guarded.php
HTML
guarded.pad
Result
<?php

// A page under a directory guard stays out of the page cache: a hit is answered before
// any guard runs, so a cached copy would go to whoever asked, whatever the guard says. The
// guard here wants a header, and a request with it gets a fresh build each time while one
// without it is refused - it was given the copy the first request left in the cache.

$url = $padHost . 'regression/cache_file/?locked/probe&padInclude&guarded';

$keyOne = padCurl ( [ 'url' => $url, 'headers' => [ 'X-Key' => 'open' ] ] );
$keyTwo = padCurl ( [ 'url' => $url, 'headers' => [ 'X-Key' => 'open' ] ] );
$noKey = padCurl ( $url );

$guardedResult = 'built fresh: ' . ( $keyOne ['data'] !== $keyTwo ['data'] ? 'yes' : 'NO' )
.
', without the key: ' . $noKey ['result'];

?>
{$guardedResult}