restic/restic · warning

invalid start of string: %q

Error message

invalid start of string: %q

What it means

ID.UnmarshalJSON checked the total length (66 bytes) but the first byte is not a '"', so the value is not a JSON string. Per the json.Unmarshaler contract restic assumes it receives a well-formed JSON value, so this only happens when the bytes are malformed: leading whitespace plus a quote elsewhere, raw hex padded to 66 bytes, or binary garbage from a corrupted file. It signals input that never was valid JSON at that position.

Source

Thrown at internal/restic/id.go:87

func (id ID) MarshalJSON() ([]byte, error) {
	buf := make([]byte, 2+hex.EncodedLen(len(id)))

	buf[0] = '"'
	hex.Encode(buf[1:], id[:])
	buf[len(buf)-1] = '"'

	return buf, nil
}

// UnmarshalJSON parses the JSON-encoded data and stores the result in id.
func (id *ID) UnmarshalJSON(b []byte) error {
	// check string length
	if len(b) != len(`""`)+hex.EncodedLen(idSize) {
		return fmt.Errorf("invalid length for ID: %q", b)
	}

	if b[0] != '"' {
		return fmt.Errorf("invalid start of string: %q", b[0])
	}

	// Strip JSON string delimiters. The json.Unmarshaler contract says we get
	// a valid JSON value, so we don't need to check that b[len(b)-1] == '"'.
	b = b[1 : len(b)-1]

	_, err := hex.Decode(id[:], b)
	if err != nil {
		return fmt.Errorf("invalid ID: %s", err)
	}

	return nil
}

// IDFromHash returns the ID for the hash.
func IDFromHash(hash []byte) (id ID) {
	if len(hash) != idSize {
		panic("invalid hash type, not enough/too many bytes")

View on GitHub (pinned to a80be1478a)

Solutions

  1. Use encoding/json APIs (json.Unmarshal, json.Decoder) to decode the enclosing structure instead of slicing raw bytes
  2. For repository files, run restic check and rebuild-index to regenerate damaged indexes
  3. Log the offending bytes to find what prefix is polluting the value
Defensive patterns

Strategy: validation

Validate before calling

if len(raw) == 66 && raw[0] != '"' {
    return fmt.Errorf("ID field is not a JSON string (missing quote): %q", raw)
}

Type guard

func isQuotedJSONString(raw []byte) bool { return len(raw) >= 2 && raw[0] == '"' }

Prevention

When it happens

Trigger: Feeding a json.Decoder output offset wrong (decoding from the middle of a document); corrupted index/snapshot files where bytes shifted; manually constructed []byte that includes a prefix (e.g., 'id:' or a space) before the quoted ID.

Common situations: Custom parsers slicing JSON buffers by assumption instead of using json.Decoder token flow; files corrupted by truncated writes or bad sync jobs.

Related errors


AI-assisted analysis of restic/restic@a80be1478a (2026-08-15). Data as JSON: /api/errors/fc47ef73b2419ced. Report an issue: GitHub.