juicedata/juicefs · error

capture read position %s: %w

Error message

capture read position %s: %w

What it means

storjClient.Put first checks whether the input is an io.ReadSeeker so it can retry uploads by rewinding. Capturing the current position with rs.Seek(0, io.SeekCurrent) failing means the reader advertised seekability but the underlying Seek operation failed, so a resumable/retried upload cannot be safely started.

Source

Thrown at pkg/object/storj.go:151

		download, e = s.project.DownloadObject(ctx, s.bucket, key, opts)
		return e
	})
	if err != nil {
		return nil, err
	}
	return download, nil
}

func (s *storjClient) Put(ctx context.Context, key string, in io.Reader, getters ...AttrGetter) error {
	// Retry is only safe when the reader is seekable: rewinding lets us restart
	// the upload from the beginning after a rate-limit response.
	rs, seekable := in.(io.ReadSeeker)
	if !seekable {
		return s.putOnce(ctx, key, in)
	}
	startPos, err := rs.Seek(0, io.SeekCurrent)
	if err != nil {
		return fmt.Errorf("capture read position %s: %w", key, err)
	}
	return storjBackoff(ctx, func() error {
		if _, err := rs.Seek(startPos, io.SeekStart); err != nil {
			return err
		}
		return s.putOnce(ctx, key, rs)
	})
}

func (s *storjClient) putOnce(ctx context.Context, key string, in io.Reader) error {
	upload, err := s.project.UploadObject(ctx, s.bucket, key, nil)
	if err != nil {
		return fmt.Errorf("begin upload %s: %w", key, err)
	}
	if _, err = io.Copy(upload, in); err != nil {
		_ = upload.Abort()
		return fmt.Errorf("upload %s: %w", key, err)
	}

View on GitHub (pinned to c9a67b23e8)

Solutions

  1. Supply a fully seekable source (real file or bytes.Reader) instead of a pipe/stdin.
  2. If the input is not seekable, ensure it reaches the path that uses putOnce (single-shot upload) — don't wrap it in a fake ReadSeeker.
  3. If using a custom reader, implement Seek correctly or remove the io.ReadSeeker interface so Put falls back to putOnce.

Example fix

// before
in := bufio.NewReader(os.Stdin) // not truly seekable
client.Put(ctx, key, in)
// after
in, err := os.Open(tmpFile)
if err != nil { return err }
defer in.Close()
client.Put(ctx, key, in) // real io.ReadSeeker
Defensive patterns

Strategy: type-guard

Validate before calling

if rs, ok := in.(io.ReadSeeker); ok {
    if _, err := rs.Seek(0, io.SeekCurrent); err != nil {
        return fmt.Errorf("input not truly seekable: %v", err)
    }
}

Type guard

func isSeekable(r io.Reader) bool {
    rs, ok := r.(io.ReadSeeker)
    if !ok { return false }
    _, err := rs.Seek(0, io.SeekCurrent)
    return err == nil
}

Try / catch

if err := client.Put(ctx, key, in); err != nil && strings.Contains(err.Error(), "capture read position") {
    // fall back to staging the data into a temp file, then retry Put
}

Prevention

When it happens

Trigger: Passing a reader that implements io.ReadSeeker but whose Seek returns an error — e.g. an *os.File on a pipe/non-seekable fd, a closed file, or a custom reader whose Seek is unimplemented/fails at runtime, then calling Put.

Common situations: Piping from stdin or a process substitution into a JuiceFS command whose upload path expects seekable input; a wrapped reader (bufio/compression) that falsely claims ReadSeeker support.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of juicedata/juicefs@c9a67b23e8 (2026-09-06). Data as JSON: /api/errors/d40fa3907205b4ee. Report an issue: GitHub.