JuliusBrussee/caveman · error

ccr: typed object byte lengths cannot be negative

Error message

ccr: typed object byte lengths cannot be negative

What it means

prepareObject rejects typed Objects whose OriginalByteLength or StoredByteLength are negative. These are only defaulted (to len(Data)) when zero; negative values are treated as invalid metadata and the put fails closed rather than storing inconsistent size accounting.

Solutions

  1. Leave both length fields zero so prepareObject defaults them to len(obj.Data).
  2. Clamp computed lengths to >= 0 before calling PutObject.
  3. Validate any unmarshaled byteLength values are non-negative at deserialization time.
  4. Fix the size computation that produced the negative value (usually a subtraction underflow).

Example fix

// before
obj.StoredByteLength = len(raw) - len(comp) // may go negative

// after
if n := len(raw) - len(comp); n >= 0 {
    obj.StoredByteLength = n
} // else leave 0 to let prepareObject default it
Defensive patterns

Strategy: validation

Validate before calling

if obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {
    return errors.New("negative byte lengths")
}

Type guard

func saneLengths(o ccr.Object) bool {
    return o.OriginalByteLength >= 0 && o.StoredByteLength >= 0
}

Try / catch

if err := clampLengths(&obj); err != nil { return err }
if _, err := store.PutObject(obj); err != nil {
    if strings.Contains(err.Error(), "cannot be negative") {
        return fmt.Errorf("size accounting bug: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: PutObject with an Object built from int arithmetic that can go negative (e.g. length computed from a subtraction, or a negative value unmarshaled from JSON), while len(Data) > 0 so the zero-defaulting never applies.

Common situations: Size fields populated by buggy calculations (stored = original - compressed); JSON payloads with negative byteLength accepted upstream; integer underflow when computing compressed savings.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/673f1be4f0c2eccb. Report an issue: GitHub.

Appendix: source

Thrown at engine/ccr/store.go:143

		obj.CreatedAt = time.Now().UTC()
	} else {
		obj.CreatedAt = obj.CreatedAt.UTC()
	}
	dataSum := sha256.Sum256(obj.Data)
	computedHash := "sha256:" + hex.EncodeToString(dataSum[:])
	if obj.ContentHash == "" {
		obj.ContentHash = computedHash
	} else if obj.ContentHash != computedHash {
		return Object{}, errors.New("ccr: typed object content_hash does not match data")
	}
	if obj.OriginalByteLength == 0 {
		obj.OriginalByteLength = len(obj.Data)
	}
	if obj.StoredByteLength == 0 {
		obj.StoredByteLength = len(obj.Data)
	}
	if obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {
		return Object{}, errors.New("ccr: typed object byte lengths cannot be negative")
	}
	if obj.ID == "" {
		identity := strings.Join([]string{string(obj.Type), obj.SessionID, obj.Source, obj.RepositoryState, obj.ContentHash}, "\x00")
		sum := sha256.Sum256([]byte(identity))
		obj.ID = "ccr_obj_" + hex.EncodeToString(sum[:16])
	}
	obj.Dependencies = append([]string(nil), obj.Dependencies...)
	obj.Data = bytes.Clone(obj.Data)
	return obj, nil
}

// sameImmutableObject compares fields PutObject promises never to change.
// CreatedAt is assigned at first storage, while currentness and lifecycle have
// explicit mutation methods, so repeat puts intentionally do not compare them.
func sameImmutableObject(left, right Object) bool {
	return left.ID == right.ID &&
		left.Type == right.Type &&
		left.ContentHash == right.ContentHash &&

View on GitHub (pinned to 3ee70a1026)