J3 Issue ?
avatar waveywhite
waveywhite
19 Mar 2018

Steps to reproduce the issue

Try setting the expiry date to sometime in the future that isn't 15 minutes

$app = JFactory::getApplication();
$app->setHeader('Expires', gmdate('D, d M Y H:i:s', time() + 60) . ' GMT');

Optionally, also set the application to enable browser cache
$app->allowCache(true);

Expected result

We should get a HTTP header with Expires set to 1 minute in the future, somehow.

Actual result

If we don't set allowCache(true) then we get a date in 2005. This would be OK if allowCache allowed our value of Expires. Instead, it is hard-wired to set an expiry date of 15 minutes into the future (900s)

System information (as much as possible)

Joomla 3.8.6

Additional comments

I propose adding a function setExpiryPeriod to WebApplication which sets the number of seconds into the future the HTTP expiry field can be set.

avatar waveywhite waveywhite - open - 19 Mar 2018
avatar joomla-cms-bot joomla-cms-bot - change - 19 Mar 2018
Labels Added: ?
avatar joomla-cms-bot joomla-cms-bot - labeled - 19 Mar 2018
avatar mbabker
mbabker - comment - 19 Mar 2018

We should get a HTTP header with Expires set to 1 minute in the future, somehow.

Why one minute? At that point you might as well not have a cache directive.

This would be OK if allowCache allowed our value of Expires.

You can manually set an Expires header elsewhere, the code in the WebApplication setting the 15 minute header isn't passing the $replace flag to setHeader meaning if an Expires header is already set it won't be overwritten.

I propose adding a function setExpiryPeriod to WebApplication which sets the number of seconds into the future the HTTP expiry field can be set.

Honestly, I'd rather this be a DateTime object versus an arbitrary number of seconds. Otherwise the Expires header can and will change because the response is sent on a different second, I'd suggest we should be able to set a consistent expiry time and the best way to do that is with a date object. The arbitrary time() + 900 code works as a generic default but lacks flexibility.

avatar waveywhite waveywhite - change - 19 Mar 2018
Status New Closed
Closed_Date 0000-00-00 00:00:00 2018-03-19 13:30:58
Closed_By waveywhite
avatar waveywhite waveywhite - close - 19 Mar 2018
avatar waveywhite waveywhite - change - 19 Mar 2018
Status Closed New
Closed_Date 2018-03-19 13:30:58
Closed_By waveywhite
avatar waveywhite waveywhite - reopen - 19 Mar 2018
avatar franz-wohlkoenig franz-wohlkoenig - change - 19 Mar 2018
Status New Discussion
avatar franz-wohlkoenig franz-wohlkoenig - change - 19 Mar 2018
Category Libraries
avatar waveywhite
waveywhite - comment - 19 Mar 2018

One minute is just an example. In our case we have a large busy site with dynamic AJAX resources which we would like the browser to cache between page loads, so 2-5 minutes can make a huge difference.

I'll check out setting the expires header in this way, so I guess that this is not so much an issue as a feature request for a nice-to-have. In which case, having a DateTime sounds sensible and could be more usable than a count of seconds.

avatar mbabker
mbabker - comment - 19 Mar 2018

One minute is just an example. In our case we have a large busy site with dynamic AJAX resources which we would like the browser to cache between page loads, so 2-5 minutes can make a huge difference.

Ahh, sorry. Read that at first as you expected the framework to default to one minute. Apparently I need my Monday morning nap before commenting on anymore GitHub issues 🤣

avatar ggppdk
ggppdk - comment - 19 Mar 2018

Here is how to do it
[EDIT]

  • please test to confirm i made no mistake ... !
  • examine adding more stuff / logic to the 'Cache-Control' header

If you call

JResponse::allowCache(true);

The above will prevent 2 headers from being added by WebApplication::respond():

$this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);

// HTTP 1.0
$this->setHeader('Pragma', 'no-cache');

You still get the expires header added as you describe but that is ok, (see below)
since you can just use 'Cache-Control' + max-age to take priority over expires header (it is more flexible and the newer / modern way of doing it)

So the final code to use is

// Prevent Joomla WebApplication::respond(): from setting 'Cache-Control' and 'Pragma'
JFactory::getApplication()->allowCache(true);

// CONTROL INTERMEDIARY CACHES (PROXY, ETC)
// Instruct them not cache (potentially) private resource (here you can add more code to decide)
$cacheControl = JFactory::getUser()->id ? 'private' : 'public';

// Set MAX-AGE, to allow modern browsers to cache the page, despite expires header in the past
$cacheControl .= ', max-age=300';
JResponse::setHeader('Cache-Control', $cacheControl );

// Make sure no legacy proxies do any caching (just in case)
// or just do not add this at all, Cache-Control + max-age take priority
// or as you said you cannot override it (if so just skip this)
JResponse::setHeader('Expires', 'Fri, 01 Jan 1990 00:00:00 GMT');
avatar mbabker
mbabker - comment - 19 Mar 2018

Except use JFactory::getApplication()->setHeader() instead of the deprecated JResponse class 😉 (which just proxies to the application anyway, so you also save a couple CPU cycles in the process)

avatar ggppdk
ggppdk - comment - 19 Mar 2018

I do not say that

JFactory::getApplication()->allowCache(true);

should not take more parameters to allow more control over what it does, maybe it should be made more flexible

avatar mbabker
mbabker - comment - 19 Mar 2018

allowCache() is a poorly defined API endpoint and should be broken up. If you don't pass a param it acts as a getter, if you do pass a param it acts as a setter. Since it's toggling a boolean flag I wouldn't complicate that method anymore. If you want to add an endpoint that creates more control over the cache behavior feel free to propose it, but we shouldn't be adding an overly complex method to do all the things.

avatar waveywhite
waveywhite - comment - 20 Mar 2018

OK. It turns out the source of my woes is in fact \Joomla\CMS\Document\JsonDocument. It's overwriting the caching parameter in the application and I can't set it. It ignores the cache parameter passed in to render.

	public function render($cache = false, $params = array())
	{
		$app = \JFactory::getApplication();

		$app->allowCache(false);

		if ($this->_mime == 'application/json')
		{
			// Browser other than Internet Explorer < 10
			$app->setHeader('Content-Disposition', 'attachment; filename="' . $this->getName() . '.json"', true);
		}

		parent::render();

		return $this->getBuffer();
	}
avatar waveywhite
waveywhite - comment - 23 Mar 2018

So the problems are:

  • JsonDocument::render calls $app->allowCache(false), ignoring the cache parameter passed in
  • WebApplication::setCache is a poor interface and can't control how long the caching is for
  • The document and application classes are too closely coupled and it's not clear who is responsible for setting the caching policy. Is it the document or the application?

When is Joomla 4 being released? ;-)

avatar waveywhite
waveywhite - comment - 23 Mar 2018

I'm going to have to give up on solving this in my application, I need a fix to Joomla! I've been trying @ggppdk's suggestion but it doesn't work in this case.

I set Cache-Control to max-age=300 in my JSON view (document->type() is json), by using the Application api, as suggested by @mbabker

error_log in WebApplication::setHeader shows:

params = name=Cache-Control value=max-age=300 single= replace=1
headers =  [{"name":"Cache-Control","value":"max-age=300"}]

JsonDocument then sets $app->allowCache(false) as a hard-wired value

shows:

params = name=Pragma value=no-cache single= replace=
headers =  [
    {"name":"Cache-Control","value":"max-age=300"},{"name":"Content-Type","value":"text\\/plain; charset=utf-8"},
    {"name":"Expires","value":"Wed, 17 Aug 2005 00:00:00 GMT"},
    {"name":"Last-Modified","value":"Fri, 23 Mar 2018 10:01:10 GMT"},{"name":"Cache-Control","value":"no-store, no-cache, must-revalidate, post-check=0, pre-check=0"},
    {"name":"Pragma","value":"no-cache"}]

Resulting headers are:

Cache-Control: max-age=300, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Date: Fri, 23 Mar 2018 10:01:10 GMT
Expires: Wed, 17 Aug 2005 00:00:00 GMT
Last-Modified: Fri, 23 Mar 2018 10:01:10 GMT
Pragma: no-cache

So, Cache-Control header is treated as a multi-value header and the combination of the values disables caching.

Can we just take the $app->allCache(false) call out of JsonDocument::render()? It defaults to false in WebApplication, anyway.

-David.

avatar waveywhite waveywhite - change - 23 Mar 2018
Title
Browser cache hard-wired to 15 minutes in libraries/src/Application/WebApplication.php
Browser cache impossible to control for JSON output
avatar waveywhite waveywhite - edited - 23 Mar 2018
avatar ggppdk
ggppdk - comment - 23 Mar 2018

Yes my suggestion will not work in case of JSON document as you said
\Joomla\CMS\Document\JsonDocument.
has code inside render() that will force cache to off

	public function render($cache = false, $params = array())
	{
		$app = \JFactory::getApplication();

		$app->allowCache(false);
avatar mbabker
mbabker - comment - 23 Mar 2018

I don't see why it needs to be there (and if someone can give a valid reason then it can stay).

avatar ggppdk
ggppdk - comment - 23 Mar 2018

I think the idea of adding it there was to guarantee that JSON responses are not cached,

Removing it now, will cause a change in default behaviour that will break websites that have code / configuration to enable browser caching and do it without checking the document type but expect that JSON responses are not cached

Only by extending API we could keep "B/C" behaviour

function allowCache($allow, $force_types=array())

thus any call like
$app->allowCache(false);
aka called without specifying $force_types, cannot override the call that specified $force_types

the above is ugly ? probably yes, but i am talking about B/C and about breaking some websites ...

avatar mbabker
mbabker - comment - 23 Mar 2018

Adding method parameters comes with the technical cost of new B/C constraints for the future. Please don't do that. Even if it's temporary it's bad. And some will argue adding optional parameters to existing methods is a B/C break too (actually on this platform we've gone so far as to say adding methods to classes breaks B/C, don't get me started).

You do realize there is no place in core calling allowCache(true), right? Meaning the only way this breaks is if some extension is blindly calling it. Which, there probably is. Because plugins do such a terrible job at checking context that it's almost easier right now to rip out everything that's not supporting HTML responses than it is to add features or make changes for non-HTML responses.

avatar brianteeman brianteeman - change - 25 Mar 2018
Labels Added: J3 Issue
avatar brianteeman brianteeman - labeled - 25 Mar 2018
avatar brianteeman
brianteeman - comment - 25 Mar 2018

same as #19105 ?

avatar waveywhite
waveywhite - comment - 25 Mar 2018

Yes, this is the same.

avatar waveywhite
waveywhite - comment - 25 Mar 2018

I'm not keen on the additional parameter but it makes sense, I think, to remove the erroneous call in JsonDocument::render. I see the potential issue with plugins but it will pose a performance issue rather than a behaviour issue in that scenario, because the cache would then be disabled. If it was the other way round it would change the behaviour.

avatar ggppdk
ggppdk - comment - 25 Mar 2018

I was going to suggest this as a work around
and i see it is already mentioned in the other issue

For now, in my module I did an ugly workaround by just returning the headers and data and exiting prematurely. However, I would prefer to not skip the remaining Joomla stuff that would be happening, like some event triggers being handled by some plugins.

i mean as temporarily solution until the Joomla version that this addressed
call php header(...) directly and exit when your output is done ...

avatar mbabker
mbabker - comment - 25 Mar 2018

I really hate that workaround. I'd rather us fix the freakin' framework than keep saying "bypass and avoid it".

All saying "use this workaround" is doing is basically saying "m'eh, we'll fix this eventually, if you're lucky".

avatar joomla-cms-bot joomla-cms-bot - close - 12 Mar 2020
avatar joomla-cms-bot joomla-cms-bot - change - 12 Mar 2020
Status Discussion Closed
Closed_Date 0000-00-00 00:00:00 2020-03-12 20:40:01
Closed_By joomla-cms-bot
avatar jwaisner jwaisner - change - 12 Mar 2020
Status Closed Duplicate Report
Closed_By joomla-cms-bot jwaisner
avatar joomla-cms-bot
joomla-cms-bot - comment - 12 Mar 2020

Set to "closed" on behalf of @jwaisner by The JTracker Application at issues.joomla.org/joomla-cms/19941

avatar jwaisner
jwaisner - comment - 12 Mar 2020

Duplicate of #19105


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/19941.

Add a Comment

Login with GitHub to post a comment