Skip to content

feat(http-guzzle): add option to retry only unprocessed mutations - #143

Merged
Vincent Biret (baywet) merged 2 commits into
microsoft:mainfrom
jackson-asmith:fix/retry-unprocessed-mutations-only
Nov 13, 2026
Merged

Vincent Biret (baywet) merged 2 commits into
microsoft:mainfrom
jackson-asmith:fix/retry-unprocessed-mutations-only

Conversation

@jackson-asmith

@jackson-asmith Jackson Smith (jackson-asmith) commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #142

This ports microsoft/kiota-dotnet#864 to PHP. The other ports so far are microsoft/kiota-http-go#312, microsoft/kiota-python#897, microsoft/kiota-java#2251 and microsoft/kiota-typescript#2230.

Problem

RetryHandler retries 429, 503 and 504 responses whatever the HTTP method. For POST and PATCH, a retry after a 504 can apply the request twice. A 504 means a gateway stopped waiting, not that the server did nothing, so the first attempt may already have succeeded. With Microsoft Graph's sendMail, this sends the same email twice.

Callers can't avoid this today without turning off retries. The shouldRetry callback receives the delay, the attempt count and the response, but not the request, so there's no way to skip retries for some methods.

Change

This adds RetryOption::setRetryUnprocessedMutationsOnly() and getRetryUnprocessedMutationsOnly(), plus a matching optional last constructor argument, matching the .NET option.

  • Default is false. Existing behavior doesn't change.
  • When true, POST and PATCH are retried only after a 429 or 503. By common service convention, those responses mean the server didn't process the request. A 504 is returned to the caller.
  • Other methods aren't affected. GET, PUT, DELETE and QUERY keep retrying as before.
  • The check runs on every attempt, so a POST that gets a 503 and then a 504 stops at the 504.
  • It works as a client-wide option or per request.
  • It covers both paths: the check is in shouldRetry(), which handles both a normal response and a 504 that Guzzle raises as a BadResponseException.
$option = (new RetryOption())->setRetryUnprocessedMutationsOnly(true);

KiotaClientFactory pushes Guzzle's redirect middleware before the retry handler, so the redirect middleware wraps it and the retry handler only sees the request for the current redirect hop. Checking the request's method is therefore always the method being resent.

Tests

RetryHandlerTest has 26 new tests covering:

  • the default
  • each method and status combination above
  • method matching that ignores case
  • the 503 then 504 sequence
  • the per-request option
  • Guzzle's redirect middleware and the retry handler together: a POST that a 303 turns into a GET is retried after a 504, and a POST kept by a 307 is not

The exception path is covered too: with Guzzle's http_errors middleware below the retry handler, a POST or PATCH 504 is raised without another request when the option is on, and a 429 or 503 is still retried.

With the new check removed from shouldRetry(), the 8 tests for the new behavior fail and the other 18 pass. Locally on PHP 8.5, monorepo-builder validate, PHPStan and the full PHPUnit suite (476 tests) pass.

Add RetryOption::setRetryUnprocessedMutationsOnly() and a matching
constructor argument. It defaults to false, which keeps the current
behavior. When it is true, POST and PATCH requests are retried only
after a 429 or 503 response, which indicate the server did not process
the request. A 504 is returned to the caller, because the server may
already have applied the request and a retry could apply it twice.
Other methods are unaffected.
Copilot AI balanced review requested due to automatic review settings November 13, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

POST/PATCH rejection-path tests with the option enabled are missing, leaving unverified that 504 propagates the original exception without retrying while 429/503 still retry.

1 open issue
What changed in this PR

Adds opt-in retry protection to Kiota’s Guzzle middleware to reduce duplicate POST/PATCH side effects after gateway timeouts, addressing #142 while preserving existing defaults.

Changes:

  • Adds retryUnprocessedMutationsOnly, defaulting to false, with constructor and accessor support.
  • When enabled, restricts POST/PATCH retries to 429/503 responses while preserving existing retry checks and other methods’ behavior.
  • Adds regression tests for defaults, methods, status sequences, per-request options, and redirects.
File Description
packages/​http/​guzzle/​tests/​Middleware/​RetryHandlerTest.php Adds retry-policy and redirect regression coverage.
packages/​http/​guzzle/​src/​Middleware/​RetryHandler.php Applies the method-specific policy during retry decisions.
packages/​http/​guzzle/​src/​Middleware/​Options/​RetryOption.php Exposes and documents the opt-in setting and its safety assumptions.

Review effort: Balanced

…tionsOnly

Add Guzzle's http_errors middleware below the retry handler so 4xx and
5xx responses arrive as BadResponseException. With the option on, a
POST or PATCH 504 is raised without another request, and a 429 or 503
is still retried.
Copilot AI balanced review requested due to automatic review settings November 13, 2026 18:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues are reported for the opt-in retry-safety change.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@github-project-automation github-project-automation Bot moved this to In Progress 🚧 in Kiota Nov 13, 2026
@baywet
Vincent Biret (baywet) added this pull request to the merge queue Nov 13, 2026
Merged via the queue into microsoft:main with commit aa69e4b Nov 13, 2026
11 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress 🚧 to Done ✔️ in Kiota Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done ✔️

Development

Successfully merging this pull request may close these issues.

RetryHandler retries non-idempotent POST requests on 504, which can duplicate side effects such as Graph sendMail

3 participants