nats-io/nats-server · error

expected state.json first

Error message

expected state.json first

What it means

RestoreStreamV2 restores a JetStream stream from an s2-compressed tar-like archive. The very first archive entry must be 'state.json' holding the stream's StreamState snapshot; if the first entry's header name differs, the server aborts the restore because it cannot know the stream's sequence/consumer layout before writing any messages. This guards against restoring a corrupted, truncated, or non-snapshot archive.

Source

Thrown at server/stream_backup.go:260

	if err = writeGeneric(_EMPTY_, 0, 0, 0, 0, nil); err != nil {
		errCh <- err
	}
}

// RestoreStreamSnapshotV2 will restore a stream from a snapshot.
func (a *Account) RestoreStreamV2(ncfg *StreamConfig, r io.Reader) (retMset *stream, retErr error) {
	dec := s2.NewReader(r)
	tr := archive.NewReader(dec)

	var nstate StreamState

	// Load the stream state.
	hdr, err := tr.Next()
	if err != nil {
		return nil, err
	}
	if hdr.Name != "state.json" {
		return nil, fmt.Errorf("expected state.json first")
	}
	state, err := io.ReadAll(tr)
	if err != nil {
		return nil, fmt.Errorf("expected state.json contents")
	}
	if err := json.Unmarshal(state, &nstate); err != nil {
		return nil, fmt.Errorf("error in state.json: %w", err)
	}

	s, jsa, err := a.checkForJetStream()
	if err != nil {
		return nil, err
	}
	js := jsa.js
	if js == nil {
		return nil, NewJSNotEnabledForAccountError()
	}
	if _, err := a.lookupStream(ncfg.Name); err == nil {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Re-take the stream backup with the nats-server snapshot path (nats stream backup / StreamSnapshot) so the archive starts with state.json
  2. Verify the archive's first entry: open with s2 reader + tar reader and check hdr.Name == "state.json" before calling the API
  3. If the file was truncated during transfer, re-download/re-copy it and compare checksums with the source
  4. If using a custom writer, ensure writeGeneric's first call writes the 'state.json' entry before consumers/messages, matching the snapshot layout

Example fix

// before: restoring an arbitrary tar file
f, _ := os.Open("stream.tar")
mset, err := acc.RestoreStreamV2(cfg, f)
// after: verify it is an s2 snapshot archive whose first entry is state.json
f, _ := os.Open("stream.tar.bak")
zr := s2.NewReader(f)
tr := archive.NewReader(zr)
hdr, err := tr.Next()
if err != nil || hdr.Name != "state.json" {
    return fmt.Errorf("not a valid stream snapshot archive")
}
f.Seek(0, 0)
mset, err := acc.RestoreStreamV2(cfg, f)
Defensive patterns

Strategy: validation

Validate before calling

func isValidSnapshot(r io.ReadSeeker) bool {
    zr := s2.NewReader(r)
    tr := archive.NewReader(zr)
    hdr, err := tr.Next()
    ok := err == nil && hdr != nil && hdr.Name == "state.json"
    r.Seek(0, 0)
    return ok
}

Try / catch

mset, err := acc.RestoreStreamV2(cfg, r)
if err != nil {
    if strings.Contains(err.Error(), "expected state.json first") {
        // treat as invalid archive: re-snapshot the source stream
    }
    return err
}

Prevention

When it happens

Trigger: Calling Account.RestoreStreamV2 (or the JS API $JS.API.STREAM.RESTORE subject, handled by processStreamRestore) with a reader whose decompressed archive's first tar entry is not named 'state.json' — e.g. an archive created by a different tool, hand-built tar, a v1-format snapshot, or an archive reordered/truncated so the header read lands on the wrong entry.

Common situations: Re-wrapping or post-processing a backup file with tar/zip tools that renamed or reordered entries; restoring a file produced by another JetStream version or by a custom exporter; feeding an empty/corrupt or partially-downloaded snapshot file; pointing the restore at a plain (non-s2, non-archive) file.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/a3b48042f239ad14. Report an issue: GitHub.