argoproj/argo-workflows · error
plugin %s save failed: %w
Error message
plugin %s save failed: %w
What it means
The unary Save RPC to the artifact plugin failed at the transport/gRPC level. The driver calls client.Save with the artifact path and metadata; any error returned by the gRPC call (network failure, unimplemented method, context timeout, plugin crash) is wrapped as 'plugin %s save failed: %w'. The wrapped gRPC status is the real diagnosis.
Source
Thrown at workflow/artifacts/plugin/plugin.go:212
writer.CloseWithError(fmt.Errorf("plugin %s stream write failed: %w", d.pluginName, writeErr))
return
}
}
}
}()
return reader, nil
}
// Save implements ArtifactDriver.Save by calling the plugin service
func (d *Driver) Save(ctx context.Context, path string, outputArtifact *wfv1.Artifact) error {
grpcArtifact := convertToGRPC(outputArtifact)
resp, err := d.client.Save(ctx, &artifact.SaveArtifactRequest{
Path: path,
OutputArtifact: grpcArtifact,
})
if err != nil {
return fmt.Errorf("plugin %s save failed: %w", d.pluginName, err)
}
if !resp.Success {
return fmt.Errorf("plugin %s save failed: %s", d.pluginName, resp.Error)
}
return nil
}
// saveStreamChunkSize is the size of each chunk sent over the streaming SaveStream RPC.
// 2MiB stays well under gRPC's default 4MiB max message size while keeping the
// per-chunk marshal/syscall overhead low for multi-GB artifacts.
const saveStreamChunkSize = 2 * 1024 * 1024
// SaveStream implements ArtifactDriver.SaveStream. If the plugin advertises
// streaming support (per GetCapabilities), the reader is streamed directly with no
// local buffering. Otherwise it falls back to buffering to a temp file and calling
// the existing unary Save, so a plugin that predates streaming keeps working.
//
// Capability is checked before reader is touched: once GetCapabilities confirmsView on GitHub (pinned to 35bff19146)
Solutions
- Decode the wrapped gRPC status (status.FromError) to see the exact code (Unavailable, DeadlineExceeded, Unimplemented, ResourceExhausted)
- Check the plugin container logs and restart state (kubectl get pods, describe)
- If Unavailable, verify the plugin is running and its unix socket path matches configuration
- For DeadlineExceeded, increase the controller's timeout or check plugin save performance
Defensive patterns
Strategy: retry
Validate before calling
// Pre-check plugin connectivity before issuing Save:
connState := driver.ConnState() // if exposed, or probe socket:
if _, err := os.Stat(socketPath); err != nil {
return fmt.Errorf("plugin socket missing: %w", err)
} Type guard
func grpcUnavailable(err error) bool {
st, ok := status.FromError(errors.Unwrap(err))
return ok && st.Code() == codes.Unavailable
} Try / catch
err := driver.Save(ctx, path, artifact)
if err != nil {
if grpcUnavailable(err) {
// backoff and retry: plugin may be restarting
return retryWithBackoff(func() error { return driver.Save(ctx, path, artifact) })
}
return err
} Prevention
- Give plugin pods liveness probes and adequate memory to survive large saves
- Pin plugin version with the controller to avoid mid-workflow restarts/redeploys
- Use the errors.Unwrap chain and status.FromError to classify the gRPC code before reacting
- Set realistic context deadlines matching artifact size
When it happens
Trigger: Calling Driver.Save (workflow/artifacts/plugin/plugin.go:205) when the plugin pod is down/restarting, the unix socket connection broke, the context deadline expired, or the plugin returns a gRPC error status.
Common situations: Plugin OOM-killed during a large artifact save, plugin redeployed mid-workflow, artifact save exceeds gRPC 4MiB message limit for huge inline metadata, or node/network issues on the unix socket.
Related errors
- plugin %s stream error: %s
- plugin %s save failed: %s
- plugin %s save stream failed to open: %w
- plugin %s save stream failed %s: %w
- plugin %s save stream failed: %w
AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03).
Data as JSON: /api/errors/a7572b22d74591f4.
Report an issue: GitHub.