fix(stripe): name the missing permission, stop faking an empty list #514

Merged
longjacksonle merged 1 commit from fix/BUNYIP-516-stripe-permission-errors-visible into main 2026-08-12 05:33:45 +02:00
Member

Creating a webhook endpoint from /admin/stripe failed with "An unexpected error occurred. Please try again later." and the endpoint list underneath reported "No webhook endpoints yet." Both statements were wrong in the same way: Stripe had answered 403 more_permissions_required because the restricted key lacked the Webhook Endpoints permission, and every layer discarded that fact on the way up. The setup docs on that same page asked for five permissions and not this one, so an admin who followed the instructions exactly could not resolve the incident from anything the screen said.

Bumps dunite-stripe (and dunite-stripe-core, byte-identical at both revs, so the graph keeps one copy) to the merge of DUNITE-10, which adds StripeServiceError::Stripe { message, details } carrying the HTTP status and Stripe's error.code / error.type, and makes list_webhook_endpoints check the HTTP status instead of parsing a 403 body into Ok(vec![]). The member-scoped cargo update moved exactly those two packages. DUNITE-9 rides along in the same rev and added recurring_interval_count to StripePriceResponse, so the pricing test fixture gains that field.

stripe_err maps the new variant by Stripe error code. more_permissions_required (and any 403) and a rejected key (401, api_key_expired, invalid_api_key) become AppError::validation, a 400; every other code keeps the 500. The 4xx is the whole point rather than a detail: ApiError::user_message collapses every 5xx to a generic line by design (BUNYIP-477), so a perfect message on a 500 could never have reached the browser. That rule is not loosened, and the only user-facing strings are the bunyip-authored ones built from the new StripePermission enum. Stripe's own error.message is not carried by StripeErrorDetails at all, so nothing from Stripe is rendered or logged (BUNYIP-265). stripe_err_for(permission) lets a call site name the permission it needs; all ten Stripe calls in admin_stripe.rs use it, and an unattributed failure says so instead of guessing a permission.

The three Stripe-backed blocks now take Result rather than Option, so "could not read this list" and "this list is empty" are different renderings: the failure says what bunyip does not know, names the permission that usually explains it, and carries the api request id. The false "No webhook endpoints yet" is the more dangerous half of the bug, because it reads as information rather than as a failure. The Prices form's product picker says why it is empty when the products could not be read, instead of looking like an account with no products.

A failed create now redirects back with the reason, the submitted URL and the submitted event text, and the block restates the failure next to the button that produced it. A toast was the wrong surface: it faded after 2.5 seconds and the form came back blank, so retrying meant retyping everything.

The request id shown is bunyip-api's X-Request-Id, not the error envelope's meta.request_id: dunite-core mints a fresh id while rendering an error body, so that value correlates with no log line anywhere. ClientIpRootSpanBuilder now records the same id the middleware echoes in that header, so the reference an admin quotes is the request_id on the api log lines.

#BUNYIP-516

Creating a webhook endpoint from `/admin/stripe` failed with "An unexpected error occurred. Please try again later." and the endpoint list underneath reported "No webhook endpoints yet." Both statements were wrong in the same way: Stripe had answered 403 `more_permissions_required` because the restricted key lacked the Webhook Endpoints permission, and every layer discarded that fact on the way up. The setup docs on that same page asked for five permissions and not this one, so an admin who followed the instructions exactly could not resolve the incident from anything the screen said. Bumps `dunite-stripe` (and `dunite-stripe-core`, byte-identical at both revs, so the graph keeps one copy) to the merge of DUNITE-10, which adds `StripeServiceError::Stripe { message, details }` carrying the HTTP status and Stripe's `error.code` / `error.type`, and makes `list_webhook_endpoints` check the HTTP status instead of parsing a 403 body into `Ok(vec![])`. The member-scoped `cargo update` moved exactly those two packages. DUNITE-9 rides along in the same rev and added `recurring_interval_count` to `StripePriceResponse`, so the pricing test fixture gains that field. `stripe_err` maps the new variant by Stripe error code. `more_permissions_required` (and any 403) and a rejected key (401, `api_key_expired`, `invalid_api_key`) become `AppError::validation`, a 400; every other code keeps the 500. The 4xx is the whole point rather than a detail: `ApiError::user_message` collapses every 5xx to a generic line by design (BUNYIP-477), so a perfect message on a 500 could never have reached the browser. That rule is not loosened, and the only user-facing strings are the bunyip-authored ones built from the new `StripePermission` enum. Stripe's own `error.message` is not carried by `StripeErrorDetails` at all, so nothing from Stripe is rendered or logged (BUNYIP-265). `stripe_err_for(permission)` lets a call site name the permission it needs; all ten Stripe calls in `admin_stripe.rs` use it, and an unattributed failure says so instead of guessing a permission. The three Stripe-backed blocks now take `Result` rather than `Option`, so "could not read this list" and "this list is empty" are different renderings: the failure says what bunyip does not know, names the permission that usually explains it, and carries the api request id. The false "No webhook endpoints yet" is the more dangerous half of the bug, because it reads as information rather than as a failure. The Prices form's product picker says why it is empty when the products could not be read, instead of looking like an account with no products. A failed create now redirects back with the reason, the submitted URL and the submitted event text, and the block restates the failure next to the button that produced it. A toast was the wrong surface: it faded after 2.5 seconds and the form came back blank, so retrying meant retyping everything. The request id shown is bunyip-api's `X-Request-Id`, not the error envelope's `meta.request_id`: dunite-core mints a fresh id while rendering an error body, so that value correlates with no log line anywhere. `ClientIpRootSpanBuilder` now records the same id the middleware echoes in that header, so the reference an admin quotes is the `request_id` on the api log lines. #BUNYIP-516
fix(stripe): name the missing permission, stop faking an empty list
Some checks failed
E2E / PR gate (pull_request) Successful in 33s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
5348812b74
Creating a webhook endpoint from `/admin/stripe` failed with "An unexpected error occurred. Please try again later." and the endpoint list underneath reported "No webhook endpoints yet." Both statements were wrong in the same way: Stripe had answered 403 `more_permissions_required` because the restricted key lacked the Webhook Endpoints permission, and every layer discarded that fact on the way up. The setup docs on that same page asked for five permissions and not this one, so an admin who followed the instructions exactly could not resolve the incident from anything the screen said.

Bumps `dunite-stripe` (and `dunite-stripe-core`, byte-identical at both revs, so the graph keeps one copy) to the merge of DUNITE-10, which adds `StripeServiceError::Stripe { message, details }` carrying the HTTP status and Stripe's `error.code` / `error.type`, and makes `list_webhook_endpoints` check the HTTP status instead of parsing a 403 body into `Ok(vec![])`. The member-scoped `cargo update` moved exactly those two packages. DUNITE-9 rides along in the same rev and added `recurring_interval_count` to `StripePriceResponse`, so the pricing test fixture gains that field.

`stripe_err` maps the new variant by Stripe error code. `more_permissions_required` (and any 403) and a rejected key (401, `api_key_expired`, `invalid_api_key`) become `AppError::validation`, a 400; every other code keeps the 500. The 4xx is the whole point rather than a detail: `ApiError::user_message` collapses every 5xx to a generic line by design (BUNYIP-477), so a perfect message on a 500 could never have reached the browser. That rule is not loosened, and the only user-facing strings are the bunyip-authored ones built from the new `StripePermission` enum. Stripe's own `error.message` is not carried by `StripeErrorDetails` at all, so nothing from Stripe is rendered or logged (BUNYIP-265). `stripe_err_for(permission)` lets a call site name the permission it needs; all ten Stripe calls in `admin_stripe.rs` use it, and an unattributed failure says so instead of guessing a permission.

The three Stripe-backed blocks now take `Result` rather than `Option`, so "could not read this list" and "this list is empty" are different renderings: the failure says what bunyip does not know, names the permission that usually explains it, and carries the api request id. The false "No webhook endpoints yet" is the more dangerous half of the bug, because it reads as information rather than as a failure. The Prices form's product picker says why it is empty when the products could not be read, instead of looking like an account with no products.

A failed create now redirects back with the reason, the submitted URL and the submitted event text, and the block restates the failure next to the button that produced it. A toast was the wrong surface: it faded after 2.5 seconds and the form came back blank, so retrying meant retyping everything.

The request id shown is bunyip-api's `X-Request-Id`, not the error envelope's `meta.request_id`: dunite-core mints a fresh id while rendering an error body, so that value correlates with no log line anywhere. `ClientIpRootSpanBuilder` now records the same id the middleware echoes in that header, so the reference an admin quotes is the `request_id` on the api log lines.

#BUNYIP-516
longjacksonle force-pushed fix/BUNYIP-516-stripe-permission-errors-visible from 5348812b74
Some checks failed
E2E / PR gate (pull_request) Successful in 33s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
to 29b98e487c
All checks were successful
E2E / PR gate (pull_request) Successful in 32s
Check / fmt + clippy + build + tests (pull_request) Successful in 34m53s
2026-08-12 04:37:39 +02:00
Compare
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-12 04:40:24 +02:00
longjacksonle force-pushed fix/BUNYIP-516-stripe-permission-errors-visible from 29b98e487c
All checks were successful
E2E / PR gate (pull_request) Successful in 32s
Check / fmt + clippy + build + tests (pull_request) Successful in 34m53s
to 1c8faf55a0
All checks were successful
E2E / PR gate (pull_request) Successful in 15s
Check / fmt + clippy + build + tests (pull_request) Successful in 14m52s
Create release / Create release from merged PR (pull_request) Has been skipped
2026-08-12 05:18:50 +02:00
Compare
longjacksonle deleted branch fix/BUNYIP-516-stripe-permission-errors-visible 2026-08-12 05:33:45 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!514
No description provided.