Skip to content

Commit 54aab06

Browse files
committed
Add server-side APCu caching for guest API responses
1 parent 153276e commit 54aab06

3 files changed

Lines changed: 67 additions & 0 deletions

File tree

extend.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use App\Console\PruneHiddenContent;
1515
use App\Console\PurgeSuspendedUsers;
1616
use App\Middleware\ContentSecurityPolicy;
17+
use App\Middleware\GuestApiCacheHeader;
1718
use App\Middleware\NoCacheHeader;
1819
use App\ServiceProvider\ApcuCacheProvider;
1920
use App\ServiceProvider\ErrorLogProvider;
@@ -47,6 +48,7 @@
4748
(new Extend\ServiceProvider())->register(SessionServiceProvider::class),
4849
(new Extend\Middleware('forum'))->add(ContentSecurityPolicy::class),
4950
(new Extend\Middleware('forum'))->add(NoCacheHeader::class),
51+
(new Extend\Middleware('api'))->add(GuestApiCacheHeader::class),
5052
(new Extend\Formatter())
5153
->configure(function (Configurator $config) {
5254
// Emoticons used to be provided by flarum/emoji
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace App\Middleware;
4+
5+
use Flarum\Http\RequestUtil;
6+
use Illuminate\Contracts\Cache\Repository as Cache;
7+
use Laminas\Diactoros\Response\JsonResponse;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Server\MiddlewareInterface;
11+
use Psr\Http\Server\RequestHandlerInterface;
12+
13+
class GuestApiCacheHeader implements MiddlewareInterface
14+
{
15+
private const TTL = 60;
16+
private const PREFIX = 'api_';
17+
18+
public function __construct(private Cache $cache)
19+
{
20+
}
21+
22+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
23+
{
24+
if ($request->getMethod() !== 'GET' || !RequestUtil::getActor($request)->isGuest()) {
25+
return $handler->handle($request);
26+
}
27+
28+
$cacheKey = self::PREFIX . md5($request->getUri()->getPath() . '?' . $request->getUri()->getQuery());
29+
30+
/** @var ?string $cached */
31+
$cached = $this->cache->get($cacheKey);
32+
if ($cached !== null) {
33+
return new JsonResponse(
34+
json_decode($cached, true, 512, JSON_THROW_ON_ERROR),
35+
200,
36+
['content-type' => 'application/vnd.api+json', 'X-Cache' => 'HIT']
37+
);
38+
}
39+
40+
$response = $handler->handle($request);
41+
42+
if ($response->getStatusCode() === 200) {
43+
$this->cache->put($cacheKey, (string) $response->getBody(), self::TTL);
44+
}
45+
46+
return $response->withHeader('X-Cache', 'MISS');
47+
}
48+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
describe('Cache headers', () => {
2+
it('caches guest API responses', () => {
3+
cy.request('/api').should((response) => {
4+
expect(response.headers['x-cache']).to.equal('MISS')
5+
})
6+
7+
cy.request('/api').should((response) => {
8+
expect(response.headers['x-cache']).to.equal('HIT')
9+
})
10+
})
11+
12+
it('does not set cache header on forum HTML', () => {
13+
cy.request('/').should((response) => {
14+
expect(response.headers['cache-control']).to.contain('no-store')
15+
})
16+
})
17+
})

0 commit comments

Comments
 (0)