{"record":{"id":"ad18f03d7a74afff","repo":"FiloSottile/age","slug":"armoredwriter-already-closed","errorCode":null,"errorMessage":"ArmoredWriter already closed","messagePattern":"ArmoredWriter already closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"armor/armor.go","lineNumber":47,"sourceCode":"\tstarted, closed bool\n\tencoder         *format.WrappedBase64Encoder\n\tdst             io.Writer\n}\n\nfunc (a *armoredWriter) writeHeader() error {\n\tif a.started {\n\t\treturn nil\n\t}\n\tif _, err := io.WriteString(a.dst, Header+\"\\n\"); err != nil {\n\t\treturn err\n\t}\n\ta.started = true\n\treturn nil\n}\n\nfunc (a *armoredWriter) Write(p []byte) (int, error) {\n\tif a.closed {\n\t\treturn 0, errors.New(\"ArmoredWriter already closed\")\n\t}\n\tif err := a.writeHeader(); err != nil {\n\t\treturn 0, err\n\t}\n\treturn a.encoder.Write(p)\n}\n\nfunc (a *armoredWriter) Close() error {\n\tif a.closed {\n\t\treturn errors.New(\"ArmoredWriter already closed\")\n\t}\n\ta.closed = true\n\tif err := a.writeHeader(); err != nil {\n\t\treturn err\n\t}\n\tif err := a.encoder.Close(); err != nil {\n\t\treturn err\n\t}","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/armor/armor.go#L29-L65","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nw := armor.NewWriter(out)\nio.WriteString(w, data)\nw.Close()\nio.WriteString(w, \"trailer\") // error: write after close\n// after\nw := armor.NewWriter(out)\nio.WriteString(w, data)\nw.Close() // no writes after Close","handlingStrategy":"try-catch","validationCode":"// Track close state around the armored writer\ntype guardedWriter struct {\n    w      io.Writer\n    mu     sync.Mutex\n    closed bool\n}\nfunc (g *guardedWriter) Write(p []byte) (int, error) {\n    g.mu.Lock()\n    defer g.mu.Unlock()\n    if g.closed {\n        return 0, io.ErrClosedPipe\n    }\n    return g.w.Write(p)\n}","typeGuard":null,"tryCatchPattern":"n, err := armoredW.Write(p)\nif err != nil {\n    if err.Error() == \"ArmoredWriter already closed\" {\n        // bug: write after close — fix control flow, do not retry\n        return fmt.Errorf(\"internal: write after armor close: %w\", err)\n    }\n    return err\n}","preventionTips":["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."],"tags":["go","age","armor","writer","use-after-close"],"backgroundTag":"write-after-close","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}