juicedata/juicefs · error

failed to get the last byte: expect "o", but got %q, error:

Error message

failed to get the last byte: expect "o", but got %q, error: %s

What it means

Same 'get partial object' case in `juicefs objbench`; this fires when the ranged GET of the last byte (offset 4, size 1) of the "hello" object does not return "o". It checks that the backend can seek to the end of an object and read a tail range correctly. Failures indicate off-by-one handling of ranges at the object end or a storage error.

Source

Thrown at cmd/objbench.go:821

			return fmt.Errorf("get not existed object should failed: %s", err)
		}
		return nil
	})

	runCase("get partial object", func(blob object.ObjectStorage) error {
		br := []byte("hello")
		if err := blob.Put(ctx, key, bytes.NewReader(br)); err != nil {
			return fmt.Errorf("put object failed: %s", err)
		}
		defer blob.Delete(ctx, key) //nolint:errcheck

		// get first
		if d, e := get(blob, key, 0, 1); e != nil || d != "h" {
			return fmt.Errorf(`failed to get the first byte:, expect "h", but got %q, error: %s`, d, e)
		}
		// get last
		if d, e := get(blob, key, 4, 1); e != nil || d != "o" {
			return fmt.Errorf(`failed to get the last byte: expect "o", but got %q, error: %s`, d, e)
		}
		// get last 3
		if d, e := get(blob, key, 2, 3); e != nil || d != "llo" {
			return fmt.Errorf(`failed to get the last three bytes: expect "llo", but got %q, error: %s`, d, e)
		}
		// get middle
		if d, e := get(blob, key, 2, 2); e != nil || d != "ll" {
			return fmt.Errorf(`failed to get two bytes: expect "ll", but got %q, error: %s`, d, e)
		}
		// get the end out of range
		if d, e := get(blob, key, 4, 2); e != nil || d != "o" {
			return warning(fmt.Errorf(`failed to get object with the end out of range, expect "o", but got %q, error: %s`, d, e))
		}
		// get the off out of range
		if d, e := get(blob, key, 6, 2); e != nil || d != "" {
			return warning(fmt.Errorf(`failed to get object with the offset out of range, expect "", but got %q, error: %s`, d, e))
		}
		return nil

View on GitHub (pinned to c9a67b23e8)

Solutions

  1. Check the embedded error (%s) for the root cause (HTTP status, auth, timeout).
  2. Test tail ranges directly with `aws s3api get-object --range bytes=4-4` to isolate the backend.
  3. Fix range/limit arithmetic in the custom ObjectStorage implementation.
  4. Confirm object size with Head and ensure the test's Put completed before the Get (eventual consistency).

Example fix

// before (off-by-one in custom storage)
end := off + limit // bytes=4-5 -> 416 or wrong data
// after
end := off + limit - 1 // inclusive end, bytes=4-4 -> "o"
Defensive patterns

Strategy: try-catch

Validate before calling

// verify tail range directly:
rc, err := blob.Get(ctx, key, size-1, 1)
if err != nil { return fmt.Errorf("tail get failed: %w", err) }

Try / catch

d, e := get(blob, key, 4, 1)
if e != nil {
    var httpErr interface{ StatusCode() int }
    if errors.As(e, &httpErr) { log.Printf("HTTP status: %d", httpErr.StatusCode()) }
    log.Printf("tail byte read failed: %v", e)
}

Prevention

When it happens

Trigger: ObjectStorage.Get(key, off=4, size=1) against a 5-byte object returns wrong bytes, empty data, or an error — e.g. backends that treat `Range: bytes=4-4` incorrectly or reject end-of-object ranges.

Common situations: S3-compatible endpoints that mis-handle Range headers near EOF; MinIO/proxy misconfiguration; custom storage implementations with off-by-one limit arithmetic; eventually-consistent stores returning the pre-write object.

Related errors


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