Bug Description
WrapContextAsUserError in packages/orchestrator/pkg/template/build/builderrors/errors.go treats all errors containing context.Canceled as user-initiated cancellations, reporting "build was cancelled". However, context.Canceled can also originate from internal timeouts (e.g., envd init timeout in WaitForEnvd), which are not user cancellations. This causes misleading error messages that make debugging extremely difficult.
Root Cause
The error propagation chain:
-
WaitForEnvd (sandbox.go:~1790) creates a child context via context.WithCancelCause(ctx). When the 60s timeout fires, it calls cancel(errors.New("syncing took too long")).
-
doRequestWithInfiniteRetries (envd.go:93) detects ctx.Done() and returns:
return nil, requestCount, fmt.Errorf("%w with cause: %w", ctx.Err(), context.Cause(ctx))
// ctx.Err() == context.Canceled ← wrapped into the error chain
-
The error bubbles up through initEnvd → WaitForEnvd → CreateSandbox.Sandbox() → BuildLayer → Build.
-
Build (builder.go:~193) has a deferred call:
e = builderrors.WrapContextAsUserError(e)
Which checks:
if errors.Is(err, context.Canceled) {
return phases.NewPhaseBuildError(phases.PhaseMeta{}, ErrCanceled)
}
Since the error chain contains context.Canceled (from step 2), it matches — and the real cause ("syncing took too long" / envd failed to start) is discarded and replaced with "build was cancelled".
Impact
- Users see
"build was cancelled" when their build actually failed due to envd startup timeout, a crashed Firecracker process, or other internal issues.
- The real failure reason is completely lost, making it nearly impossible for users to diagnose and fix their builds.
- Metrics that distinguish user cancellations from internal failures are polluted.
Reproduction
Build a template with a base image where envd takes longer than 60 seconds to initialize (e.g., a heavy image on a slow network where provisioning installs many packages). The build will fail with "build was cancelled" instead of a message indicating envd startup timeout.
Suggested Fix
WrapContextAsUserError should only classify context.Canceled as a user cancellation when the Build-level context itself was canceled (i.e., via TemplateBuildDelete or the cancellation watcher in create_template.go). Internal child-context cancellations should not be treated as user cancellations.
One approach: check ctx.Err() of the Build context directly in the defer, rather than scanning the error chain with errors.Is:
defer func() {
if ctx.Err() != nil {
// ctx is the build-level context, so this IS a user/external cancellation
e = errors.Join(e, ctx.Err())
e = builderrors.WrapContextAsUserError(e)
}
// Otherwise, do not wrap — let the real error propagate
}()
Alternatively, WrapContextAsUserError could accept the build context and only match when ctx.Err() == context.Canceled, rather than using errors.Is which traverses the entire wrapped error chain.
Affected Files
packages/orchestrator/pkg/template/build/builderrors/errors.go — WrapContextAsUserError
packages/orchestrator/pkg/template/build/builder.go:~185-194 — deferred error wrapping
packages/orchestrator/pkg/sandbox/envd.go:93 — wraps ctx.Err() (context.Canceled) into the error chain
Bug Description
WrapContextAsUserErrorinpackages/orchestrator/pkg/template/build/builderrors/errors.gotreats all errors containingcontext.Canceledas user-initiated cancellations, reporting"build was cancelled". However,context.Canceledcan also originate from internal timeouts (e.g., envd init timeout inWaitForEnvd), which are not user cancellations. This causes misleading error messages that make debugging extremely difficult.Root Cause
The error propagation chain:
WaitForEnvd(sandbox.go:~1790) creates a child context viacontext.WithCancelCause(ctx). When the 60s timeout fires, it callscancel(errors.New("syncing took too long")).doRequestWithInfiniteRetries(envd.go:93) detectsctx.Done()and returns:The error bubbles up through
initEnvd→WaitForEnvd→CreateSandbox.Sandbox()→BuildLayer→Build.Build(builder.go:~193) has a deferred call:Which checks:
Since the error chain contains
context.Canceled(from step 2), it matches — and the real cause ("syncing took too long" / envd failed to start) is discarded and replaced with"build was cancelled".Impact
"build was cancelled"when their build actually failed due to envd startup timeout, a crashed Firecracker process, or other internal issues.Reproduction
Build a template with a base image where envd takes longer than 60 seconds to initialize (e.g., a heavy image on a slow network where provisioning installs many packages). The build will fail with
"build was cancelled"instead of a message indicating envd startup timeout.Suggested Fix
WrapContextAsUserErrorshould only classifycontext.Canceledas a user cancellation when the Build-level context itself was canceled (i.e., viaTemplateBuildDeleteor the cancellation watcher increate_template.go). Internal child-context cancellations should not be treated as user cancellations.One approach: check
ctx.Err()of the Build context directly in the defer, rather than scanning the error chain witherrors.Is:Alternatively,
WrapContextAsUserErrorcould accept the build context and only match whenctx.Err() == context.Canceled, rather than usingerrors.Iswhich traverses the entire wrapped error chain.Affected Files
packages/orchestrator/pkg/template/build/builderrors/errors.go—WrapContextAsUserErrorpackages/orchestrator/pkg/template/build/builder.go:~185-194— deferred error wrappingpackages/orchestrator/pkg/sandbox/envd.go:93— wrapsctx.Err()(context.Canceled) into the error chain