{"record":{"id":"5158c23a5eb81e53","repo":"nats-io/nats-server","slug":"errinvalidarchive","errorCode":"ErrInvalidArchive","errorMessage":"archive: invalid archive stream","messagePattern":"archive: invalid archive stream","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/archive/archive.go","lineNumber":32,"sourceCode":"package archive\n\nimport (\n\t\"bufio\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n)\n\nconst MagicBytes = \"NATSARC1\"\n\n// maxNameLen bounds the entry name length accepted when reading an archive,\n// avoiding an unbounded allocation on corrupt or malicious input. The name is\n// the only variable-length field read directly from the stream.\nconst maxNameLen = 1 << 20\n\nvar (\n\tErrClosed            = errors.New(\"archive: closed\")\n\tErrInvalidArchive    = errors.New(\"archive: invalid archive stream\")\n\tErrIncompleteEntry   = errors.New(\"archive: entry not fully written\")\n\tErrNoActiveEntry     = errors.New(\"archive: no active entry\")\n\tErrWriteTooLong      = errors.New(\"archive: write exceeds declared entry size\")\n\tErrNilHeader         = errors.New(\"archive: nil header\")\n\tErrNegativeEntrySize = errors.New(\"archive: negative entry size\")\n)\n\n// Header describes one archive entry.\n//\n// On the wire each entry is the encoded header fields followed by the payload.\n// HeaderSize and PayloadSize describe how that payload is split (e.g. message\n// headers vs. body); the payload length is their sum and is not stored\n// separately. Sequence is always encoded, with 0 meaning \"unset\".\ntype Header struct {\n\tName        string\n\tHeaderSize  int64\n\tPayloadSize int64\n\tTimestamp   int64","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/nats-io/nats-server/blob/3a66a489d262bf89b71a71c955c94920394532f3/server/archive/archive.go#L14-L50","documentation":"ErrInvalidArchive means the archive stream is corrupt, truncated, or not a NATS archive at all. The Reader throws it when the magic bytes don't match NATSARC1, when a header field hits EOF mid-entry (truncation), when a varint is malformed, when declared sizes overflow or exceed maxNameLen, when a payload read comes up short, or when discarding the current entry hits EOF early. It is sticky: once set on Reader.err, all subsequent Next/Read calls fail with it.","triggerScenarios":"Reader.Next: magic mismatch (non-archive or wrong-version input); clean EOF on any header field after nameLen (truncated entry header); nameLen > maxNameLen (1 MiB); hdrSize/plSize negative after int64 conversion or total overflow; io.ReadFull short read of the entry name. Reader.Read: io.ReadFull returns any error mid-payload. Reader.discardCurrent (called via Next): EOF while skipping remaining payload bytes.","commonSituations":"Reading a file that isn't a NATS archive (wrong file passed, empty or zero-byte file, text/JSON logs); a truncated download or partial flush from a crashed producer; an archive written by a different/older format version; feeding the reader a compressed or re-encoded stream.","solutions":["Verify the input is a complete NATSARC1 archive: check the first 8 bytes and file completeness before opening a Reader.","Re-acquire or re-export the archive from the source; a truncated archive cannot be repaired in place — recover entries up to the corruption point by treating ErrInvalidArchive as end-of-valid-data.","Check the writer side: ensure the producer called WriteHeader and wrote exactly HeaderSize+PayloadSize bytes per entry, then Close/Flush'd the stream.","Confirm the archive format version matches this library (MagicBytes \"NATSARC1\"); regenerate with a compatible writer.","If reading from a network stream, ensure the connection delivered the full bytes (no proxy truncation); retry with a fresh full copy."],"exampleFix":"// before: reading an unknown file blindly\nr := archive.NewReader(f)\nfor {\n  h, err := r.Next()\n  if err != nil { log.Fatal(err) } // dies on ErrInvalidArchive mid-loop\n}\n// after: detect and treat corruption as end-of-valid-data\nr := archive.NewReader(f)\nfor {\n  h, err := r.Next()\n  if errors.Is(err, archive.ErrInvalidArchive) {\n    log.Printf(\"archive corrupt/truncated after entry %d\", i)\n    break\n  }\n  if err != nil { log.Fatal(err) }\n  i++\n}","handlingStrategy":"type-guard","validationCode":"// before reading\ninfo, err := f.Stat()\nif err != nil { return err }\nif info.Size() < len(archive.MagicBytes) { return errors.New(\"file too small to be a NATS archive\") }\nmagic := make([]byte, len(archive.MagicBytes))\nif _, err := io.ReadFull(f, magic); err != nil { return err }\nif string(magic) != archive.MagicBytes { return errors.New(\"not a NATSARC1 archive\") }\nf.Seek(0, io.SeekStart)","typeGuard":"func IsInvalidArchive(err error) bool { return errors.Is(err, archive.ErrInvalidArchive) }","tryCatchPattern":"h, err := r.Next()\nswitch {\ncase errors.Is(err, io.EOF):\n    return nil // clean end of stream\ncase errors.Is(err, archive.ErrInvalidArchive):\n    return fmt.Errorf(\"archive corrupt after entry %d: %w\", count, err) // stop; state is poisoned\ncase err != nil:\n    return err\n}","preventionTips":["Treat ErrInvalidArchive as terminal: the Reader caches it in err, so never retry Next/Read on the same Reader.","Verify the 8-byte NATSARC1 magic and file completeness (size, checksum) before parsing.","Recover entries incrementally: record each successfully read entry's offset so truncated archives can be partially salvaged.","Ensure producers Flush/Close the underlying writer so no trailing bytes are lost."],"tags":["go","archive","corrupt-data","io"],"backgroundTag":"corrupt-archive-stream","analyzedSha":"3a66a489d262bf89b71a71c955c94920394532f3","analyzedAt":"2026-09-02T04:41:54.247Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}