Skip to content

Comments

fix: add streaming flag and error handling#3214

Open
yenfryherrerafeliz wants to merge 25 commits intoaws:masterfrom
yenfryherrerafeliz:feat_add_streaming_flag_middleware
Open

fix: add streaming flag and error handling#3214
yenfryherrerafeliz wants to merge 25 commits intoaws:masterfrom
yenfryherrerafeliz:feat_add_streaming_flag_middleware

Conversation

@yenfryherrerafeliz
Copy link
Contributor

@yenfryherrerafeliz yenfryherrerafeliz commented Nov 20, 2025

Issues #3124, #3018, #3181:

Description of changes:

  • Evaluates if a client's operation requires http streaming flag to be set to true, and if so then it pass the option along.
  • Make error parsers to reused a parsed body for scenario where the body is non seekable and can't be consumed again.
  • Make sure empty payloads are not parsed.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

- Evaluates if a client's operation requires http streaming flag to be set to true, and if so then it pass the option along.
- Make error parsers to reused a parsed body for scenario where the body is non seekable and can't be consumed again.

Also change the condition:
 (!$body->isSeekable() || $body->getSize())
To:
 (!$body->isSeekable() || !$body->getSize())

The reason is that a stream non seekable will most likely not have a size, which means this condition will always be true.
Reverted the condition to what it was:
(!$body->isSeekable() || $body->getSize())

Why?, I need to understand better what is the usage of this condition before changing. It was breaking some integ tests.
- When a response body is empty then we dont try to parse it.
- When the response body is non seekable then, we read the full body before parsing.
@yenfryherrerafeliz
Copy link
Contributor Author

Running integ tets
vendor/bin/behat --format=progress --tags=integ
...................................................................... 70
...................................................................... 140
...................................................................... 210
......................................

66 scenarios (66 passed)
248 steps (248 passed)
5m9.29s (45.13Mb)
Running smoke tests
vendor/bin/behat --format=progress --suite=smoke --tags='~@noassumerole'
...................................................................... 70
...................................................................... 140
...................................................................... 210
............

111 scenarios (111 passed)
222 steps (222 passed)
0m45.99s (95.47Mb)

@yenfryherrerafeliz yenfryherrerafeliz marked this pull request as ready for review November 26, 2025 15:04
throw new \RuntimeException('The HTTP handler was rejected without an "exception" key value pair.');
}

$serviceError = "AWS HTTP error: " . $err['exception']->getMessage();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change in format might affect log parsing. I'd leave this unless it's accounted for later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I basically changed this because it was duplicating the error message. What I did in specific was to append the message from the exception when a response is not available, but, if we have a response then we just use the error message from the deserialization process.

if (!isset($err['response'])) {
    $parts = ['response' => null];
    $serviceError .= $err['exception']->getMessage(); // MOVED HERE.
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. So basically a caller will still see the same thing, just printed from somewhere else?

Copy link
Contributor Author

@yenfryherrerafeliz yenfryherrerafeliz Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, caller will still see the same thing, but with the difference that now error messages will not be duplicated. Previously, if we deserialized a modeled message then we duplicate the error we output.
For example, before we were getting:

PHP Fatal error:  Uncaught exception 'Aws\S3\Exception\S3Exception' with message 'Error executing "GetObject" on "https://bucket.s3.us-east-2.amazonaws.com/no-exists"; AWS HTTP error:
Client error: `GET https://bucket.s3.us-east-2.amazonaws.com/no-exists` resulted in a `404 Not Found` response:
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message> (truncated...)
NoSuchKey (client): The specified key does not exist.'

GuzzleHttp\Exception\ClientException: Client error: `GET https://bucket.s3.us-east-2.amazonaws.com/no-exists` resulted in a `404 Not Found` response:
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message> (truncated...)

But now, after this fix:

PHP Fatal error:  Uncaught exception 'Aws\S3\Exception\S3Exception' with message 'Error executing "GetObject" on "https://bucket.s3.us-east-2.amazonaws.com/no-exists"; AWS HTTP error:
NoSuchKey (client): The specified key does not exist.'

GuzzleHttp\Exception\ClientException: Client error: `GET https://bucket.s3.us-east-2.amazonaws.com/no-exists` resulted in a `404 Not Found` response:
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message> (truncated...)

When the response body is a non seekable stream, which happens when the stream flag is set in the request, the parser condition to do a parsing is evaluated to true, but in empty payloads, such as when doing head requests, it fails. To remedy this we read the full body content and evaluate whether the returned value is empty before parsing.
To make a body content to be fully available always in implementations a response
wrapper class has been created that takes in a response and evaluates if the body
is non-seekable then it wrappes it into a CachingStream that will allow the body
to rewind.
Overwrites responses on non-seekable streams by wrapping the non-seekable body on a CachingStream to allow rewind.
Avoid consuming original body before it is cached.
@yenfryherrerafeliz
Copy link
Contributor Author

Running integ tets
vendor/bin/behat --format=progress --tags=integ
...................................................................... 70
...................................................................... 140
...................................................................... 210
......................................

66 scenarios (66 passed)
248 steps (248 passed)
10m14.82s (43.42Mb)
Running smoke tests
vendor/bin/behat --format=progress --suite=smoke --tags='~@noassumerole'
...................................................................... 70
...................................................................... 140
...................................................................... 210
............

111 scenarios (111 passed)
222 steps (222 passed)
0m35.47s (92.58Mb)

Copy link
Member

@stobrien89 stobrien89 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments and an old one (log parsing) that needs to be addressed

Copy link
Member

@stobrien89 stobrien89 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments- I just responded to that one question without doing a proper review, but thanks for the quick turnaround

throw new \RuntimeException('The HTTP handler was rejected without an "exception" key value pair.');
}

$serviceError = "AWS HTTP error: " . $err['exception']->getMessage();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. So basically a caller will still see the same thing, just printed from somewhere else?

} else {
$response = AbstractParser::getResponseWithCachingStream($response);

$rawBody = AbstractParser::getBodyContents($response);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we prevent the double read here by using getSize() in the seekable case? At least for RestJsonParser, this method is also called in payload()

//Unions must have at least one member set to a non-null value
// If the body is empty, we can assume it is unset
if (!empty($member['union']) && ($body->isSeekable() && !$body->getSize())) {
$rawBody = AbstractParser::getBodyContents($response);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a third read if we're using rest-json

'StartLiveTail' => true
];

public function __construct(array $args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably do away with the constructor altogether, but not required

// Not error occurred. Test successfully.
// Previously, on non-seekable streams it would have failed.
// Because, it would have tried to parse an empty string.
$this->assertTrue(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we strengthen the test here with some assertions on the parsed result?

Comment on lines +48 to +69
public static function getBodyContents(ResponseInterface $response): string
{
$body = $response->getBody();
if ($body->isSeekable()) {
$body->rewind();
}

return $body->getContents();
}

public static function getResponseWithCachingStream(
ResponseInterface $response
): ResponseInterface
{
if (!$response->getBody()->isSeekable()) {
return $response->withBody(
new CachingStream($response->getBody())
);
}

return $response;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two methods should have test coverage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants