Repository navigation
fix(order-sync): Stop retrying after a terminal 401/403, back off on 429, and tell the merchant - #1
Merged
Merged
Conversation
…29, tell the merchant One store's key lacked orders:write. Every order trigger (creation, each status change, each admin save) pushed again with the same key and was refused: 444 HTTP 403s in 50 minutes, about one every 3 seconds, and the merchant saw nothing. Nothing looped on its own (Action Scheduler does not re-run a callback that returns); a refusal was simply forgotten between triggers and treated like a timeout. Tack_Sync_Gate remembers it, using the temporary/terminal split the TackQuote API applies to the vendors it calls: - TERMINAL: 401 or 403 carrying TackQuote's JSON error body (insufficient_scope, SUBSCRIPTION_INACTIVE, an invalid or revoked key). Pushes stop, a wp-admin error notice names the missing scope and the fix, one re-probe is allowed per hour (a renewed subscription heals without a key change), and saving a different key lifts the block at once. - THROTTLED: 429 waits for Retry-After (delta-seconds or HTTP-date), capped at one hour. - TEMPORARY: everything else, including a 401/403 whose body is not JSON (a firewall or challenge page in front of the API). The API client now keeps status, code, requiredScopes and Retry-After on the WP_Error data (third constructor argument, per core). The error code is unchanged. No wire-contract change: it reads fields the API already sends. An order skipped while blocked stays unmarked, so its next trigger pushes it. Only a 16-character sha256 prefix of the key is stored, never the key itself. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… only TackQuote's own JSON is terminal, re-queue on unblock From Fable's review of 01e33a9 (APPROVE with changes). - A 429 carrying insufficient_scope or requiredScopes is TackQuote throttling the same scope refusal. It stays terminal, with until = max(now + Retry-After, now + TERMINAL_REPROBE), so the wp-admin notice no longer flickers off each throttle window. - Terminal only for TackQuote's own error body: JSON whose statusCode equals the HTTP status and which carries a non-empty code. A proxy or WAF answering 403 with JSON of its own is temporary. The client now carries statusCode. - Re-queue after unblock. A block keeps `since` (when pushes started being held) across re-records. When it lifts (a push succeeds, a different key is saved via update_option_tack_quotes_api_key, or a stale-key block is dropped), Tack_Sync_Gate fires tack_quotes_order_sync_unblocked. Tack_Order_Sync schedules one async job, which runs wc_get_orders date_modified '>' since (the documented form, woocommerce docs/features/orders/wc-get-orders.md) and queues each order through the normal worker. It is bounded to 200 orders and 30 days, logs when it truncates, and does nothing when order sync is off. - Notice copy for a lapsed subscription: "Profile > Billing & Plan". - Retry-After digits are parsed with preg_match, not ctype_digit. - The notice links Tack_Settings::PAGE_SLUG directly. The bootstrap always loads it, so the dead fallback slug is gone. - readme.txt: "Unreleased" changelog entry. The version bump and tag are left to the owner. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Summary
One store's API key lacked the
orders:writescope. The plugin sentPOST /v1/integrations/woocommerce/order-syncabout every 3 seconds: 444 refusals (HTTP 403) in 50 minutes, and the merchant was never told.Nothing in the plugin looped. Action Scheduler does not re-run a callback that returns. Every order trigger (creation, each status change, each admin save) pushed again with a key that could never succeed, because a refusal was treated like a timeout.
New
Tack_Sync_Gateapplies the temporary/terminal split the TackQuote API uses:statusCodeequal to the HTTP status and a non-emptycode(insufficient_scope,SUBSCRIPTION_INACTIVE, an invalid or revoked key).notice-errornames the missing scope or the billing step (Profile > Billing & Plan).max(Retry-After, 1 h), so the notice does not flicker.Retry-After(seconds or an HTTP-date), capped at one hour.uninstall.phpremoves the new option.No wire-contract change. The plugin reads fields the API already sends. The server half (scope 403, then 429 with
Retry-After, then a daily seller notice) is already live on api.tackquote.com.Vendor sources:
as_enqueue_async_action(Context7/woocommerce/action-scheduler);wc_get_ordersdate_modified => '>' . tsform (Context7/woocommerce/woocommerce,docs/features/orders/wc-get-orders.md);WP_Errordata andwp_remote_retrieve_header(developer.wordpress.org).The readme has an Unreleased changelog entry. The version bump and tag are left to the owner.
Gates (php:8.2-cli via
docker run --rm, through~/bin/tack-heavy)php -lon every PHP filephp tests/run.phpon this branchincludes/entirelystatusCode === statuslift()stops announcing the unblockTERMINAL_REPROBE3600 -> 3run_syncno longer consults the gateOne equivalent mutant was omitted: dropping the
jsonterm cannot change an outcome, becausestatusCodeis only non-zero when the body parsed as JSON.🤖 Generated with Claude Code