Skip to content

fix(client): code the pre-send failures so callers can replay them - #47

Merged
abnegate merged 2 commits into
mainfrom
fix/unsent-error-code
Aug 2, 2026
Merged

fix(client): code the pre-send failures so callers can replay them#47
abnegate merged 2 commits into
mainfrom
fix/unsent-error-code

Conversation

@abnegate

@abnegate abnegate commented Aug 1, 2026

Copy link
Copy Markdown
Member

Connection validation runs ahead of the send, so both failures it raises leave the command unsent and nothing applied — the one class of failure a caller may safely replay.

Both carried code 0, making that distinction legible only in the message text. A caller that classifies on text turns a permanent failure into an advertised-retryable one the moment a message quotes a caller-chosen value, so callers correctly refuse to do it. The consequence downstream: a cloud connection pool could not tell a replayable pre-send refusal from a post-send receive timeout of unknown outcome, and surfaced the former to clients as a 500 during a dedicated-backing resize.

Both throws now carry MongoDB's own HostUnreachable (6), with isUnsentError() to read it. The messages are unchanged, so the client's own internal reconnect check that matches on 'Connection to MongoDB has been lost' behaves identically.

Covered by tests/UnsentErrorTest.php: an unsent failure is distinguishable by code, a post-send timeout is never reported as unsent, and an uncoded failure is not assumed unsent.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added clearer classification for host-unreachable connection failures.
    • Added a way to determine whether an error occurred before a command was sent and may be safely retried.
  • Bug Fixes

    • Pre-send connection failures now consistently report the appropriate host-unreachable error code.
    • Post-send timeouts and uncoded errors are not incorrectly classified as unsent failures.

Connection validation runs ahead of the send, so both failures it raises
leave the command unsent and nothing applied - the one class of failure
a caller may safely replay. Both carried code 0, making that legible
only in the message text, and a caller that classifies on text turns a
permanent failure into an advertised-retryable one the moment a message
quotes a caller-chosen value. A cloud connection pool consequently could
not tell a replayable refusal from a post-send timeout of unknown
outcome, and surfaced the former to clients as a 500 during a backing
resize.

Both now carry MongoDB's own HostUnreachable, with isUnsentError() to
read it. The messages are unchanged, so the client's own internal
reconnect check that matches on them still behaves identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 23:50
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@abnegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 91a91804-5006-4da4-ad4b-dceed385d9b9

📥 Commits

Reviewing files that changed from the base of the PR and between b395b0e and 232d355.

📒 Files selected for processing (4)
  • src/Client.php
  • src/Exception.php
  • src/UnsentException.php
  • tests/UnsentErrorTest.php
📝 Walkthrough

Walkthrough

The change adds Exception::HOST_UNREACHABLE, adds isUnsentError(), assigns the code to pre-send connection failures, and tests classification against post-send timeouts and uncoded errors.

Changes

Unsent error classification

Layer / File(s) Summary
Unsent error contract
src/Exception.php, tests/UnsentErrorTest.php
Defines the HOST_UNREACHABLE code and isUnsentError(). Tests cover unsent, post-send timeout, and uncoded failures.
Connection error code assignment
src/Client.php
Assigns HOST_UNREACHABLE to both pre-send connection failure paths.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: coding pre-send failures so callers can replay them.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/unsent-error-code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR now distinguishes failures raised before command transmission using an UnsentException subtype rather than an error code.

  • Replaces three pre-send connection failures with UnsentException.
  • Adds isUnsentError() behavior to the package exception hierarchy.
  • Verifies that server responses and post-send timeouts remain classified as potentially sent.
  • Resolves the prior PHP compatibility issue by avoiding typed class constants.
  • Resolves the prior unsafe code-collision issue by ensuring ordinary server-response exceptions never report themselves as unsent.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; both previously reported issues are resolved by the type-based classification.

Important Files Changed

Filename Overview
src/Client.php Pre-send dial and connection-validation failures now throw the dedicated subtype before the current command is written.
src/Exception.php Ordinary exceptions, including parsed server responses, explicitly remain classified as potentially sent.
src/UnsentException.php Adds a PHP 8.0-compatible subtype whose type identity carries the safe-replay classification.
tests/UnsentErrorTest.php Covers subtype classification, post-send timeout handling, and server code 6 collision behavior.

Reviews (2): Last reviewed commit: "fix(client): carry the pre-send failures..." | Re-trigger Greptile

Comment thread src/Exception.php Outdated
Comment thread src/Exception.php

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.

Pull request overview

Adds an explicit, code-based way to classify “pre-send” connection validation failures as replayable (unsent), so callers can safely distinguish them from post-send network/timeouts without matching on error message text.

Changes:

  • Introduces Exception::HOST_UNREACHABLE (MongoDB error code 6) and Exception::isUnsentError() for identifying replayable pre-send failures.
  • Updates Client::validateConnection() to throw connection-validation exceptions with HOST_UNREACHABLE instead of the default code 0.
  • Adds tests/UnsentErrorTest.php to verify unsent vs timeout vs uncoded error classification.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tests/UnsentErrorTest.php Adds unit tests asserting unsent errors are distinguishable by code and timeouts are not misclassified.
src/Exception.php Adds HOST_UNREACHABLE and isUnsentError() to expose unsent/replayable failures via code.
src/Client.php Tags pre-send connection validation throws with HOST_UNREACHABLE for reliable downstream classification.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Exception.php Outdated
* safely - a message quotes caller-chosen values, a code cannot be
* spelled by a caller.
*/
public const int HOST_UNREACHABLE = 6;
Comment thread src/Exception.php
Comment on lines 116 to 120
/**
* Check if this is a timeout error.
*
* @return bool
*/

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Client.php`:
- Around line 2181-2190: Update the failed reconnect-dial exception raised by
connect() and propagated through validateConnection() to use
Exception::HOST_UNREACHABLE, so unsent commands are recognized by
isUnsentError(). Add a transport test covering a failed reconnect and asserting
isUnsentError() returns true.

In `@src/Exception.php`:
- Around line 125-127: Update isUnsentError() and the related Exception
construction flow so unsent status is tracked by explicit local pre-send state
rather than inferred from code HOST_UNREACHABLE. Set that state only when the
client fails before sending, while preserving HOST_UNREACHABLE for
connection-validation classification; ensure fromResponse() and
Client::parseResponse() leave response-originated code 6 marked as sent/unknown,
and add coverage for that case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fce2e465-315d-4f40-b890-ada9b051e52f

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd4d96 and b395b0e.

📒 Files selected for processing (3)
  • src/Client.php
  • src/Exception.php
  • tests/UnsentErrorTest.php

Comment thread src/Client.php Outdated
Comment thread src/Exception.php Outdated
Review found the first attempt unsound. Connection validation and the
dial before it run ahead of every send, so a failure raised there leaves
the command unsent and nothing applied - but a numeric code cannot say
so. A post-send error response is parsed into the same Exception holding
the SERVER's code, HostUnreachable included, so any caller keying on the
code would replay an operation that was already transmitted and could
apply it twice. The typed class constant also broke the PHP 8.0-8.2 this
package still supports.

The signal is now a type: UnsentException, raised only by the client and
only before it has sent anything, answering isUnsentError() true where
the base answers false. It extends Exception, so every existing catch
behaves identically, and the messages are unchanged so the client's own
internal reconnect check still matches. The failed reconnect dial throws
it too - review caught that it escapes validateConnection while the
command is just as unsent.

Covered: an unsent failure is distinguishable by type and still caught
as the package exception, a post-send timeout is never unsent, and a
server response reporting HostUnreachable is never unsent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abnegate

abnegate commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

All four findings were right, and the second one invalidated the premise of the first attempt. Reworked in 232d355.

Error code cannot prove unsent state — correct, and this was the important one. Exception::fromResponse() and the write-error paths construct the same type carrying the server's code, so a post-send response reporting HostUnreachable would have made isUnsentError() answer true and invited a caller to replay an operation that was already transmitted. No numeric code can carry this property. The signal is now a type — UnsentException, raised only by the client and only before it has sent anything — which a server response can never produce. tests/UnsentErrorTest.php::testAServerErrorResponseIsNeverReportedAsUnsent pins exactly the case you described.

Typed constant breaks supported PHP — correct; the constant is gone entirely rather than untyped, since the type now carries the meaning.

Code failed reconnect dials as unsent errors (CodeRabbit) — correct. The failed dial at Client.php:306 escapes validateConnection() while the command is just as unsent; it throws UnsentException too.

UnsentException extends Exception, so every existing catch (Utopia\Mongo\Exception) behaves identically, and the two messages are unchanged so the client's own internal reconnect check that matches on 'Connection to MongoDB has been lost' is unaffected.

@abnegate
abnegate merged commit 3ece830 into main Aug 2, 2026
5 checks passed
@abnegate
abnegate deleted the fix/unsent-error-code branch August 2, 2026 00:46
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