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
- Stop writing after Close; emit all data before calling Close.
- Ensure Close is called exactly once (use sync.Once or remove duplicate close paths).
- 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
- Call Close exactly once, in one place (e.g. a single defer).
- Never reuse an armored writer for a second stream; create a new one.
- Buffer all payload before closing; never write after initiating shutdown.
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
- trailing data after armored file
- too much trailing whitespace
- too much leading whitespace
- empty line in armored data
- column limit exceeded
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/ad18f03d7a74afff.
Report an issue: GitHub.