Skip to content

Commit

Permalink
bugfix: rest path resolution (#3032)
Browse files Browse the repository at this point in the history
Co-authored-by: Sean O'Brien <obrien.sean.dev@gmail.com>
  • Loading branch information
stobrien89 and Sean O'Brien authored Nov 21, 2024
1 parent bceb5a6 commit 918e4ff
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 12 deletions.
7 changes: 7 additions & 0 deletions .changes/nextrelease/rest-serializer-fix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "bugfix",
"category": "Api",
"description": "Fixes issue with path resolution in rest protocol services"
}
]
8 changes: 8 additions & 0 deletions features/smoke/geoplaces.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# language: en
@smoke @geoplaces
Feature: Amazon Location Service Places V2

Scenario: Handling errors
When I attempt to call the "GetPlace" API with:
| PlaceId | foo |
Then I expect the response error code to be "ValidationException"
47 changes: 37 additions & 10 deletions src/Api/Serializer/RestSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Aws\Api\StructureShape;
use Aws\Api\TimestampShape;
use Aws\CommandInterface;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\EndpointV2\EndpointV2SerializerTrait;
use Aws\EndpointV2\Ruleset\RulesetEndpoint;
use GuzzleHttp\Psr7;
Expand Down Expand Up @@ -43,7 +42,6 @@ public function __construct(Service $api, $endpoint)

/**
* @param CommandInterface $command Command to serialize into a request.
* @param $endpointProvider Provider used for dynamic endpoint resolution.
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
*
* @return RequestInterface
Expand Down Expand Up @@ -198,6 +196,7 @@ private function applyQuery($name, Shape $member, $value, array &$opts)

private function buildEndpoint(Operation $operation, array $args, array $opts)
{
$isModifiedModel = $this->api->isModifiedModel();
// Create an associative array of variable definitions used in expansions
$varDefinitions = $this->getVarDefinitions($operation, $args);

Expand Down Expand Up @@ -226,11 +225,7 @@ function (array $matches) use ($varDefinitions) {

$path = $this->endpoint->getPath();

//Accounts for trailing '/' in path when custom endpoint
//is provided to endpointProviderV2
if ($this->api->isModifiedModel()
&& $this->api->getServiceName() === 's3'
) {
if ($isModifiedModel && $this->api->getServiceName() === 's3') {
if (substr($path, -1) === '/' && $relative[0] === '/') {
$path = rtrim($path, '/');
}
Expand All @@ -246,16 +241,19 @@ function (array $matches) use ($varDefinitions) {
return new Uri($this->endpoint->withPath('') . $relative);
}
}

if (!$isModifiedModel) {
$relative = $this->prependPath($relative, $path);
}

// If endpoint has path, remove leading '/' to preserve URI resolution.
if ($path && $relative[0] === '/') {
$relative = substr($relative, 1);
}

//Append path to endpoint when leading '//...'
// present as uri cannot be properly resolved
if ($this->api->isModifiedModel()
&& strpos($relative, '//') === 0
) {
if ($isModifiedModel && strpos($relative, '//') === 0) {
return new Uri($this->endpoint . $relative);
}

Expand Down Expand Up @@ -307,4 +305,33 @@ private function getVarDefinitions($command, $args)
}
return $varDefinitions;
}

/**
* If non-empty path with at least one segment present, compare
* with relative and prepend if starting segments are not duplicated
*
* @param string $relative
* @param string $path
*
* @return string
*/
private function prependPath(string $relative, string $path): string
{
if (empty($relative) || $relative === '/'
|| empty($path) || $path === '/'
) {
return $relative;
}

$normalizedPath = rtrim($path, '/');
$normalizedRelative = ltrim($relative, '/');

// Check if $relative starts with $path
if (strpos($normalizedRelative, ltrim($normalizedPath, '/')) === 0) {
// $relative already starts with $path, return $relative
return $relative;
}

return $normalizedPath . '/' . $normalizedRelative;
}
}
59 changes: 57 additions & 2 deletions tests/Api/Serializer/RestJsonSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ private function getTestService()
'boolHeader' => [
'http' => ['method' => 'POST'],
'input' => ['shape' => 'BoolHeaderInput']
],
'requestUriOperation' =>[
'http' => [
'method' => 'POST',
'requestUri' => 'foo/{PathSegment}'
],
'input' => ['shape' => 'RequestUriOperationInput'],
]
],
'shapes' => [
Expand All @@ -76,6 +83,17 @@ private function getTestService()
]
]
],
'RequestUriOperationInput' => [
'required' => ['PathSegment'],
'type' => 'structure',
'members' => [
"PathSegment" => [
"shape" => "PathSegmentShape",
"location" => 'uri'
],
'baz' => ['shape' => 'BazShape']
]
],
"DocumentType" => [
"type" => "structure",
"document" => true
Expand Down Expand Up @@ -139,6 +157,7 @@ private function getTestService()
'BlobShape' => ['type' => 'blob'],
'BazShape' => ['type' => 'string'],
'BoolShape' => ['type' => 'boolean'],
'PathSegmentShape' => ['type' => 'string'],
]
],
function () {}
Expand All @@ -153,11 +172,12 @@ private function getRequest($commandName, $input)
return $j($command);
}

private function getPathEndpointRequest($commandName, $input)
private function getPathEndpointRequest($commandName, $input, $options = [])
{
$service = $this->getTestService();
$command = new Command($commandName, $input);
$j = new RestJsonSerializer($service, 'http://foo.com/bar');
$path = $options['path'] ?? 'bar';
$j = new RestJsonSerializer($service, 'http://foo.com/' . $path);
return $j($command);
}

Expand Down Expand Up @@ -185,6 +205,41 @@ public function testPreparesRequestsWithEndpointWithPath()
);
}

public function testPreparesRequestsWithEndpointWithRequestUriAndPath(): void
{
$request = $this->getPathEndpointRequest(
'requestUriOperation',
['PathSegment' => 'bar', 'baz' => 'bar']
);
$this->assertSame('POST', $request->getMethod());
$this->assertSame('http://foo.com/bar/foo/bar', (string) $request->getUri());
$this->assertSame('{"baz":"bar"}', (string) $request->getBody());
$this->assertSame(
'application/json',
$request->getHeaderLine('Content-Type')
);
}

/**
* Simulates a custom endpoint provided with a starting path segment matching the
* modeled `RequestUri` starting path segment
*/
public function testPreparesRequestsWithEndpointWithDuplicateRequestUriAndPath(): void
{
$request = $this->getPathEndpointRequest(
'requestUriOperation',
['PathSegment' => 'bar', 'baz' => 'bar'],
['path' => 'foo']
);
$this->assertSame('POST', $request->getMethod());
$this->assertSame('http://foo.com/foo/bar', (string) $request->getUri());
$this->assertSame('{"baz":"bar"}', (string) $request->getBody());
$this->assertSame(
'application/json',
$request->getHeaderLine('Content-Type')
);
}

public function testPreparesRequestsWithBlobButNoForcedContentType()
{
$request = $this->getRequest('bar', ['baz' => 'bar']);
Expand Down
6 changes: 6 additions & 0 deletions tests/EndpointV2/EndpointProviderV2Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,17 @@ public function testRulesetProtocolEndpointAndErrorCases($service, $clientArgs,
$list->appendSign(Middleware::tap(function($cmd, $req) use ($service, $expected) {
$expectedEndpoint = $expected['endpoint'];
$expectedUri = new Uri($expected['endpoint']['url']);
$expectedPath = $expectedUri->getPath();

$this->assertStringContainsString(
$expectedUri->getHost(),
$req->getUri()->getHost()
);

if (!empty($expectedPath)) {
$this->assertStringStartsWith($expectedPath, $req->getUri()->getPath());
}

if (isset($expectedEndpoint['properties']['authSchemes'])) {
$expectedAuthScheme = null;
foreach ($expectedEndpoint['properties']['authSchemes'] as $authScheme) {
Expand Down

0 comments on commit 918e4ff

Please sign in to comment.