fix(boxel-cli): Extract fetch error.cause on publish/unpublish failures#4925
Merged
Conversation
Node's fetch always reports `TypeError: fetch failed` as `error.message`; the actual transport reason (ECONNRESET, TLS handshake error, undici socket error, ENOTFOUND, GOAWAY, etc.) is stashed on `error.cause` and was being silently dropped by the publish/unpublish error paths. That left the action-demo workflow showing a bare "Error: fetch failed" with no way to distinguish a real network issue from, say, a self-signed cert problem against the published-realm subdomain. Wrap the three swallowed sites: - `publish.ts` `.action()` catch: log `err.cause` separately if present. - `publish.ts` `waitForPublishedRealmReady`: capture cause into the `lastError` string so the readiness-timeout error reports the same thing the polling loop kept hitting. - `unpublish.ts` `unpublishRealm`: embed cause into the `result.error` string the CLI surfaces. This is the diagnostic the action-demo on #4897 needs to figure out why publish hangs at the initial POST despite the server-side mount completing successfully. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves diagnostics in @cardstack/boxel-cli when realm publish/unpublish operations fail due to Node fetch() reporting only TypeError: fetch failed, by extracting and surfacing the underlying transport error stored on error.cause.
Changes:
- Include
error.causedetails in readiness polling timeout errors duringrealm publish. - Log
error.causein therealm publishCLI error path to provide actionable transport failure details. - Inline
error.causedetails into the returned error string forrealm unpublishfailures.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/boxel-cli/src/commands/realm/publish.ts | Adds a helper to summarize fetch errors (including error.cause) for readiness polling and logs causes on CLI failures. |
| packages/boxel-cli/src/commands/realm/unpublish.ts | Enhances unpublish failure reporting by appending error.cause details to the returned error message. |
Comments suppressed due to low confidence (1)
packages/boxel-cli/src/commands/realm/publish.ts:209
- This change introduces new user-facing error formatting (including
error.cause) for readiness polling timeouts, but there’s no automated coverage to prevent regressions in the output. Consider adding a unit test fordescribeFetchError()(and/or a small test that simulates a fetch error with anErrorcause) so the thrown timeout message reliably includes the underlying transport detail.
}
lastError = `HTTP ${response.status}`;
} catch (error) {
lastError = describeFetchError(error);
}
let remaining = timeoutMs - (Date.now() - startedAt);
if (remaining <= 0) break;
await new Promise((r) =>
setTimeout(r, Math.min(READINESS_POLL_INTERVAL_MS, remaining)),
);
}
throw new Error(
`Timed out after ${timeoutMs}ms waiting for ${publishedRealmURL} to pass readiness check${
lastError ? `: ${lastError}` : ''
}`,
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
lukemelia
approved these changes
May 21, 2026
Per code review on #4925: `if (... && error.cause)` uses a truthy test, so causes of `''`, `0`, `false`, `NaN` would be silently dropped back to the shallow "fetch failed" output we're trying to avoid. `!= null` matches both `null` and `undefined` — the two absence markers — and lets every explicit cause value through. Applied at all three sites in this PR: - `publish.ts` `describeFetchError()` - `publish.ts` `.action()` catch - `unpublish.ts` `unpublishRealm()` catch
Per code review on #4925: the cause-flattening logic in `publish.ts`'s local `describeFetchError` and `unpublish.ts`'s inline catch block was byte-identical. Extract to `src/lib/describe-fetch-error.ts` so future tweaks (walking the full cause chain, surfacing `cause.code`, etc.) stay consistent across commands. The pretty-printing path in `publish.ts`'s `.action()` catch (which passes `err.cause` as a separate console.error arg so Node renders the full nested Error including stack frames) intentionally stays inline — it's a different shape from the "flatten to a string" use the helper covers. Tests: `tests/lib/describe-fetch-error.test.ts` covers all six shapes — no cause, Error cause, non-Error cause, the four falsy primitives (`''`, `0`, `false`, `NaN`) that motivated the `!= null` check, and the null/undefined absence cases that should drop the suffix entirely.
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.
While working on #4897, there were unhelpful
fetch failederrors like here when publishing wasn’t working. This extracts more detail fromerror.causeto help clarify failures.