FiloSottile/age · error

ArmoredWriter already closed

Error message

ArmoredWriter already closed

What it means

armor.ArmoredWriter is a one-shot writer: once Close is called, the armor footer has been emitted and the underlying base64 encoder is finished. Any further Write call after Close returns this sentinel error instead of silently producing invalid armored output.

Source

Thrown at armor/armor.go:47

	started, closed bool
	encoder         *format.WrappedBase64Encoder
	dst             io.Writer
}

func (a *armoredWriter) writeHeader() error {
	if a.started {
		return nil
	}
	if _, err := io.WriteString(a.dst, Header+"\n"); err != nil {
		return err
	}
	a.started = true
	return nil
}

func (a *armoredWriter) Write(p []byte) (int, error) {
	if a.closed {
		return 0, errors.New("ArmoredWriter already closed")
	}
	if err := a.writeHeader(); err != nil {
		return 0, err
	}
	return a.encoder.Write(p)
}

func (a *armoredWriter) Close() error {
	if a.closed {
		return errors.New("ArmoredWriter already closed")
	}
	a.closed = true
	if err := a.writeHeader(); err != nil {
		return err
	}
	if err := a.encoder.Close(); err != nil {
		return err
	}

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Stop writing after Close; emit all data before calling Close.
  2. Ensure Close is called exactly once (use sync.Once or remove duplicate close paths).
  3. Create a new armor.ArmoredWriter (armor.NewWriter) for each separate armored stream instead of reusing a closed one.

Example fix

// before
w := armor.NewWriter(out)
io.WriteString(w, data)
w.Close()
io.WriteString(w, "trailer") // error: write after close
// after
w := armor.NewWriter(out)
io.WriteString(w, data)
w.Close() // no writes after Close
Defensive patterns

Strategy: try-catch

Validate before calling

// Track close state around the armored writer
type guardedWriter struct {
    w      io.Writer
    mu     sync.Mutex
    closed bool
}
func (g *guardedWriter) Write(p []byte) (int, error) {
    g.mu.Lock()
    defer g.mu.Unlock()
    if g.closed {
        return 0, io.ErrClosedPipe
    }
    return g.w.Write(p)
}

Try / catch

n, err := armoredW.Write(p)
if err != nil {
    if err.Error() == "ArmoredWriter already closed" {
        // bug: write after close — fix control flow, do not retry
        return fmt.Errorf("internal: write after armor close: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Write on the io.Writer returned by armor.NewWriter after having called Close on it — e.g. writing a trailer after close, or reusing the writer for a second payload.

Common situations: Wrapping the armored writer inside an abstraction whose flush/close path writes trailing data, calling both a deferred Close and an explicit Close followed by more writes, or reusing a closed armored writer for multiple files.

Related errors


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/ad18f03d7a74afff. Report an issue: GitHub.