argoproj/argo-workflows · warning

plugin %s stream write failed: %w

Error message

plugin %s stream write failed: %w

What it means

This error occurs in the OpenStream pump goroutine when writing a received plugin chunk into the io.Pipe fails via writer.Write. Since the pipe's read side is handed to the caller, a write error almost always means the consumer closed or stopped reading the pipe (or the pipe was already closed with an error). The driver wraps the write error with the plugin name and tears down the stream.

Source

Thrown at workflow/artifacts/plugin/plugin.go:194

		for {
			resp, err := stream.Recv()
			if errors.Is(err, io.EOF) {
				break
			}
			if err != nil {
				writer.CloseWithError(fmt.Errorf("plugin %s stream receive failed: %w", d.pluginName, err))
				return
			}
			if resp.Error != "" {
				writer.CloseWithError(fmt.Errorf("plugin %s stream error: %s", d.pluginName, resp.Error))
				return
			}
			if resp.IsEnd {
				break
			}
			if len(resp.Data) > 0 {
				if _, writeErr := writer.Write(resp.Data); writeErr != nil {
					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)

View on GitHub (pinned to 35bff19146)

Solutions

  1. Check whether your code closes the returned ReadCloser before fully reading it — early Close triggers this
  2. Ensure you do not close the reader while the plugin stream is still being consumed
  3. If abandoning a stream, cancel the context passed to OpenStream so the goroutine exits cleanly
  4. Inspect the wrapped writeErr after 'stream write failed: ' for the actual pipe error (usually io.ErrClosedPipe)
Defensive patterns

Strategy: try-catch

Type guard

func isClosedPipe(err error) bool {
    return errors.Is(err, io.ErrClosedPipe) || errors.Is(err, os.ErrClosed)
}

Try / catch

reader, err := driver.OpenStream(ctx, artifact)
if err != nil { return err }
_, copyErr := io.Copy(dst, reader)
reader.Close()
if copyErr != nil {
    if isClosedPipe(copyErr) {
        // consumer closed early; ensure ctx cancel to stop the driver goroutine
        cancel()
        return nil // or handle as intentional early termination
    }
    return copyErr
}

Prevention

When it happens

Trigger: Calling Driver.OpenStream and then closing or abandoning the returned io.ReadCloser before the plugin finishes streaming, so the driver goroutine's writer.Write fails with io.ErrClosedPipe.

Common situations: Caller uses io.Copy with a context timeout and gives up early; artifact loading code errors elsewhere and closes the reader; double-close of the ReadCloser; process shutting down mid-stream.

Related errors


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