From c422f7f52920f366728e32baf5c146e72fbb9ecf Mon Sep 17 00:00:00 2001 From: John Arundel Date: Wed, 7 Oct 2026 16:53:01 +0100 Subject: [PATCH] readautocloser: only close once (fixes #250) --- README.md | 1 + script.go | 14 +++++++++++++- script_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7074a87..b1c9eed 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,7 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext | Version | New | | ----------- | ------- | +| 0.25.2 | Fixed bug where [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) would error on posting a file ([#250](https://github.com/bitfield/script/issues/250))| | 0.25.1 | [`Unique`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Unique) | | 0.25.0 | [`ExecCommand`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ExecCommand) / [`Shell`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Shell) supersede `Exec` (thanks [Dhanalakshmi-D04](https://github.com/Dhanalakshmi-D04)) | | | [`WithContext`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithContext) (thanks [billvamva](https://github.com/billvamva)) | diff --git a/script.go b/script.go index 653a941..c72a6d9 100644 --- a/script.go +++ b/script.go @@ -1203,6 +1203,18 @@ type ReadAutoCloser struct { r io.ReadCloser } +// 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 +} + // NewReadAutoCloser returns a [ReadAutoCloser] wrapping the reader r. func NewReadAutoCloser(r io.Reader) ReadAutoCloser { if _, ok := r.(io.Closer); !ok { @@ -1213,7 +1225,7 @@ func NewReadAutoCloser(r io.Reader) ReadAutoCloser { // 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}} } // Close closes ra's reader, returning any resulting error. diff --git a/script_test.go b/script_test.go index 598a111..9b96525 100644 --- a/script_test.go +++ b/script_test.go @@ -1029,6 +1029,18 @@ func TestPostPostsToGivenURLUsingPipeAsRequestBody(t *testing.T) { } } +func TestPostDoesNotErrorWhenFileAutoClosed(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() + _, err := script.File("testdata/hello.txt").Post(ts.URL).String() + if err != nil { + t.Fatal(err) + } +} + func TestRejectRegexp_DropsMatchingLinesFromInput(t *testing.T) { t.Parallel() input := "hello world" @@ -1477,6 +1489,27 @@ func TestReadAutoCloser_ReadsAllDataFromSourceAndClosesItAutomatically(t *testin } } +type mockCloser struct { + io.Reader + closed bool + t *testing.T +} + +func (mc *mockCloser) Close() error { + if mc.closed { + mc.t.Fatal("source closed twice") + } + mc.closed = true + return nil +} + +func TestReadAutoCloser_OnlyClosesSourceOnce(t *testing.T) { + t.Parallel() + acr := script.NewReadAutoCloser(&mockCloser{t: t}) + acr.Close() + acr.Close() +} + func TestSliceProducesElementsOfSpecifiedSliceOnePerLine(t *testing.T) { t.Parallel() want := "1\n2\n3\n"