fix: surface 429 rate-limit errors in e2e tests and CLI#1309
Conversation
Multiple layered issues caused encryption key 429 errors to produce
confusing, unrelated-looking test failures:
- awaitCommand() in e2e utils now rejects on non-zero exit codes
instead of silently resolving (the most critical fix)
- cliInspectJson/cliHealthJson throw on empty stdout instead of
falling back to '{}'
- maybeDecryptFields re-throws HTTP errors instead of silently
falling back to encrypted placeholders
- fetchRunKey includes response body and status text in errors
- Added 429 to CLI status text map
🦋 Changeset detectedLatest commit: 36bd024 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (56 failed)mongodb (3 failed):
redis (2 failed):
turso (51 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
There was a problem hiding this comment.
Pull request overview
This PR improves error surfacing for HTTP failures (especially 429 rate-limit errors) across the e2e test suite, CLI hydration layer, and Vercel encryption key fetch. Previously, rate-limit responses would produce confusing, unrelated test failures with misleading assertion errors. The fix addresses four stacked issues: silent exit code handling in tests, empty stdout fallback, swallowed HTTP errors during decryption, and generic error messages in fetchRunKey.
Changes:
awaitCommand()now rejects on non-zero exit codes (with stderr/stdout in the error), andcliInspectJson()/cliHealthJson()throw on empty stdout instead of silently parsing{}.maybeDecryptFields()re-throws HTTP errors instead of falling back to encrypted placeholders, andfetchRunKey()now includes response body and status text in error messages.- Added
429: 'Too Many Requests'to the CLI'sgetStatusText()map.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
packages/core/e2e/utils.ts |
awaitCommand() rejects on non-zero exit code; cliInspectJson()/cliHealthJson() throw on empty stdout |
packages/cli/src/lib/inspect/hydration.ts |
Re-throws HTTP-related errors from decryption catch block instead of silently falling back |
packages/cli/src/lib/inspect/output.ts |
Adds 429 status code to getStatusText() lookup |
packages/world-vercel/src/encryption.ts |
Enriches fetchRunKey error with response body and status text |
.changeset/fix-world-vercel-fetchrunkey-error.md |
Changeset for @workflow/world-vercel patch |
.changeset/fix-cli-429-error-surfacing.md |
Changeset for @workflow/cli patch |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| // If the key fetch failed due to an HTTP error (e.g. 429 rate limit, | ||
| // 500 server error), re-throw so the caller surfaces a clear failure | ||
| // instead of silently showing encrypted placeholders. | ||
| if (message.includes('HTTP ')) { |
There was a problem hiding this comment.
The message.includes('HTTP ') heuristic for distinguishing HTTP errors from decryption errors is fragile. If any future error message from maybeDecrypt, importKey, or other code in the try block happens to contain the substring "HTTP ", it will be incorrectly re-thrown instead of falling through to the graceful fallback.
Consider a more robust approach, such as checking for the specific prefix used in fetchRunKey (e.g., message.startsWith('Failed to fetch run key')) or tagging HTTP errors with a custom property (e.g., error.isHttpError = true) in fetchRunKey and checking for that property here.
| if (message.includes('HTTP ')) { | |
| const isHttpError = | |
| (err as any)?.isHttpError === true || | |
| message.startsWith('Failed to fetch run key'); | |
| if (isHttpError) { |
…ignal * origin/main: (26 commits) Fix flaky streamer test ENOENT when chunks directory does not exist yet (#1330) Version Packages (beta) (#1325) [web-shared] Improve workflow observability event list UX (#1337) feat: add `exists` getter to `Run` class (#1336) Support client-side tools in DurableAgent (#1329) [world-postgres] [world-local] Execute Graphile jobs directly instead of defering to world-local queue (#1334) Merge CLAUDE.md into AGENTS.md and symlink CLAUDE.md (#1326) [web] Polish loading indicators (#1327) Fix flaky webhookWorkflow e2e test by polling instead of fixed sleep (#1328) feat: support `deploymentId: 'latest'` in `start()` to resolve most recent deployment (#1317) Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation [web] [world-vercel] Ensure user-passed run IDs are URL encoded and call out self-hosted security (#1322) Version Packages (beta) (#1306) Remove hard-coded VERCEL_DEPLOYMENT_KEY from nextjs-turbopack workbench (#1319) fix(web): move react-router deps to devDependencies (#1265) fix(ai): use workspace:* for workflow peer dependency (#1320) fix(core): pass resolved deploymentId to getEncryptionKeyForRun in start() (#1318) fix: surface 429 rate-limit errors in e2e tests and CLI (#1309) fix(world-local): return HTTP 200 instead of 503 for queue timeout re-enqueue signals (#1307) [web-shared] [cli] Refactor observability data fetching (#1261) ... # Conflicts: # packages/core/e2e/e2e.test.ts # packages/web-shared/src/components/sidebar/attribute-panel.tsx # workbench/example/workflows/99_e2e.ts
Summary
Problem
When the Vercel run-key endpoint returns a 429, the test failures show misleading assertion errors like
expected 133, received "🔒 Encrypted"with no indication of the actual cause. This is due to 4 stacked issues:awaitCommand()ignores exit codes (packages/core/e2e/utils.ts) — always resolves even when the CLI exits with code 1cliInspectJson()falls back to{}on empty stdout — silently parses to an empty objectmaybeDecryptFields()swallows HTTP errors (packages/cli/src/lib/inspect/hydration.ts) — catches 429 errors and silently falls back to encrypted placeholdersfetchRunKey()produces generic error messages (packages/world-vercel/src/encryption.ts) — drops the response body and status textChanges
packages/core/e2e/utils.tsawaitCommand()now rejects on non-zero exit codes with stderr/stdout in the error.cliInspectJson()/cliHealthJson()throw on empty stdout.packages/cli/src/lib/inspect/hydration.tsmaybeDecryptFields()re-throws HTTP errors instead of silently falling back to encrypted placeholderspackages/cli/src/lib/inspect/output.ts429: 'Too Many Requests'togetStatusText()packages/world-vercel/src/encryption.tsfetchRunKey()includes response body and status text in error messages