nats-io/nats-server · error

ErrClosed

ErrClosed

Error message

archive: closed

What it means

The archive package (server/archive) defines ErrClosed = errors.New("archive: closed"). The Writer rejects all operations after Close has been called: WriteHeader (archive.go:79) and Write (archive.go:129) return ErrClosed when a.closed is set. It signals use of an already-finalized archive writer, mirroring the sentinel-error style of compress/gzip and archive/tar.

Source

Thrown at server/archive/archive.go:31

package archive

import (
	"bufio"
	"encoding/binary"
	"errors"
	"io"
)

const MagicBytes = "NATSARC1"

// maxNameLen bounds the entry name length accepted when reading an archive,
// avoiding an unbounded allocation on corrupt or malicious input. The name is
// the only variable-length field read directly from the stream.
const maxNameLen = 1 << 20

var (
	ErrClosed            = errors.New("archive: closed")
	ErrInvalidArchive    = errors.New("archive: invalid archive stream")
	ErrIncompleteEntry   = errors.New("archive: entry not fully written")
	ErrNoActiveEntry     = errors.New("archive: no active entry")
	ErrWriteTooLong      = errors.New("archive: write exceeds declared entry size")
	ErrNilHeader         = errors.New("archive: nil header")
	ErrNegativeEntrySize = errors.New("archive: negative entry size")
)

// Header describes one archive entry.
//
// On the wire each entry is the encoded header fields followed by the payload.
// HeaderSize and PayloadSize describe how that payload is split (e.g. message
// headers vs. body); the payload length is their sum and is not stored
// separately. Sequence is always encoded, with 0 meaning "unset".
type Header struct {
	Name        string
	HeaderSize  int64
	PayloadSize int64

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Restructure code so all WriteHeader/Write calls happen before Close; Close must be the final operation on the Writer.
  2. If the writer may be closed early on error, guard subsequent writes (check a closed flag or skip the write path).
  3. Compare against archive.ErrClosed with errors.Is to handle this case explicitly instead of treating it as I/O failure.
  4. Create a new Writer if you need to write another archive after closing the previous one.

Example fix

// before
def w.Close()
w.WriteHeader(hdr) // may run after Close -> ErrClosed
// after
err := writeEntries(w, hdrs)
if err == nil {
    err = w.Close()
}
if errors.Is(err, archive.ErrClosed) {
    // writer already finalized; open a new one to continue
    w = archive.NewWriter(sink)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// guard against writing to an already-closed writer
var mu sync.Mutex
closed := false

func safeWriteHeader(w *archive.Writer, hdr *archive.Header) error {
    mu.Lock()
    if closed { mu.Unlock(); return archive.ErrClosed }
    mu.Unlock()
    return w.WriteHeader(hdr)
}

Type guard

func isClosed(err error) bool { return errors.Is(err, archive.ErrClosed) }

Try / catch

if err := w.WriteHeader(hdr); err != nil {
    if errors.Is(err, archive.ErrClosed) {
        // writer finalized: open a fresh writer or abort the entry stream
        return fmt.Errorf("archive writer already closed: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling WriteHeader, Write, or Flush on an *archive.Writer after Close() has returned; closing the writer and then attempting to write another entry; double-close followed by reuse of the same Writer value.

Common situations: Code paths that close the archive on error (e.g. defer w.Close()) and then still attempt writes in a cleanup path; reusing a Writer field across retries after one attempt closed it; lifetime bugs where the writer is closed by one goroutine while another still streams entries.

Related errors


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