Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
Expand Down
2 changes: 1 addition & 1 deletion aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ protected function format_timestamp(
$timestampFormat = !empty($shape['timestampFormat'])
? $shape['timestampFormat']
: 'iso8601';
$query[$prefix] = TimestampShape::format($value, $timestampFormat);
$query[$prefix] = TimestampShape::formatAsString(
$value,
$timestampFormat
);
}

protected function format_boolean(Shape $shape, $value, $prefix, array &$query)
Expand Down
2 changes: 1 addition & 1 deletion aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
72 changes: 63 additions & 9 deletions aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,18 @@ 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
// always just a stream of data.
$opts['body'] = Psr7\Utils::streamFor($body);
$stream = Psr7\Utils::streamFor($body);
// User-owned resource which should be detached instead of closed
// during garbage-collection
if (is_resource($body)) {
$stream = \Aws\detach_on_close_stream($stream);
}
$opts['body'] = $stream;
return;
}

Expand All @@ -173,20 +179,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';
Expand All @@ -208,7 +230,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);
}

/**
Expand All @@ -218,8 +240,40 @@ 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)
Expand Down Expand Up @@ -461,17 +515,17 @@ private function getVarDefinitions(
}

/**
* @param DateTimeInterface|string|int $value
* @param DateTimeInterface|string|int|float $value
* @param string $timestampFormat
*
* @return string
*/
private function formatTimestamp(
DateTimeInterface|string|int $value,
DateTimeInterface|string|int|float $value,
string $timestampFormat
): string
{
return TimestampShape::format($value, $timestampFormat);
return TimestampShape::formatAsString($value, $timestampFormat);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
29 changes: 13 additions & 16 deletions aws/aws-sdk-php/src/Api/Serializer/RpcV2CborSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Aws\Api\Exception\RpcV2CborException;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\Api\TimestampShape;
use DateTimeInterface;

/**
Expand Down Expand Up @@ -88,11 +89,11 @@ protected function resolveBlob(mixed $value): array

/**
* Wraps timestamp values in order to be encoded properly into
* value tag 1.
* value tag 1. Sub-second precision is preserved.
*
* @param mixed $value
*
* @return string[]
* @return array
* @throws RpcV2CborException
*/
protected function resolveTimestamp(
Expand All @@ -103,22 +104,18 @@ protected function resolveTimestamp(
return ['__cbor_timestamp' => $value];
}

if ($value instanceof DateTimeInterface) {
// Preserve milliseconds
$micro = (int) $value->format('u');
$value = $value->getTimestamp() + $micro / 1e6;
} else {
$timestamp = strtotime($value);
if ($timestamp === false) {
throw new RpcV2CborException(
'Request serialization failed: Invalid date/time: ' . $value,
);
}

$value = $timestamp;
if (is_string($value) && strtotime($value) === false) {
throw new RpcV2CborException(
'Request serialization failed: Invalid date/time: ' . $value,
);
}

// Wrapper to differentiate timestamp values during encoding
return ['__cbor_timestamp' => $value];
return [
'__cbor_timestamp' => TimestampShape::format(
$value,
'unixTimestamp'
)
];
}
}
4 changes: 3 additions & 1 deletion aws/aws-sdk-php/src/Api/Serializer/XmlBody.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ private function add_timestamp(
$timestampFormat = !empty($shape['timestampFormat'])
? $shape['timestampFormat']
: 'iso8601';
$xml->writeRaw(TimestampShape::format($value, $timestampFormat));
$xml->writeRaw(
TimestampShape::formatAsString($value, $timestampFormat)
);
$xml->endElement();
}

Expand Down
73 changes: 66 additions & 7 deletions aws/aws-sdk-php/src/Api/TimestampShape.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,93 @@ public function __construct(array $definition, ShapeMap $shapeMap)
/**
* Formats a timestamp value for a service.
*
* Sub-second precision provided by the caller is preserved for the
* `iso8601` and `unixTimestamp` formats. `rfc822` (HTTP-date) only
* supports whole seconds, so fractional seconds are dropped.
*
* @param mixed $value Value to format
* @param string $format Format used to serialize the value
*
* @return int|string
* @return int|float|string
* @throws \UnexpectedValueException if the format is unknown.
* @throws \InvalidArgumentException if the value is an unsupported type.
*/
public static function format($value, $format)
{
if ($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
$seconds = $value->getTimestamp();
$micros = (int) $value->format('u');
} elseif (is_string($value)) {
$value = strtotime($value);
} elseif (!is_int($value) && !is_float($value)) {
$seconds = strtotime($value);
// strtotime() discards fractional seconds; re-parse to keep them.
$micros = $seconds === false
? 0
: (int) (new \DateTimeImmutable($value))->format('u');
} elseif (is_int($value)) {
$seconds = $value;
$micros = 0;
} elseif (is_float($value)) {
$seconds = (int) floor($value);
$micros = (int) round(($value - $seconds) * 1000000);
if ($micros === 1000000) {
$seconds++;
$micros = 0;
}
} else {
throw new \InvalidArgumentException('Unable to handle the provided'
. ' timestamp type: ' . gettype($value));
}

switch ($format) {
case 'iso8601':
return gmdate('Y-m-d\TH:i:s\Z', (int) $value);
$fraction = $micros === 0 ? '' : sprintf('.%06d', $micros);
return gmdate('Y-m-d\TH:i:s', (int) $seconds) . $fraction . 'Z';
case 'rfc822':
return gmdate('D, d M Y H:i:s \G\M\T', (int) $value);
return gmdate('D, d M Y H:i:s \G\M\T', (int) $seconds);
case 'unixTimestamp':
return $value;
if (is_float($value)) {
// Pass through the caller's float untouched.
return $value;
}
if ($micros === 0) {
return $seconds;
}
// Add the microseconds as integers before dividing so the
// float is only rounded once. Dividing first and then adding
// rounds twice, which can produce a slightly different value,
// e.g. 1 + 3691 / 10**6 gives 1.0036909999999999 instead of
// 1.003691.
return ($seconds * 1000000 + $micros) / 1000000;
default:
throw new \UnexpectedValueException('Unknown timestamp format: '
. $format);
}
}

/**
* Formats a timestamp value for a service as a string.
*
* Behaves like {@see format()}, except that a `unixTimestamp` value is
* returned as a decimal string (e.g. "1704110400.123456"). Casting a
* float to a string directly is subject to the `precision` ini setting
* (14 significant digits by default), which would silently drop the
* sub-second component of an epoch-seconds timestamp.
*
* @param mixed $value Value to format
* @param string $format Format used to serialize the value
*
* @return string
* @throws \UnexpectedValueException if the format is unknown.
* @throws \InvalidArgumentException if the value is an unsupported type.
*/
public static function formatAsString($value, $format): string
{
$formatted = self::format($value, $format);
if (is_float($formatted)) {
// Round to microseconds and trim insignificant trailing zeros.
$formatted = rtrim(rtrim(sprintf('%.6F', $formatted), '0'), '.');
}

return (string) $formatted;
}
}
16 changes: 12 additions & 4 deletions aws/aws-sdk-php/src/AwsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ public static function getArguments()
* signature version to use with a service (e.g., v4). Note that
* per/operation signature version MAY override this requested signature
* version.
* - transport_sharing: (string) Set to a transport sharing mode ("none",
* "handler_prefer", "handler_require", "persistent_prefer", or
* "persistent_require") to enable connection sharing on the default
* HTTP handler. The "*_prefer" modes degrade gracefully when the
* installed version of Guzzle or the runtime cannot honor them, and
* the "*_require" modes throw. This option only applies when the SDK
* creates the default HTTP handler.
* - use_aws_shared_config_files: (bool, default=bool(true)) Set to false to
* disable checking for shared config file in '~/.aws/config' and
* '~/.aws/credentials'. This will override the AWS_CONFIG_FILE
Expand Down Expand Up @@ -544,7 +551,7 @@ private function addQueryModeHeader(): void
{
$list = $this->getHandlerList();
$list->appendBuild(
Middleware::mapRequest(function (RequestInterface $r) {
Middleware::mapRequest(static function (RequestInterface $r) {
return $r->withHeader(
'x-amzn-query-mode',
"true"
Expand Down Expand Up @@ -657,11 +664,12 @@ private function addUserAgentMiddleware($args)
*/
private function addEventStreamHttpFlagMiddleware(): void
{
$api = $this->getApi();
$this->getHandlerList()
-> appendInit(
function (callable $handler) {
return function (CommandInterface $command, $request = null) use ($handler) {
$operation = $this->getApi()->getOperation($command->getName());
static function (callable $handler) use ($api) {
return static function (CommandInterface $command, $request = null) use ($handler, $api) {
$operation = $api->getOperation($command->getName());
$output = $operation->getOutput();
foreach ($output->getMembers() as $memberProps) {
if (!empty($memberProps['eventstream'])) {
Expand Down
Loading
Loading