goharbor/harbor · error · lib/errors.Error

NOT_FOUND

NOT_FOUND

Error message

artifact %s:%s not found

What it means

getByTag resolves a tag reference for the artifact API: it loads the repository, queries tags by RepositoryID and Name, and when zero tags match it returns NotFound with 'artifact repo:tag not found'. It is reached via GetByReference whenever the reference does not parse as a digest — i.e., every GET-artifact call addressed by tag.

Source

Thrown at src/controller/artifact/controller.go:338

	return c.assembleArtifact(ctx, art, option), nil
}

func (c *controller) getByTag(ctx context.Context, repository, tag string, option *Option) (*Artifact, error) {
	repo, err := c.repoMgr.GetByName(ctx, repository)
	if err != nil {
		return nil, err
	}
	tags, err := c.tagCtl.List(ctx, &q.Query{
		Keywords: map[string]any{
			"RepositoryID": repo.RepositoryID,
			"Name":         tag,
		},
	}, nil)
	if err != nil {
		return nil, err
	}
	if len(tags) == 0 {
		return nil, errors.New(nil).WithCode(errors.NotFoundCode).
			WithMessagef("artifact %s:%s not found", repository, tag)
	}
	return c.Get(ctx, tags[0].ArtifactID, option)
}

func (c *controller) Delete(ctx context.Context, id int64) error {
	accs, err := c.accessoryMgr.List(ctx, q.New(q.KeyWords{"ArtifactID": id}))
	if err != nil {
		return err
	}
	return orm.WithTransaction(func(ctx context.Context) error {
		return c.deleteDeeply(ctx, id, true, len(accs) > 0)
	})(orm.SetTransactionOpNameToContext(ctx, "tx-delete-artifact-delete"))
}

// "isRoot" is used to specify whether the artifact is the root parent artifact
// the error handling logic for the root parent artifact and others is different
// "isAccessory" is used to specify whether the artifact is an accessory.

View on GitHub (pinned to 7b2fd08cc5)

Solutions

  1. Confirm the tag exists: list artifacts with tags (GET .../artifacts?with_tag=true) or query the tag API
  2. Use the digest instead of the tag for stable, race-free lookups
  3. Double-URL-encode repository names that contain '/'
  4. Treat 404 as an expected outcome when the tag may have been removed between listing and fetch

Example fix

# before: repository name with '/' not encoded -> wrong repo, 404
curl https://harbor/api/v2.0/projects/library/repositories/my/team/app/artifacts/latest
# after: double-URL-encode the repository name
curl https://harbor/api/v2.0/projects/library/repositories/my%252Fteam%252Fapp/artifacts/latest
Defensive patterns

Strategy: try-catch

Try / catch

art, err := artifactCtl.GetByReference(ctx, repoName, ref, nil)
if err != nil {
    if errors.IsErr(err, errors.NotFoundCode) {
        // tag absent: fall back to digest or treat as deleted
    }
    return err
}

Prevention

When it happens

Trigger: GET /api/v2.0/projects/{project}/repositories/{repo}/artifacts/{tag} where that tag does not exist in that repository: never created, deleted, untagged, mistyped, or the repository name containing '/' was not double-URL-encoded so the wrong repo is looked up.

Common situations: UI or automation refreshing a tag that was deleted concurrently, case mismatches in tag names, forgetting that repo names with slashes need double encoding (my/team/app -> my%252Fteam%252Fapp).

Related errors


AI-assisted analysis of goharbor/harbor@7b2fd08cc5 (2026-08-16). Data as JSON: /api/errors/af786e2277e9abb4. Report an issue: GitHub.