Skip to content

File(...).Post(...) fails with "file already closed" #250

Closed

Description

@Steaksawce

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:

  1. ReadAutoCloser.Read closes the underlying reader when it reaches io.EOF.
  2. net/http then calls Body.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

  1. bitfield commented on Oct 7, 2026

    @bitfield
    Owner

    Great, thanks @Steaksawce! It took four years for anyone to spot that bug.

  2. Steaksawce commented on Oct 7, 2026

    @Steaksawce
    Author
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions