Skip to content

Commit

Permalink
Merge pull request #3843 from julienfalque/fix-varnish-ban-header-length
Browse files Browse the repository at this point in the history
Chunk headers to comply with Varnish max length
  • Loading branch information
soyuka committed Nov 24, 2020
2 parents 6679793 + a9f4138 commit 6a56b62
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 2 deletions.
40 changes: 38 additions & 2 deletions src/HttpCache/VarnishPurger.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@
*/
final class VarnishPurger implements PurgerInterface
{
private const DEFAULT_VARNISH_MAX_HEADER_LENGTH = 8000;

private $clients;
private $maxHeaderLength;

/**
* @param ClientInterface[] $clients
*/
public function __construct(array $clients)
public function __construct(array $clients, int $maxHeaderLength = self::DEFAULT_VARNISH_MAX_HEADER_LENGTH)
{
$this->clients = $clients;
$this->maxHeaderLength = $maxHeaderLength;
}

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

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

private function banRegex(string $regex): void
{
foreach ($this->clients as $client) {
$client->request('BAN', '', ['headers' => ['ApiPlatform-Ban-Regex' => $regex]]);
}
}

private function chunkRegexParts(array $parts): iterable
{
if (1 === \count($parts)) {
yield $parts[0];

return;
}

$concatenatedParts = sprintf('(%s)', implode(")\n(", $parts));

if (\strlen($concatenatedParts) <= $this->maxHeaderLength) {
yield str_replace("\n", '|', $concatenatedParts);

return;
}

$lastSeparator = strrpos(substr($concatenatedParts, 0, $this->maxHeaderLength + 1), "\n");

$chunk = substr($concatenatedParts, 0, $lastSeparator);

yield str_replace("\n", '|', $chunk);

$nextParts = \array_slice($parts, substr_count($chunk, "\n") + 1);

yield from $this->chunkRegexParts($nextParts);
}
}
101 changes: 101 additions & 0 deletions tests/HttpCache/VarnishPurgerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
use ApiPlatform\Core\HttpCache\VarnishPurger;
use ApiPlatform\Core\Tests\ProphecyTrait;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response;
use LogicException;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand Down Expand Up @@ -49,4 +53,101 @@ public function testEmptyTags()
$purger = new VarnishPurger([$clientProphecy1->reveal()]);
$purger->purge([]);
}

/**
* @dataProvider provideChunkHeaderCases
*/
public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $regexesToSend)
{
$client = new class() implements ClientInterface {
public $sentRegexes = [];

public function send(RequestInterface $request, array $options = []): ResponseInterface
{
throw new LogicException('Not implemented');
}

public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
{
throw new LogicException('Not implemented');
}

public function request($method, $uri, array $options = []): ResponseInterface
{
$this->sentRegexes[] = $options['headers']['ApiPlatform-Ban-Regex'];

return new Response();
}

public function requestAsync($method, $uri, array $options = []): PromiseInterface
{
throw new LogicException('Not implemented');
}

public function getConfig($option = null)
{
throw new LogicException('Not implemented');
}
};

$purger = new VarnishPurger([$client], $maxHeaderLength);
$purger->purge($iris);

self::assertSame($regexesToSend, $client->sentRegexes);
}

public function provideChunkHeaderCases()
{
yield 'few iris' => [
50,
['/foo', '/bar'],
['((^|\,)/foo($|\,))|((^|\,)/bar($|\,))'],
];

yield 'iris to generate a header with exactly the maximum length' => [
56,
['/foo', '/bar', '/baz'],
['((^|\,)/foo($|\,))|((^|\,)/bar($|\,))|((^|\,)/baz($|\,))'],
];

yield 'iris to generate a header with exactly the maximum length and a smaller one' => [
37,
['/foo', '/bar', '/baz'],
[
'((^|\,)/foo($|\,))|((^|\,)/bar($|\,))',
'(^|\,)/baz($|\,)',
],
];

yield 'with last iri too long to be part of the same header' => [
50,
['/foo', '/bar', '/some-longer-tag'],
[
'((^|\,)/foo($|\,))|((^|\,)/bar($|\,))',
'(^|\,)/some\-longer\-tag($|\,)',
],
];

yield 'iris to have five headers' => [
50,
['/foo/1', '/foo/2', '/foo/3', '/foo/4', '/foo/5', '/foo/6', '/foo/7', '/foo/8', '/foo/9', '/foo/10'],
[
'((^|\,)/foo/1($|\,))|((^|\,)/foo/2($|\,))',
'((^|\,)/foo/3($|\,))|((^|\,)/foo/4($|\,))',
'((^|\,)/foo/5($|\,))|((^|\,)/foo/6($|\,))',
'((^|\,)/foo/7($|\,))|((^|\,)/foo/8($|\,))',
'((^|\,)/foo/9($|\,))|((^|\,)/foo/10($|\,))',
],
];

yield 'with varnish default limit' => [
8000,
array_fill(0, 1000, '/foo'),
[
implode('|', array_fill(0, 421, '((^|\,)/foo($|\,))')),
implode('|', array_fill(0, 421, '((^|\,)/foo($|\,))')),
implode('|', array_fill(0, 158, '((^|\,)/foo($|\,))')),
],
];
}
}

0 comments on commit 6a56b62

Please sign in to comment.