Skip to content

Bug: WrapContextAsUserError misclassifies internal timeouts as user-initiated cancellations #3154

Description

@AdaAibaby

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:

  1. 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")).

  2. 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
  3. The error bubbles up through initEnvd → WaitForEnvd → CreateSandbox.Sandbox() → BuildLayer → Build.

  4. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions