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
- Re-take the stream backup with the nats-server snapshot path (nats stream backup / StreamSnapshot) so the archive starts with state.json
- Verify the archive's first entry: open with s2 reader + tar reader and check hdr.Name == "state.json" before calling the API
- If the file was truncated during transfer, re-download/re-copy it and compare checksums with the source
- 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
- Only restore archives produced by nats-server StreamSnapshot / `nats stream backup`
- Never reorder or rename entries when repacking snapshot archives
- Verify the first tar entry is state.json before calling the restore API
- Keep backup and restore nats-server versions compatible
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
- expected consumer, found %q
- flush of last block failed: %w
- restore for stream '%s > %s' requires reply subject for each
- restore for stream '%s > %s' received short chunk
- 10062
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/a3b48042f239ad14.
Report an issue: GitHub.