diff --git a/src/Api/Serializer/AbstractRpcV2Serializer.php b/src/Api/Serializer/AbstractRpcV2Serializer.php index 4c2230cb23..6a6636ba2d 100644 --- a/src/Api/Serializer/AbstractRpcV2Serializer.php +++ b/src/Api/Serializer/AbstractRpcV2Serializer.php @@ -78,7 +78,7 @@ public function __invoke( // Content-Type must not be set if ($operation['input'] !== null) { $body = $this->serialize($operation->getInput(), $commandArgs); - $headers['Content-Length'] = strlen($body); + $headers['Content-Length'] = (string) strlen($body); } else { unset($headers['Content-Type']); } diff --git a/src/Api/Serializer/JsonRpcSerializer.php b/src/Api/Serializer/JsonRpcSerializer.php index a4f5e6f2e4..72f7333760 100644 --- a/src/Api/Serializer/JsonRpcSerializer.php +++ b/src/Api/Serializer/JsonRpcSerializer.php @@ -66,7 +66,7 @@ public function __invoke( $headers = [ 'X-Amz-Target' => $this->api->getMetadata('targetPrefix') . '.' . $operationName, 'Content-Type' => $this->contentType, - 'Content-Length' => strlen($body) + 'Content-Length' => (string) strlen($body) ]; if ($endpoint instanceof RulesetEndpoint) { diff --git a/src/Api/Serializer/QuerySerializer.php b/src/Api/Serializer/QuerySerializer.php index c38c881818..8df75a12bb 100644 --- a/src/Api/Serializer/QuerySerializer.php +++ b/src/Api/Serializer/QuerySerializer.php @@ -61,7 +61,7 @@ public function __invoke( } $body = http_build_query($body, '', '&', PHP_QUERY_RFC3986); $headers = [ - 'Content-Length' => strlen($body), + 'Content-Length' => (string) strlen($body), 'Content-Type' => 'application/x-www-form-urlencoded' ]; $requestUri = $operation['http']['requestUri'] ?? null; diff --git a/src/Api/Serializer/RestJsonSerializer.php b/src/Api/Serializer/RestJsonSerializer.php index e198664c64..a4c5d72f64 100644 --- a/src/Api/Serializer/RestJsonSerializer.php +++ b/src/Api/Serializer/RestJsonSerializer.php @@ -35,7 +35,7 @@ protected function payload(StructureShape $member, array|string $value, array &$ { $opts['headers']['Content-Type'] = $this->contentType; $body = $this->jsonFormatter->build($member, $value); - $opts['headers']['Content-Length'] = strlen($body); + $opts['headers']['Content-Length'] = (string) strlen($body); $opts['body'] = $body; } } diff --git a/src/Api/Serializer/RestSerializer.php b/src/Api/Serializer/RestSerializer.php index 520020856d..9c186946ce 100644 --- a/src/Api/Serializer/RestSerializer.php +++ b/src/Api/Serializer/RestSerializer.php @@ -159,7 +159,7 @@ private function applyPayload(StructureShape $input, $name, array $args, array & $body = $args[$name]; if (!$m['streaming'] && is_string($body)) { - $opts['headers']['Content-Length'] = strlen($body); + $opts['headers']['Content-Length'] = (string) strlen($body); } // Streaming bodies or payloads that are strings are @@ -173,20 +173,36 @@ private function applyPayload(StructureShape $input, $name, array $args, array & private function applyHeader($name, Shape $member, $value, array &$opts) { - // Handle lists by recursively applying header logic to each element + if ($value === null) { + return; + } + + // Handle lists by applying header logic to each element if ($member instanceof ListShape) { + if (!is_array($value)) { + throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.'); + } + $listMember = $member->getMember(); $headerValues = []; foreach ($value as $listValue) { + if ($listValue === null) { + throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.'); + } + $tempOpts = ['headers' => []]; $this->applyHeader('temp', $listMember, $listValue, $tempOpts); + if (!array_key_exists('temp', $tempOpts['headers'])) { + throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.'); + } + $convertedValue = $tempOpts['headers']['temp']; $headerValues[] = $convertedValue; } $value = $headerValues; - } elseif (!is_null($value)) { + } else { switch ($member->getType()) { case 'timestamp': $timestampFormat = $member['timestampFormat'] ?? 'rfc822'; @@ -208,7 +224,7 @@ private function applyHeader($name, Shape $member, $value, array &$opts) $value = base64_encode($value); } - $opts['headers'][$member['locationName'] ?: $name] = $value; + $opts['headers'][$member['locationName'] ?: $name] = self::prepareHeaderValue($value); } /** @@ -218,10 +234,42 @@ private function applyHeaderMap($name, Shape $member, array $value, array &$opts { $prefix = $member['locationName']; foreach ($value as $k => $v) { - $opts['headers'][$prefix . $k] = $v; + if ($v === null) { + continue; + } + + $opts['headers'][$prefix . $k] = self::prepareHeaderValue($v); } } + /** + * @return string|string[] + */ + private static function prepareHeaderValue($value) + { + if (is_scalar($value)) { + return (string) $value; + } + + if (is_array($value)) { + if ($value === []) { + return ''; + } + + foreach ($value as $key => $item) { + if (!is_scalar($item)) { + throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.'); + } + + $value[$key] = (string) $item; + } + + return $value; + } + + throw new \InvalidArgumentException('Header values must be scalar or an array of scalars.'); + } + private function applyQuery($name, Shape $member, $value, array &$opts) { if ($member instanceof MapShape) { diff --git a/src/Api/Serializer/RestXmlSerializer.php b/src/Api/Serializer/RestXmlSerializer.php index 2cf496a4b9..493dd0f435 100644 --- a/src/Api/Serializer/RestXmlSerializer.php +++ b/src/Api/Serializer/RestXmlSerializer.php @@ -30,7 +30,7 @@ protected function payload(StructureShape $member, array $value, array &$opts) { $opts['headers']['Content-Type'] = 'application/xml'; $body = $this->getXmlBody($member, $value); - $opts['headers']['Content-Length'] = strlen($body); + $opts['headers']['Content-Length'] = (string) strlen($body); $opts['body'] = $body; } diff --git a/src/CloudSearchDomain/CloudSearchDomainClient.php b/src/CloudSearchDomain/CloudSearchDomainClient.php index 8b01a2a13c..b8f24550fc 100644 --- a/src/CloudSearchDomain/CloudSearchDomainClient.php +++ b/src/CloudSearchDomain/CloudSearchDomainClient.php @@ -78,7 +78,7 @@ public static function convertGetToPost(RequestInterface $r) $query = $r->getUri()->getQuery(); $req = $r->withMethod('POST') ->withBody(Psr7\Utils::streamFor($query)) - ->withHeader('Content-Length', strlen($query)) + ->withHeader('Content-Length', (string) strlen($query)) ->withHeader('Content-Type', 'application/x-www-form-urlencoded') ->withUri($r->getUri()->withQuery('')); return $req; diff --git a/src/StreamRequestPayloadMiddleware.php b/src/StreamRequestPayloadMiddleware.php index 3cad8b2003..38fccd53c3 100644 --- a/src/StreamRequestPayloadMiddleware.php +++ b/src/StreamRequestPayloadMiddleware.php @@ -74,7 +74,7 @@ public function __invoke(CommandInterface $command, RequestInterface $request) } $request = $request->withHeader( 'Content-Length', - $size + (string) $size ); } } diff --git a/tests/Api/Serializer/JsonRpcSerializerTest.php b/tests/Api/Serializer/JsonRpcSerializerTest.php index 373f835a39..2375b66716 100644 --- a/tests/Api/Serializer/JsonRpcSerializerTest.php +++ b/tests/Api/Serializer/JsonRpcSerializerTest.php @@ -49,6 +49,7 @@ function () {} $request->getHeaderLine('Content-Type') ); $this->assertSame('test.foo', $request->getHeaderLine('X-Amz-Target')); + $this->assertSame(['13'], $request->getHeader('Content-Length')); $this->assertSame('{"baz":"bam"}', (string) $request->getBody()); } diff --git a/tests/Api/Serializer/QuerySerializerTest.php b/tests/Api/Serializer/QuerySerializerTest.php index 176355d5ee..5640a89281 100644 --- a/tests/Api/Serializer/QuerySerializerTest.php +++ b/tests/Api/Serializer/QuerySerializerTest.php @@ -47,6 +47,7 @@ function () {} $request = $q($cmd); $this->assertSame('POST', $request->getMethod()); $this->assertSame('http://foo.com/', (string) $request->getUri()); + $this->assertSame(['25'], $request->getHeader('Content-Length')); $this->assertSame('Action=foo&Version=1&baz=', (string) $request->getBody()); } diff --git a/tests/Api/Serializer/RestJsonSerializerTest.php b/tests/Api/Serializer/RestJsonSerializerTest.php index 15be71b162..3f8f8dbe25 100644 --- a/tests/Api/Serializer/RestJsonSerializerTest.php +++ b/tests/Api/Serializer/RestJsonSerializerTest.php @@ -63,6 +63,18 @@ private function getTestService(): Service 'http' => ['method' => 'POST'], 'input' => ['shape' => 'BoolHeaderInput'] ], + 'intHeader' => [ + 'http' => ['method' => 'POST'], + 'input' => ['shape' => 'IntHeaderInput'] + ], + 'nullHeader' => [ + 'http' => ['method' => 'POST'], + 'input' => ['shape' => 'NullHeaderInput'] + ], + 'listHeader' => [ + 'http' => ['method' => 'POST'], + 'input' => ['shape' => 'ListHeaderInput'] + ], 'requestUriOperation' =>[ 'http' => [ 'method' => 'POST', @@ -182,9 +194,44 @@ private function getTestService(): Service ], ] ], + 'IntHeaderInput' => [ + 'type' => 'structure', + 'members' => [ + 'int' => [ + 'shape' => 'IntShape', + 'location' => 'header', + 'locationName' => 'Is-Int', + ], + ] + ], + 'NullHeaderInput' => [ + 'type' => 'structure', + 'members' => [ + 'null' => [ + 'shape' => 'BazShape', + 'location' => 'header', + 'locationName' => 'Is-Null', + ], + ] + ], + 'ListHeaderInput' => [ + 'type' => 'structure', + 'members' => [ + 'list' => [ + 'shape' => 'ListShape', + 'location' => 'header', + 'locationName' => 'Is-List', + ], + ] + ], 'BlobShape' => ['type' => 'blob'], 'BazShape' => ['type' => 'string'], 'BoolShape' => ['type' => 'boolean'], + 'IntShape' => ['type' => 'integer'], + 'ListShape' => [ + 'type' => 'list', + 'member' => ['shape' => 'BazShape'], + ], 'PathSegmentShape' => ['type' => 'string'], ] ], @@ -223,6 +270,7 @@ public function testPreparesRequestsWithContentType(): void 'application/json', $request->getHeaderLine('Content-Type') ); + $this->assertSame(['13'], $request->getHeader('Content-Length')); } public function testPreparesRequestsWithEndpointWithPath(): void @@ -409,6 +457,38 @@ public static function boolProvider(): iterable ]; } + public function testSerializesIntegerHeaderValueToString(): void + { + $request = $this->getRequest('intHeader', ['int' => 5]); + $this->assertSame(['5'], $request->getHeader('Is-Int')); + } + + public function testOmitsNullHeaderValue(): void + { + $request = $this->getRequest('nullHeader', ['null' => null]); + $this->assertFalse($request->hasHeader('Is-Null')); + } + + public function testSerializesEmptyHeaderListToEmptyString(): void + { + $request = $this->getRequest('listHeader', ['list' => []]); + $this->assertSame([''], $request->getHeader('Is-List')); + } + + public function testSerializesHeaderListValuesToStrings(): void + { + $request = $this->getRequest('listHeader', ['list' => ['foo', 5, true, false]]); + $this->assertSame(['foo', '5', '1', ''], $request->getHeader('Is-List')); + } + + public function testRejectsInvalidHeaderListValue(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Header values must be scalar or an array of scalars.'); + + $this->getRequest('listHeader', ['list' => ['foo', null]]); + } + public function testDoesNotOverrideScheme(): void { $serializer = new RestJsonSerializer($this->getTestService(), 'http://foo.com'); diff --git a/tests/Api/Serializer/RestXmlSerializerTest.php b/tests/Api/Serializer/RestXmlSerializerTest.php index 6bd3dde2b6..95f6769e5b 100644 --- a/tests/Api/Serializer/RestXmlSerializerTest.php +++ b/tests/Api/Serializer/RestXmlSerializerTest.php @@ -80,6 +80,7 @@ public function testPreparesRequestsWithStructurePayloadXmlContentType() 'application/xml', $request->getHeaderLine('Content-Type') ); + $this->assertSame([(string) strlen((string) $request->getBody())], $request->getHeader('Content-Length')); } /** diff --git a/tests/Api/Serializer/RpcV2CborSerializerTest.php b/tests/Api/Serializer/RpcV2CborSerializerTest.php index c13566ab2d..f218a5ae89 100644 --- a/tests/Api/Serializer/RpcV2CborSerializerTest.php +++ b/tests/Api/Serializer/RpcV2CborSerializerTest.php @@ -561,6 +561,6 @@ public function testSetsCborHeaders(): void 'rpc-v2-cbor', $request->getHeaderLine('Smithy-Protocol') ); - $this->assertTrue($request->hasHeader('Content-Length')); + $this->assertSame([(string) strlen((string) $request->getBody())], $request->getHeader('Content-Length')); } } diff --git a/tests/CloudSearchDomain/CloudSearchDomainTest.php b/tests/CloudSearchDomain/CloudSearchDomainTest.php index 37593f82e8..37f1366865 100644 --- a/tests/CloudSearchDomain/CloudSearchDomainTest.php +++ b/tests/CloudSearchDomain/CloudSearchDomainTest.php @@ -40,6 +40,7 @@ public function testConvertGetToPost() $request = CloudSearchDomainClient::convertGetToPost($request); $this->assertSame('POST', $request->getMethod()); $this->assertSame('application/x-www-form-urlencoded', $request->getHeaderLine('Content-Type')); + $this->assertSame(['7'], $request->getHeader('Content-Length')); $this->assertSame('7', $request->getHeaderLine('Content-Length')); $this->assertSame('foo=bar', (string)$request->getBody()); $this->assertSame('', $request->getUri()->getQuery()); diff --git a/tests/StreamRequestPayloadMiddlewareTest.php b/tests/StreamRequestPayloadMiddlewareTest.php index 295e780444..a2c53cbae5 100644 --- a/tests/StreamRequestPayloadMiddlewareTest.php +++ b/tests/StreamRequestPayloadMiddlewareTest.php @@ -76,7 +76,7 @@ public static function addsProperHeadersDataProvider(): array 'InputStream' => $inputStream, ] ], - [ 'Content-Length' => [26] ], + [ 'Content-Length' => ['26'] ], [ 'transfer-encoding' ], ], [ @@ -86,7 +86,7 @@ public static function addsProperHeadersDataProvider(): array 'InputStream' => $inputStream, ] ], - [ 'Content-Length' => [26] ], + [ 'Content-Length' => ['26'] ], [ 'transfer-encoding' ], ], [ @@ -96,7 +96,7 @@ public static function addsProperHeadersDataProvider(): array 'InputStream' => $inputStream, ] ], - [ 'Content-Length' => [26] ], + [ 'Content-Length' => ['26'] ], [ 'transfer-encoding' ], ], ];