Skip to content

[HttpClient] Retry safe requests using HTTP/1.1 when HTTP/2 fails #34199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 5, 2019

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Oct 31, 2019

Q A
Branch? 4.3
Bug fix? yes
New feature? no
Deprecations? no
Tickets -
License MIT
Doc PR -

cURL support of HTTP/2 is not as robust as HTTP/1.1. When doing >1k requests, the stream can break for buggy reasons. New versions of cURL are fixed already, but let's make our logic more resilient anyway, and switch to HTTP/1.1 when a safe request fails for send/recv reasons.

@stof
Copy link
Member

stof commented Nov 4, 2019

Why making 3 retries when HTTP/2 fails ?

@nicolas-grekas
Copy link
Member Author

Why making 3 retries when HTTP/2 fails ?

updated to 2: the first on HTTP/2, the last at HTTP/1.1

Copy link
Member Author

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

PR ready

@@ -66,6 +68,7 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
}

$this->multi = $multi = new CurlClientState();
self::$curlVersion = self::$curlVersion ?? curl_version();
Copy link
Member Author

@nicolas-grekas nicolas-grekas Nov 4, 2019

Choose a reason for hiding this comment

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

Calling curl_version() repeatedly is leaking memory - I suppose the array created by the extension is not collected as userland arrays.

Copy link
Member

Choose a reason for hiding this comment

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

I suggest to also report that as a PHP bug.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that's a bug - token_get_all() has the same "leak", reclaimed when calling https://www.php.net/manual/en/function.gc-mem-caches.php

$this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the idle timeout.';

if (\is_string($error)) {
$this->errorMessage = $error;
Copy link
Member Author

Choose a reason for hiding this comment

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

The message now contains the corresponding URL.


curl_setopt($ch, CURLOPT_HEADERFUNCTION, null);
curl_setopt($ch, CURLOPT_READFUNCTION, null);
curl_setopt($ch, CURLOPT_INFILE, null);
Copy link
Member Author

Choose a reason for hiding this comment

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

This is not needed - I've actually seen complains from curl_multi_exec() about this state change while processing the responses.

@@ -150,8 +148,6 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
public function getInfo(string $type = null)
{
if (!$info = $this->finalInfo) {
self::perform($this->multi);
Copy link
Member Author

Choose a reason for hiding this comment

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

Performing here totally kills performances when processing a large number of requests.

public function getContent(bool $throw = true): string
{
$performing = self::$performing;
self::$performing = $performing || '_0' === curl_getinfo($this->handle, CURLINFO_PRIVATE);
Copy link
Member Author

Choose a reason for hiding this comment

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

Performing here while the content is already available also kills performances when processing a large number of requests.

$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE) ?: '_0';

if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
Copy link
Member Author

Choose a reason for hiding this comment

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

The consts aren't declared in the PHP extension - yet these HTTP2 error numbers can still be yielded.

@@ -80,8 +80,6 @@ public function __construct(NativeClientState $multi, $context, string $url, $op
public function getInfo(string $type = null)
{
if (!$info = $this->finalInfo) {
self::perform($this->multi);
Copy link
Member Author

Choose a reason for hiding this comment

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

for consistency with CurlResponse

nicolas-grekas added a commit that referenced this pull request Nov 5, 2019
…2 fails (nicolas-grekas)

This PR was merged into the 4.3 branch.

Discussion
----------

[HttpClient] Retry safe requests using HTTP/1.1 when HTTP/2 fails

| Q             | A
| ------------- | ---
| Branch?       | 4.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

cURL support of HTTP/2 is not as robust as HTTP/1.1. When doing >1k requests, the stream can break for buggy reasons. New versions of cURL are fixed already, but let's make our logic more resilient anyway, and switch to HTTP/1.1 when a *safe* request fails for send/recv reasons.

Commits
-------

9f7cd66 [HttpClient] Retry safe requests when then fail before the body arrives
@nicolas-grekas nicolas-grekas merged commit 9f7cd66 into symfony:4.3 Nov 5, 2019
@stof
Copy link
Member

stof commented Nov 5, 2019

@nicolas-grekas but isn't it still only 1 retry, not 2 ? The first request is not a retry.

And if your expectation is to count the initial request as well, then the issue is using H0 rather than H1.

@nicolas-grekas nicolas-grekas deleted the hc-safe-retry branch November 5, 2019 16:21
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Nov 5, 2019

but isn't it still only 1 retry, not 2 ? The first request is not a retry.

H2 (initial value) => retry with HTTP/2 again and decrement to H1
H1 => retry with HTTP/1.1 and decrement to H0
H0 => no retry

so, that's two retries? or do you mean something else?

@fabpot fabpot mentioned this pull request Nov 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants