Skip to content

Commit fc04c8e

Browse files
Nyholmdbu
authored andcommitted
Dont add path to an url we already added a path to. (#141)
1 parent 3feffd5 commit fc04c8e

File tree

4 files changed

+120
-15
lines changed

4 files changed

+120
-15
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Added interfaces for BatchClient, HttpClientRouter and HttpMethodsClient.
1111
(These interfaces use the `Interface` suffix to avoid name collisions.)
1212
- Added an interface for HttpClientPool and moved the abstract class to the HttpClientPool sub namespace.
13+
- AddPathPlugin: Do not add the prefix if the URL already has the same prefix.
1314

1415
### Removed
1516
- Deprecated option `debug_plugins` has been removed from `PluginClient`
@@ -18,7 +19,7 @@
1819

1920
### Changed
2021

21-
- [RetryPlugin] Renamed the configuration options for the exception retry callback from `decider` to `exception_decider`
22+
- RetryPlugin: Renamed the configuration options for the exception retry callback from `decider` to `exception_decider`
2223
and `delay` to `exception_delay`. The old names still work but are deprecated.
2324

2425
## 1.8.2 - 2018-12-14

spec/Plugin/AddPathPluginSpec.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ public 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

+31-12
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@ final class AddPathPlugin implements Plugin
2121
*/
2222
private $uri;
2323

24-
/**
25-
* Stores identifiers of the already altered requests.
26-
*
27-
* @var array
28-
*/
29-
private $alteredRequests = [];
30-
3124
public function __construct(UriInterface $uri)
3225
{
3326
if ('' === $uri->getPath()) {
@@ -42,16 +35,42 @@ public function __construct(UriInterface $uri)
4235
}
4336

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

51-
if (!array_key_exists($identifier, $this->alteredRequests)) {
52-
$prefixedUrl = $this->uri->getPath().$request->getUri()->getPath();
53-
$request = $request->withUri($request->getUri()->withPath($prefixedUrl));
54-
$this->alteredRequests[$identifier] = $identifier;
70+
if (substr($path, 0, strlen($prepend)) !== $prepend) {
71+
$request = $request->withUri($request->getUri()
72+
->withPath($prepend.$path)
73+
);
5574
}
5675

5776
return $next($request);

tests/Plugin/AddPathPluginTest.php

+85
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)