From 66f36b6038ab75de38d4f5a1a6d0f6f1f2775dbc Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Mon, 5 Oct 2026 12:07:27 -0700 Subject: [PATCH 1/2] fix(add): fail fast with a clear error when a package isn't found When the search service doesn't know a bare package name (e.g. `devbox add inotifywait`), return an error that suggests `devbox search` instead of downloading nixpkgs and running `nix search`, which is slow and often fails with a confusing error. Dotted attribute paths (stdenv.cc.cc.lib, openssl.dev, python3Packages.requests) aren't all indexed by search, so those still fall back to nixpkgs. Also stop the v1 resolver from reporting network and server errors as "package not found". Fixes #2765 Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/devbox/packages.go | 17 ++++++++++++++++- internal/lock/resolve.go | 5 ++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/devbox/packages.go b/internal/devbox/packages.go index 6c8d7319e75..6e1f266ff48 100644 --- a/internal/devbox/packages.go +++ b/internal/devbox/packages.go @@ -133,6 +133,14 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt // about not building on the current system, since user's can continue // via --exclude-platform flag. packageNameForConfig = pkg.Versioned() + } else if errors.Is(err, nix.ErrPackageNotFound) && + !strings.Contains(pkg.CanonicalName(), ".") { + // Search indexes all top-level nixpkgs packages, so falling back to + // nixpkgs (which requires a slow download) won't find it either. + // Nested attribute paths (e.g. stdenv.cc.cc.lib, openssl.dev, + // python3Packages.requests) are not all indexed, so those still fall + // back below. + return packageNotFoundError(pkg) } else if !versionedPkg.IsDevboxPackage { // This means it didn't validate and we don't want to fallback to legacy // Just propagate the error. @@ -146,7 +154,7 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt if err != nil { // This means it looked like a devbox package or attribute path, but we // could not find it in search or in the legacy nixpkgs path. - return usererr.New("Package %s not found", pkg.Raw) + return packageNotFoundError(pkg) } } @@ -171,6 +179,13 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt return d.printPostAddMessage(ctx, pkgs, unchangedPackageNames, opts) } +func packageNotFoundError(pkg *devpkg.Package) error { + return usererr.New( + "Package %q not found. To search for packages, use `devbox search %s`", + pkg.Raw, pkg.CanonicalName(), + ) +} + func (d *Devbox) setPackageOptions(pkgs []string, opts devopt.AddOpts) error { for _, pkg := range pkgs { if err := d.cfg.PackageMutator().AddPlatforms( diff --git a/internal/lock/resolve.go b/internal/lock/resolve.go index 2183d7c6024..402b6f16618 100644 --- a/internal/lock/resolve.go +++ b/internal/lock/resolve.go @@ -68,9 +68,12 @@ func (f *File) FetchResolvedPackage(pkg string, refresh bool) (*Package, error) } packageVersion, err := searcher.Client().Resolve(name, version) - if err != nil { + if errors.Is(err, searcher.ErrNotFound) { return nil, errors.Wrapf(nix.ErrPackageNotFound, "%s@%s", name, version) } + if err != nil { + return nil, err + } sysInfos, err := buildLockSystemInfos(packageVersion) if err != nil { From f43b7bf4d7ccddc05b5f00d63d37700a6e06e920 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Mon, 5 Oct 2026 12:20:39 -0700 Subject: [PATCH 2/2] fix(add): only fast-fail for devbox packages, add tests - Gate the not-found fast path on IsDevboxPackage so flake refs and other non-devbox packages keep propagating their original error instead of getting an empty `devbox search` hint. - Note in the comment that the fast path is a heuristic. - Add a unit test that /v1/resolve and /v2/resolve map only 404s to ErrPackageNotFound. - Add a testscript for `devbox add` with a missing package. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/devbox/packages.go | 7 +++-- internal/lock/resolve_test.go | 43 ++++++++++++++++++++++++++ testscripts/add/add_not_found.test.txt | 20 ++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 internal/lock/resolve_test.go create mode 100644 testscripts/add/add_not_found.test.txt diff --git a/internal/devbox/packages.go b/internal/devbox/packages.go index 6e1f266ff48..9c7dd9a7c15 100644 --- a/internal/devbox/packages.go +++ b/internal/devbox/packages.go @@ -134,10 +134,11 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt // via --exclude-platform flag. packageNameForConfig = pkg.Versioned() } else if errors.Is(err, nix.ErrPackageNotFound) && + versionedPkg.IsDevboxPackage && !strings.Contains(pkg.CanonicalName(), ".") { - // Search indexes all top-level nixpkgs packages, so falling back to - // nixpkgs (which requires a slow download) won't find it either. - // Nested attribute paths (e.g. stdenv.cc.cc.lib, openssl.dev, + // Heuristic: search indexes top-level nixpkgs packages, so falling + // back to nixpkgs (which requires a slow download) won't find it + // either. Nested attribute paths (e.g. stdenv.cc.cc.lib, openssl.dev, // python3Packages.requests) are not all indexed, so those still fall // back below. return packageNotFoundError(pkg) diff --git a/internal/lock/resolve_test.go b/internal/lock/resolve_test.go new file mode 100644 index 00000000000..12b3efe6117 --- /dev/null +++ b/internal/lock/resolve_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 Jetify Inc. and contributors. All rights reserved. +// Use of this source code is governed by the license in the LICENSE file. + +package lock + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "go.jetify.com/devbox/internal/envir" + "go.jetify.com/devbox/internal/nix" +) + +func TestFetchResolvedPackageErrors(t *testing.T) { + for _, resolveV2 := range []string{"0", "1"} { + for _, test := range []struct { + status int + wantNotFound bool + }{ + {http.StatusNotFound, true}, + {http.StatusInternalServerError, false}, + } { + t.Run("v2="+resolveV2+"/"+http.StatusText(test.status), func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.status) + })) + t.Cleanup(srv.Close) + t.Setenv(envir.DevboxSearchHost, srv.URL) + t.Setenv("DEVBOX_FEATURE_RESOLVE_V2", resolveV2) + + _, err := (&File{}).FetchResolvedPackage("hello@latest", false) + if err == nil { + t.Fatal("got nil error, want an error") + } + if got := errors.Is(err, nix.ErrPackageNotFound); got != test.wantNotFound { + t.Errorf("errors.Is(err, nix.ErrPackageNotFound) = %v, want %v (err: %v)", got, test.wantNotFound, err) + } + }) + } + } +} diff --git a/testscripts/add/add_not_found.test.txt b/testscripts/add/add_not_found.test.txt new file mode 100644 index 00000000000..a690c52f2da --- /dev/null +++ b/testscripts/add/add_not_found.test.txt @@ -0,0 +1,20 @@ +# Testscript for adding a package that doesn't exist. + +exec devbox init + +# A bare name that search doesn't know should fail fast without +# downloading nixpkgs. +! exec devbox add inotifywait +stderr 'Package "inotifywait" not found. To search for packages, use `devbox search inotifywait`' +! stderr 'Ensuring nixpkgs registry is downloaded' + +# The search hint drops the version. +! exec devbox add hello@99999 +stderr 'Package "hello@99999" not found. To search for packages, use `devbox search hello`' + +json.superset devbox.json expected_devbox.json + +-- expected_devbox.json -- +{ + "packages": [] +}