argoproj/argo-workflows · error

unable to test if blob %s is a directory: %w

Error message

unable to test if blob %s is a directory: %w

What it means

After OpenStream hits BlobNotFound or an empty file, it calls IsDirectory to check whether the blob name is actually a directory (prefix). If IsDirectory itself fails (listing the prefix errors), the driver wraps that failure with this message instead of deciding file-vs-directory.

Source

Thrown at workflow/artifacts/azure/azure.go:261

	// Attempt the download. If it fails with a BlobNotFound error, or succeeds but with
	// a content length of 0, then it could be that we're attempting to stream a directory.
	// Check if the blob represents a directory and return an error if so. If not, then
	// return either the original BlobNotFound error or the empty file stream.
	emptyFile := false
	response, origErr := blobClient.DownloadStream(ctx, nil)
	if origErr == nil {
		emptyFile = *response.ContentLength == 0
		// We have a normal file blob, so just return the response body stream
		if !emptyFile {
			return response.Body, nil
		}
	} else if !bloberror.HasCode(origErr, bloberror.BlobNotFound) {
		return nil, fmt.Errorf("unable to open stream for blob %s: %w", artifact.Azure.Blob, origErr)
	}

	isDir, err := azblobDriver.IsDirectory(ctx, artifact)
	if err != nil {
		return nil, fmt.Errorf("unable to test if blob %s is a directory: %w", artifact.Azure.Blob, err)
	}
	if isDir {
		return nil, argoerrors.New(argoerrors.CodeNotImplemented, "Directory Stream capability currently unimplemented for Azure Blob")
	} else if !emptyFile {
		// Not a directory (and not successful retrieval of an empty file), so return
		// the original BlobNotFound error
		return nil, fmt.Errorf("unable to open blob stream for %s: %w", artifact.Azure.Blob, origErr)
	}

	return response.Body, nil
}

// Save saves an artifact to Azure Blob Storage
func (azblobDriver *ArtifactDriver) Save(ctx context.Context, path string, outputArtifact *wfv1.Artifact) error {
	logger := logging.RequireLoggerFromContext(ctx)
	logger.WithField("endpoint", outputArtifact.Azure.Endpoint).
		WithField("container", outputArtifact.Azure.Container).
		WithField("blob", outputArtifact.Azure.Blob).

View on GitHub (pinned to 35bff19146)

Solutions

  1. Fix the cause of the listing failure shown by the wrapped error (credentials, container existence, permissions).
  2. Ensure the account key/identity has Storage Blob Data Reader (list) rights, not just read of a single blob.
  3. Verify container name and endpoint in the artifact spec.
  4. Retry on transient storage errors.
  5. If the artifact should really exist, check the producing step's blob name matches exactly (no stray prefix).

Example fix

// before: identity lacks list permission
role: "Storage Blob Data Reader (read only single blob)"
// after: grant data-plane list access
role: "Storage Blob Data Reader"  # includes list on container
Defensive patterns

Strategy: retry

Validate before calling

props, err := containerClient.NewBlockBlobClient(blob).GetProperties(ctx, nil)
if err != nil && !bloberror.HasCode(err, bloberror.BlobNotFound) {
  return fmt.Errorf("storage backend unhealthy: %w", err)
}

Type guard

func isTransientAzureErr(err error) bool {
  var re *azcore.ResponseError
  return errors.As(err, &re) && (re.StatusCode == 429 || re.StatusCode >= 500)
}

Try / catch

err := driver.OpenStream(ctx, artifact)
// wrap call
if err != nil && strings.Contains(err.Error(), "unable to test if blob") {
  if isTransientAzureErr(errors.Unwrap(err)) {
    // exponential backoff retry
  }
}

Prevention

When it happens

Trigger: OpenStream on a missing/empty blob when the subsequent ListBlob flat-metadata call inside IsDirectory fails: auth errors, container not found, throttling, network failures.

Common situations: Same credential problems as the original download but only surfacing on the fallback path; ADLS Gen2 accounts where prefix listing needs extra permissions; transient 429/503 during listing.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/1b34938013352132. Report an issue: GitHub.