JuliusBrussee/caveman · error

encode report

Error message

encode report: %w

What it means

After writing payload bytes, emitCompressResult marshals the compressReport (Result + error code) to JSON for stderr. If json.Marshal fails, the error is wrapped as "encode report: %w". This is nearly impossible with plain data and signals an internal encoding problem.

Solutions

  1. Check recently added fields on compressReport or engine.Result for non-JSON-serializable types.
  2. Add or fix MarshalJSON on the offending type, or drop/redact the unserializable field.
  3. Marshal the report in a unit test to catch regressions before runtime.

Example fix

// before
type compressReport struct {
    Notify func() `json:"notify"` // unsupported
}
// after
type compressReport struct {
    Result engine.Result
    Error  string
}
Defensive patterns

Strategy: try-catch

Validate before calling

// smoke-test report serialization in CI
if _, err := json.Marshal(compressReport{Result: engine.Result{}, Error: "cave_ccr_unavailable"}); err != nil { t.Fatal(err) }

Try / catch

if _, err := json.Marshal(report); err != nil {
    return fmt.Errorf("encode report: %w", err) // surface, then inspect %v of wrapped cause
}

Prevention

When it happens

Trigger: json.Marshal(compressReport{Result: res, Error: code}) returns an error — e.g. a field in Result/engine.Result cannot be serialized (unsupported type such as a channel or func slipped into the struct).

Common situations: A developer adds a field of an unsupported type (chan, func, or cyclic structure) to compressReport or the embedded engine.Result; custom MarshalJSON panics/returns error on unusual token counts.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


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

Appendix: source

Thrown at engine/cmd/caveman-engine/main.go:138

// the original bytes. Any contradictory result fails rather than guessing.
func emitCompressResult(input []byte, res engine.Result, compressErr error, stdout, stderr io.Writer) error {
	code := ""
	if compressErr != nil {
		if !bytes.Equal(res.Output, input) || !res.PassedThrough() || res.TokensAfter != res.TokensBefore {
			return fmt.Errorf("unsafe recovery failure result: %w", compressErr)
		}
		code = "cave_ccr_unavailable"
		if errors.Is(compressErr, ccr.ErrBudgetExceeded) {
			code = "cave_ccr_budget_exceeded"
		}
	}
	if _, err := stdout.Write(res.Output); err != nil {
		return fmt.Errorf("write stdout: %w", err)
	}
	// Report stays on stderr so stdout remains clean payload bytes.
	report, err := json.Marshal(compressReport{Result: res, Error: code})
	if err != nil {
		return fmt.Errorf("encode report: %w", err)
	}
	if _, err := fmt.Fprintln(stderr, string(report)); err != nil {
		return fmt.Errorf("write report: %w", err)
	}
	return nil
}

func runDetect() {
	input, err := readBoundedInput(os.Stdin, maxStdinBytes)
	if err != nil {
		fatal("read stdin: %v", err)
	}
	// Detection needs no store.
	eng := engine.New(nil, nil)
	fmt.Println(eng.Detect(input))
}

func runRetrieve(args []string) {

View on GitHub (pinned to 3ee70a1026)