Repository navigation
File(...).Post(...) fails with "file already closed" #250
Description
I'm new to go and spent a few hours on this but Claude did most of the code.
Posting a file with script.File(path).Post(url) fails with file already closed, while the same content sent via Echo works. I think the cause is that the file gets closed twice. The tests below reproduce the failure on master.
Reproduction
package main
import (
"log"
"os"
"github.com/bitfield/script"
)
func main() {
// os.Args[1] == "./.tmpfile", os.Args[2] == "http://localhost:8080/post"
_, err := script.File(os.Args[1]).Post(os.Args[2]).Stdout()
if err != nil {
log.Println(err)
}
}Output:
Post "http://localhost:8080/post": net/http: HTTP/1.x transport connection broken: close .\.tmpfile: file already closed
The same failure appears on Linux (from a regression test, see below):
Post "http://127.0.0.1:39845": net/http: HTTP/1.x transport connection broken: close /tmp/TestPostFileDoesNotFailOnDoubleClose2765656784/001/in.txt: file already closed
Workaround
Reading the file myself and using Echo works:
s, err := os.ReadFile(os.Args[1])
if err != nil {
log.Println(err)
}
_, err = script.Echo(string(s)).Post(os.Args[2]).Stdout()
if err != nil {
log.Println(err)
}Some Claude slop:
Post passes p.Reader (a ReadAutoCloser) to http.NewRequestWithContext as the request body. That body gets closed twice:
ReadAutoCloser.Readcloses the underlying reader when it reachesio.EOF.net/httpthen callsBody.Close()itself, and returns any error from it.
For Echo, the reader is a strings.Reader wrapped in io.NopCloser, so a second close does nothing. For File, it's an *os.File, and the second Close returns os.ErrClosed, which fails the request.
I had another fix that switched everything to pointers, but that seemed too invasive. This one keeps ReadAutoCloser and Pipe unchanged, with no change to exported types or signatures.
This is the code I used to fix it
--- a/script.go
+++ b/script.go
@@ -1213,7 +1213,19 @@
// This can never happen, but just in case it does...
panic("internal error: type assertion to io.ReadCloser failed")
}
- return ReadAutoCloser{rc}
+ return ReadAutoCloser{&onceCloser{ReadCloser: rc}}
+}
+
+// onceCloser makes Close safe to call more than once.
+type onceCloser struct {
+ io.ReadCloser
+ once sync.Once
+ err error
+}
+
+func (c *onceCloser) Close() error {
+ c.once.Do(func() { c.err = c.ReadCloser.Close() })
+ return c.err
}
// Close closes ra's reader, returning any resulting error.Regression tests added to script_test.go
func TestPostFileDoesNotFailOnDoubleClose(t *testing.T) {
t.Parallel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, r.Body)
}))
defer ts.Close()
path := filepath.Join(t.TempDir(), "in.txt")
if err := os.WriteFile(path, []byte("hello"), 0o600); err != nil {
t.Fatal(err)
}
got, err := script.File(path).Post(ts.URL).String()
if err != nil {
t.Fatal(err)
}
if got != "hello" {
t.Errorf("want %q, got %q", "hello", got)
}
}
func TestReadAutoCloserCloseAfterEOFReturnsNil(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "x")
if err := os.WriteFile(path, []byte("hello"), 0o600); err != nil {
t.Fatal(err)
}
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
rac := script.NewReadAutoCloser(f)
// Reading to EOF closes the file automatically.
if _, err := io.ReadAll(rac); err != nil {
t.Fatal(err)
}
// Closing again, as net/http does with a request body, should not fail.
if err := rac.Close(); err != nil {
t.Fatalf("Close after EOF: %v", err)
}
if err := rac.Close(); err != nil {
t.Fatalf("repeated Close: %v", err)
}
}The new tests fail on current master with "file already closed" and pass with the change. I used go build ./..., go vet ./..., and go test -race -count=50 ./... successfully with new code/tests.
Environment
- Go version: go version go1.27.0 windows/amd64, go1.27.1-X:nodwarf5 linux/amd64
- OS: Windows and archlinux docker container
- script version: v0.25.1
I use this package a lot, I fixed it just to learn. I figure the bug report is probably enough, maybe the code might help with the fix or point out the issue.
Activity
bitfield commentedon Oct 7, 2026
Great, thanks @Steaksawce! It took four years for anyone to spot that bug.
Steaksawce commentedon Oct 7, 2026
@bitfield ;)