Skip to content

Commit 6a56b62

Browse files
authored
Merge pull request api-platform#3843 from julienfalque/fix-varnish-ban-header-length
Chunk headers to comply with Varnish max length
2 parents 6679793 + a9f4138 commit 6a56b62

File tree

2 files changed

+139
-2
lines changed

2 files changed

+139
-2
lines changed

src/HttpCache/VarnishPurger.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
*/
2525
final class VarnishPurger implements PurgerInterface
2626
{
27+
private const DEFAULT_VARNISH_MAX_HEADER_LENGTH = 8000;
28+
2729
private $clients;
30+
private $maxHeaderLength;
2831

2932
/**
3033
* @param ClientInterface[] $clients
3134
*/
32-
public function __construct(array $clients)
35+
public function __construct(array $clients, int $maxHeaderLength = self::DEFAULT_VARNISH_MAX_HEADER_LENGTH)
3336
{
3437
$this->clients = $clients;
38+
$this->maxHeaderLength = $maxHeaderLength;
3539
}
3640

3741
/**
@@ -48,10 +52,42 @@ public function purge(array $iris)
4852
return sprintf('(^|\,)%s($|\,)', preg_quote($iri));
4953
}, $iris);
5054

51-
$regex = \count($parts) > 1 ? sprintf('(%s)', implode(')|(', $parts)) : array_shift($parts);
55+
foreach ($this->chunkRegexParts($parts) as $regex) {
56+
$this->banRegex($regex);
57+
}
58+
}
5259

60+
private function banRegex(string $regex): void
61+
{
5362
foreach ($this->clients as $client) {
5463
$client->request('BAN', '', ['headers' => ['ApiPlatform-Ban-Regex' => $regex]]);
5564
}
5665
}
66+
67+
private function chunkRegexParts(array $parts): iterable
68+
{
69+
if (1 === \count($parts)) {
70+
yield $parts[0];
71+
72+
return;
73+
}
74+
75+
$concatenatedParts = sprintf('(%s)', implode(")\n(", $parts));
76+
77+
if (\strlen($concatenatedParts) <= $this->maxHeaderLength) {
78+
yield str_replace("\n", '|', $concatenatedParts);
79+
80+
return;
81+
}
82+
83+
$lastSeparator = strrpos(substr($concatenatedParts, 0, $this->maxHeaderLength + 1), "\n");
84+
85+
$chunk = substr($concatenatedParts, 0, $lastSeparator);
86+
87+
yield str_replace("\n", '|', $chunk);
88+
89+
$nextParts = \array_slice($parts, substr_count($chunk, "\n") + 1);
90+
91+
yield from $this->chunkRegexParts($nextParts);
92+
}
5793
}

tests/HttpCache/VarnishPurgerTest.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616
use ApiPlatform\Core\HttpCache\VarnishPurger;
1717
use ApiPlatform\Core\Tests\ProphecyTrait;
1818
use GuzzleHttp\ClientInterface;
19+
use GuzzleHttp\Promise\PromiseInterface;
1920
use GuzzleHttp\Psr7\Response;
21+
use LogicException;
2022
use PHPUnit\Framework\TestCase;
23+
use Psr\Http\Message\RequestInterface;
24+
use Psr\Http\Message\ResponseInterface;
2125

2226
/**
2327
* @author Kévin Dunglas <dunglas@gmail.com>
@@ -49,4 +53,101 @@ public function testEmptyTags()
4953
$purger = new VarnishPurger([$clientProphecy1->reveal()]);
5054
$purger->purge([]);
5155
}
56+
57+
/**
58+
* @dataProvider provideChunkHeaderCases
59+
*/
60+
public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $regexesToSend)
61+
{
62+
$client = new class() implements ClientInterface {
63+
public $sentRegexes = [];
64+
65+
public function send(RequestInterface $request, array $options = []): ResponseInterface
66+
{
67+
throw new LogicException('Not implemented');
68+
}
69+
70+
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
71+
{
72+
throw new LogicException('Not implemented');
73+
}
74+
75+
public function request($method, $uri, array $options = []): ResponseInterface
76+
{
77+
$this->sentRegexes[] = $options['headers']['ApiPlatform-Ban-Regex'];
78+
79+
return new Response();
80+
}
81+
82+
public function requestAsync($method, $uri, array $options = []): PromiseInterface
83+
{
84+
throw new LogicException('Not implemented');
85+
}
86+
87+
public function getConfig($option = null)
88+
{
89+
throw new LogicException('Not implemented');
90+
}
91+
};
92+
93+
$purger = new VarnishPurger([$client], $maxHeaderLength);
94+
$purger->purge($iris);
95+
96+
self::assertSame($regexesToSend, $client->sentRegexes);
97+
}
98+
99+
public function provideChunkHeaderCases()
100+
{
101+
yield 'few iris' => [
102+
50,
103+
['/foo', '/bar'],
104+
['((^|\,)/foo($|\,))|((^|\,)/bar($|\,))'],
105+
];
106+
107+
yield 'iris to generate a header with exactly the maximum length' => [
108+
56,
109+
['/foo', '/bar', '/baz'],
110+
['((^|\,)/foo($|\,))|((^|\,)/bar($|\,))|((^|\,)/baz($|\,))'],
111+
];
112+
113+
yield 'iris to generate a header with exactly the maximum length and a smaller one' => [
114+
37,
115+
['/foo', '/bar', '/baz'],
116+
[
117+
'((^|\,)/foo($|\,))|((^|\,)/bar($|\,))',
118+
'(^|\,)/baz($|\,)',
119+
],
120+
];
121+
122+
yield 'with last iri too long to be part of the same header' => [
123+
50,
124+
['/foo', '/bar', '/some-longer-tag'],
125+
[
126+
'((^|\,)/foo($|\,))|((^|\,)/bar($|\,))',
127+
'(^|\,)/some\-longer\-tag($|\,)',
128+
],
129+
];
130+
131+
yield 'iris to have five headers' => [
132+
50,
133+
['/foo/1', '/foo/2', '/foo/3', '/foo/4', '/foo/5', '/foo/6', '/foo/7', '/foo/8', '/foo/9', '/foo/10'],
134+
[
135+
'((^|\,)/foo/1($|\,))|((^|\,)/foo/2($|\,))',
136+
'((^|\,)/foo/3($|\,))|((^|\,)/foo/4($|\,))',
137+
'((^|\,)/foo/5($|\,))|((^|\,)/foo/6($|\,))',
138+
'((^|\,)/foo/7($|\,))|((^|\,)/foo/8($|\,))',
139+
'((^|\,)/foo/9($|\,))|((^|\,)/foo/10($|\,))',
140+
],
141+
];
142+
143+
yield 'with varnish default limit' => [
144+
8000,
145+
array_fill(0, 1000, '/foo'),
146+
[
147+
implode('|', array_fill(0, 421, '((^|\,)/foo($|\,))')),
148+
implode('|', array_fill(0, 421, '((^|\,)/foo($|\,))')),
149+
implode('|', array_fill(0, 158, '((^|\,)/foo($|\,))')),
150+
],
151+
];
152+
}
52153
}

0 commit comments

Comments
 (0)