Skip to content

Commit 213bac1

Browse files
Nyholmruudk
authored andcommitted
Dont add path to an url we already added a path to. (php-http#141)
# Conflicts: # CHANGELOG.md # src/Plugin/AddPathPlugin.php
1 parent 9c21b60 commit 213bac1

File tree

3 files changed

+117
-17
lines changed

3 files changed

+117
-17
lines changed

spec/Plugin/AddPathPluginSpec.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ function it_adds_path(
3737
$host->getPath()->shouldBeCalled()->willReturn('/api');
3838

3939
$request->getUri()->shouldBeCalled()->willReturn($uri);
40-
$request->withUri($uri)->shouldBeCalled()->willReturn($request);
40+
$request->withUri($uri)->shouldBeCalledTimes(1)->willReturn($request);
4141

42-
$uri->withPath('/api/users')->shouldBeCalled()->willReturn($uri);
42+
$uri->withPath('/api/users')->shouldBeCalledTimes(1)->willReturn($uri);
4343
$uri->getPath()->shouldBeCalled()->willReturn('/users');
4444

4545
$this->beConstructedWith($host);

src/Plugin/AddPathPlugin.php

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,6 @@ final class AddPathPlugin implements Plugin
1818
*/
1919
private $uri;
2020

21-
/**
22-
* Stores identifiers of the already altered requests.
23-
*
24-
* @var array
25-
*/
26-
private $alteredRequests = [];
27-
28-
/**
29-
* @param UriInterface $uri
30-
*/
3121
public function __construct(UriInterface $uri)
3222
{
3323
if ('' === $uri->getPath()) {
@@ -42,17 +32,42 @@ public function __construct(UriInterface $uri)
4232
}
4333

4434
/**
35+
* Adds a prefix in the beginning of the URL's path.
36+
*
37+
* The prefix is not added if that prefix is already on the URL's path. This will fail on the edge
38+
* case of the prefix being repeated, for example if `https://example.com/api/api/foo` is a valid
39+
* URL on the server and the configured prefix is `/api`.
40+
*
41+
* We looked at other solutions, but they are all much more complicated, while still having edge
42+
* cases:
43+
* - Doing an spl_object_hash on `$first` will lead to collisions over time because over time the
44+
* hash can collide.
45+
* - Have the PluginClient provide a magic header to identify the request chain and only apply
46+
* this plugin once.
47+
*
48+
* There are 2 reasons for the AddPathPlugin to be executed twice on the same request:
49+
* - A plugin can restart the chain by calling `$first`, e.g. redirect
50+
* - A plugin can call `$next` more than once, e.g. retry
51+
*
52+
* Depending on the scenario, the path should or should not be added. E.g. `$first` could
53+
* be called after a redirect response from the server. The server likely already has the
54+
* correct path.
55+
*
56+
* No solution fits all use cases. This implementation will work fine for the common use cases.
57+
* If you have a specific situation where this is not the right thing, you can build a custom plugin
58+
* that does exactly what you need.
59+
*
4560
* {@inheritdoc}
4661
*/
4762
public function handleRequest(RequestInterface $request, callable $next, callable $first)
4863
{
49-
$identifier = spl_object_hash((object) $first);
64+
$prepend = $this->uri->getPath();
65+
$path = $request->getUri()->getPath();
5066

51-
if (!array_key_exists($identifier, $this->alteredRequests)) {
67+
if (substr($path, 0, strlen($prepend)) !== $prepend) {
5268
$request = $request->withUri($request->getUri()
53-
->withPath($this->uri->getPath().$request->getUri()->getPath())
54-
);
55-
$this->alteredRequests[$identifier] = $identifier;
69+
->withPath($prepend.$path)
70+
);
5671
}
5772

5873
return $next($request);

tests/Plugin/AddPathPluginTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace tests\Http\Client\Common\Plugin;
4+
5+
use Http\Client\Common\Plugin;
6+
use Http\Client\Common\Plugin\AddPathPlugin;
7+
use Http\Client\Common\PluginClient;
8+
use Http\Client\HttpClient;
9+
use Nyholm\Psr7\Request;
10+
use Nyholm\Psr7\Response;
11+
use Nyholm\Psr7\Uri;
12+
use PHPUnit\Framework\TestCase;
13+
use Psr\Http\Message\RequestInterface;
14+
15+
class AddPathPluginTest extends TestCase
16+
{
17+
/**
18+
* @var Plugin
19+
*/
20+
private $plugin;
21+
22+
/**
23+
* @var callable empty
24+
*/
25+
private $first;
26+
27+
protected function setUp()
28+
{
29+
$this->first = function () {};
30+
$this->plugin = new AddPathPlugin(new Uri('/api'));
31+
}
32+
33+
public function testRewriteSameUrl()
34+
{
35+
$verify = function (RequestInterface $request) {
36+
$this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString());
37+
};
38+
39+
$request = new Request('GET', 'https://example.com/foo', ['Content-Type'=>'text/html']);
40+
$this->plugin->handleRequest($request, $verify, $this->first);
41+
42+
// Make a second call with the same $request object
43+
$this->plugin->handleRequest($request, $verify, $this->first);
44+
45+
// Make a new call with a new object but same URL
46+
$request = new Request('GET', 'https://example.com/foo', ['Content-Type'=>'text/plain']);
47+
$this->plugin->handleRequest($request, $verify, $this->first);
48+
}
49+
50+
public function testRewriteCallingThePluginTwice()
51+
{
52+
$request = new Request('GET', 'https://example.com/foo');
53+
$this->plugin->handleRequest($request, function (RequestInterface $request) {
54+
$this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString());
55+
56+
// Run the plugin again with the modified request
57+
$this->plugin->handleRequest($request, function (RequestInterface $request) {
58+
$this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString());
59+
}, $this->first);
60+
}, $this->first);
61+
}
62+
63+
public function testRewriteWithDifferentUrl()
64+
{
65+
$request = new Request('GET', 'https://example.com/foo');
66+
$this->plugin->handleRequest($request, function (RequestInterface $request) {
67+
$this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString());
68+
}, $this->first);
69+
70+
$request = new Request('GET', 'https://example.com/bar');
71+
$this->plugin->handleRequest($request, function (RequestInterface $request) {
72+
$this->assertEquals('https://example.com/api/bar', $request->getUri()->__toString());
73+
}, $this->first);
74+
}
75+
76+
public function testRewriteWhenPathIsIncluded()
77+
{
78+
$verify = function (RequestInterface $request) {
79+
$this->assertEquals('https://example.com/api/foo', $request->getUri()->__toString());
80+
};
81+
82+
$request = new Request('GET', 'https://example.com/api/foo');
83+
$this->plugin->handleRequest($request, $verify, $this->first);
84+
}
85+
}

0 commit comments

Comments
 (0)