Skip to content

Commit c8dfcc5

Browse files
tuupoladbu
authored andcommitted
Optionally enable caching of any request method (#24)
Allow configuring any valid request method as defined by RFC7230 to be cached https://tools.ietf.org/html/rfc7230#section-3.1.1 https://tools.ietf.org/html/rfc7230#section-3.2.6 Technically RFC7230 allows lowercase request methods but consensus was they might be too confusing for users. Include request body in cache key calculation
1 parent 9ea1714 commit c8dfcc5

File tree

3 files changed

+100
-5
lines changed

3 files changed

+100
-5
lines changed

Diff for: CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# Change Log
22

3+
## 1.3.0 - unreleased
4+
### Added
5+
6+
- New `methods` setting which allows to configure the request methods which can be cached.
37

48
## 1.2.0 - 2016-08-16
59

610
### Changed
711

8-
- The default value for `default_ttl` is changed from `null` to `0`.
12+
- The default value for `default_ttl` is changed from `null` to `0`.
913

1014
### Fixed
1115

Diff for: spec/CachePluginSpec.php

+79-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i
4141

4242
$request->getMethod()->willReturn('GET');
4343
$request->getUri()->willReturn('/');
44+
$request->getBody()->shouldBeCalled();
45+
4446
$response->getStatusCode()->willReturn(200);
4547
$response->getBody()->willReturn($stream);
4648
$response->getHeader('Cache-Control')->willReturn(array())->shouldBeCalled();
@@ -71,6 +73,8 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte
7173
{
7274
$request->getMethod()->willReturn('GET');
7375
$request->getUri()->willReturn('/');
76+
$request->getBody()->shouldBeCalled();
77+
7478
$response->getStatusCode()->willReturn(400);
7579
$response->getHeader('Cache-Control')->willReturn(array());
7680
$response->getHeader('Expires')->willReturn(array());
@@ -85,7 +89,7 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte
8589
$this->handleRequest($request, $next, function () {});
8690
}
8791

88-
function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response)
92+
function it_doesnt_store_post_requests_by_default(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response)
8993
{
9094
$request->getMethod()->willReturn('POST');
9195
$request->getUri()->willReturn('/');
@@ -97,6 +101,74 @@ function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemIn
97101
$this->handleRequest($request, $next, function () {});
98102
}
99103

104+
function it_stores_post_requests_when_allowed(
105+
CacheItemPoolInterface $pool,
106+
CacheItemInterface $item,
107+
RequestInterface $request,
108+
ResponseInterface $response,
109+
StreamFactory $streamFactory,
110+
StreamInterface $stream
111+
) {
112+
$this->beConstructedWith($pool, $streamFactory, [
113+
'default_ttl' => 60,
114+
'cache_lifetime' => 1000,
115+
'methods' => ['GET', 'HEAD', 'POST']
116+
]);
117+
118+
$httpBody = 'hello=world';
119+
$stream->__toString()->willReturn($httpBody);
120+
$stream->isSeekable()->willReturn(true);
121+
$stream->rewind()->shouldBeCalled();
122+
123+
$request->getMethod()->willReturn('POST');
124+
$request->getUri()->willReturn('/post');
125+
$request->getBody()->willReturn($stream);
126+
127+
$response->getStatusCode()->willReturn(200);
128+
$response->getBody()->willReturn($stream);
129+
$response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled();
130+
$response->getHeader('Expires')->willReturn([])->shouldBeCalled();
131+
$response->getHeader('ETag')->willReturn([])->shouldBeCalled();
132+
133+
$pool->getItem('e4311a9af932c603b400a54efab21b6d7dea7a90')->shouldBeCalled()->willReturn($item);
134+
$item->isHit()->willReturn(false);
135+
$item->expiresAfter(1060)->willReturn($item)->shouldBeCalled();
136+
137+
$item->set($this->getCacheItemMatcher([
138+
'response' => $response->getWrappedObject(),
139+
'body' => $httpBody,
140+
'expiresAt' => 0,
141+
'createdAt' => 0,
142+
'etag' => []
143+
]))->willReturn($item)->shouldBeCalled();
144+
145+
$pool->save(Argument::any())->shouldBeCalled();
146+
147+
$next = function (RequestInterface $request) use ($response) {
148+
return new FulfilledPromise($response->getWrappedObject());
149+
};
150+
151+
$this->handleRequest($request, $next, function () {});
152+
}
153+
154+
function it_does_not_allow_invalid_request_methods(
155+
CacheItemPoolInterface $pool,
156+
CacheItemInterface $item,
157+
RequestInterface $request,
158+
ResponseInterface $response,
159+
StreamFactory $streamFactory,
160+
StreamInterface $stream
161+
) {
162+
$this
163+
->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException")
164+
->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD', 'POST ']]]);
165+
$this
166+
->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException")
167+
->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD"', 'POST']]]);
168+
$this
169+
->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException")
170+
->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'head', 'POST']]]);
171+
}
100172

101173
function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
102174
{
@@ -107,6 +179,8 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI
107179

108180
$request->getMethod()->willReturn('GET');
109181
$request->getUri()->willReturn('/');
182+
$request->getBody()->shouldBeCalled();
183+
110184
$response->getStatusCode()->willReturn(200);
111185
$response->getBody()->willReturn($stream);
112186
$response->getHeader('Cache-Control')->willReturn(array('max-age=40'));
@@ -141,6 +215,7 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R
141215
$stream->__toString()->willReturn($httpBody);
142216
$stream->isSeekable()->willReturn(true);
143217
$stream->rewind()->shouldBeCalled();
218+
$request->getBody()->shouldBeCalled();
144219

145220
$request->getMethod()->willReturn('GET');
146221
$request->getUri()->willReturn('/');
@@ -176,6 +251,7 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool,
176251

177252
$request->getMethod()->willReturn('GET');
178253
$request->getUri()->willReturn('/');
254+
$request->getBody()->shouldBeCalled();
179255

180256
$request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request);
181257
$request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request);
@@ -205,6 +281,7 @@ function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInt
205281

206282
$request->getMethod()->willReturn('GET');
207283
$request->getUri()->willReturn('/');
284+
$request->getBody()->shouldBeCalled();
208285

209286
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
210287
$item->isHit()->willReturn(true);
@@ -233,6 +310,7 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca
233310

234311
$request->getMethod()->willReturn('GET');
235312
$request->getUri()->willReturn('/');
313+
$request->getBody()->shouldBeCalled();
236314

237315
$request->withHeader(Argument::any(), Argument::any())->willReturn($request);
238316
$request->withHeader(Argument::any(), Argument::any())->willReturn($request);

Diff for: src/CachePlugin.php

+16-3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ final class CachePlugin implements Plugin
4545
* @var int $cache_lifetime (seconds) To support serving a previous stale response when the server answers 304
4646
* we have to store the cache for a longer time than the server originally says it is valid for.
4747
* We store a cache item for $cache_lifetime + max age of the response.
48+
* @var array $methods list of request methods which can be cached.
4849
* }
4950
*/
5051
public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = [])
@@ -64,7 +65,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
6465
{
6566
$method = strtoupper($request->getMethod());
6667
// if the request not is cachable, move to $next
67-
if ($method !== 'GET' && $method !== 'HEAD') {
68+
if (!in_array($method, $this->config['methods'])) {
6869
return $next($request);
6970
}
7071

@@ -205,7 +206,6 @@ private function getCacheControlDirective(ResponseInterface $response, $name)
205206
$headers = $response->getHeader('Cache-Control');
206207
foreach ($headers as $header) {
207208
if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {
208-
209209
// return the value for $name if it exists
210210
if (isset($matches[1])) {
211211
return $matches[1];
@@ -225,7 +225,12 @@ private function getCacheControlDirective(ResponseInterface $response, $name)
225225
*/
226226
private function createCacheKey(RequestInterface $request)
227227
{
228-
return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri());
228+
$body = (string) $request->getBody();
229+
if (!empty($body)) {
230+
$body = ' '.$body;
231+
}
232+
233+
return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri().$body);
229234
}
230235

231236
/**
@@ -273,12 +278,20 @@ private function configureOptions(OptionsResolver $resolver)
273278
'default_ttl' => 0,
274279
'respect_cache_headers' => true,
275280
'hash_algo' => 'sha1',
281+
'methods' => ['GET', 'HEAD'],
276282
]);
277283

278284
$resolver->setAllowedTypes('cache_lifetime', ['int', 'null']);
279285
$resolver->setAllowedTypes('default_ttl', ['int', 'null']);
280286
$resolver->setAllowedTypes('respect_cache_headers', 'bool');
287+
$resolver->setAllowedTypes('methods', 'array');
281288
$resolver->setAllowedValues('hash_algo', hash_algos());
289+
$resolver->setAllowedValues('methods', function ($value) {
290+
/* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */
291+
$matches = preg_grep('/[^A-Z0-9!#$%&\'*\/+\-.^_`|~]/', $value);
292+
293+
return empty($matches);
294+
});
282295
}
283296

284297
/**

0 commit comments

Comments
 (0)