fix: report Cookbot quota errors accurately instead of "busy, try again" - #86
Merged
Conversation
A user hit `Cookbot is busy right now. Please wait a moment and try again.`
and retried four times, each failing instantly. The message was mine, and
it was wrong: the server had said the account's AI credits for the billing
cycle were used up, which no amount of waiting fixes.
The cause was collapsing a whole gRPC status into one guess. `cookbot`
returns exactly three machine-readable reasons, all previously flattened
into misleading guidance:
Status::resource_exhausted("quota_exhausted") -> "busy, wait a moment"
Status::permission_denied("ai_feature_not_available") -> "check you are signed in"
Status::unavailable("quota_check_failed") -> "check your internet connection"
Match those reasons and say what actually happened. None of the three is
helped by retrying, so none of them suggests it any more.
The deeper mistake was throwing the server's explanation away. Only the
status code survived, the detail went to a console the packaged app does
not persist, so the report was undiagnosable from the message alone -
this one was only pinned down by reading the server source. Statuses that
reflect a server decision now keep the server's detail appended; pure
transport noise (`read ECONNRESET`) is still replaced, since the remedy
does not depend on which socket died.
Also split the two unrelated causes of RESOURCE_EXHAUSTED: gRPC reuses it
for the message size limit, where "wait and try again" is likewise wrong
because every retry resends the same oversized conversation. And raise
`max_receive_message_length` from the grpc-js default of 4 MB, so a large
response fails as a real error rather than as a fake usage limit.
`max_send_message_length` is deliberately left alone - it defaults to -1
(unlimited), so setting it would impose a new cap.
Classifying the status correctly in isolation is not enough: the reason also has to survive the retry loop and come out of request() as the text the chat UI renders.
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A user hit
Cookbot is busy right now. Please wait a moment and try again.and retried four times, each failing instantly, with the server healthy.That message was mine, from #83, and it was wrong. The server had said the account's AI credits for the billing cycle were used up — which no amount of waiting fixes. The user retried because the message told them to.
Cause
I collapsed a whole gRPC status into a single guess.
cookbotreturns exactly three machine-readable reasons, and all three were flattened into misleading guidance:resource_exhausted("quota_exhausted")permission_denied("ai_feature_not_available")unavailable("quota_check_failed")grep -rhon 'Status::[a-z_]*("[a-z_]*"' cookbot/crates/server/src/grpc/*.rsreturns those three and nothing else, and the quota one comes from a single site:Each is now matched and explained. None of the three is helped by retrying, so none of them suggests it.
The deeper mistake
toUserFacing()kept only the status code and sent the server's explanation to a console the packaged app does not persist. So a user report contained nothing to diagnose from — this one was only pinned down by reading the server source, which is not a debugging strategy that generalises.Statuses reflecting a server decision now keep the detail appended. Pure transport noise (
read ECONNRESET) is still replaced, since the remedy does not depend on which socket died.Also
max_receive_message_lengthfrom the grpc-js default of 4 MB, so a large response fails as a real error rather than a fake usage limit.max_send_message_lengthis deliberately left alone — verified ingrpc-js/build/src/constants.jsthat it defaults to-1(unlimited), so setting it would have introduced a cap. My first attempt did exactly that.36 tests passing, lint clean. New
cookbot-error.spec.tscovers each server reason, detail preservation, and the size-vs-quota split.Follow-up worth its own issue
The packaged app persists no backend log, so
console.errordiagnostics are unrecoverable from a user report. Worth fixing separately — it is why this took a source dive rather than a log read.