Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Bug Fixes:

- fix(kvstoreentry): close directory upload files and rewind before retrying ([#1942](https://github.com/fastly/cli/issues/1942))
- fix(compute/serve): provide default `override_host` to Viceroy via temporary manifest and handle IPv6 hostnames ([#1940](https://github.com/fastly/cli/pull/1940))

### Enhancements:
Expand Down
12 changes: 12 additions & 0 deletions pkg/commands/kvstoreentry/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ func (c *CreateCommand) ProcessDir(in io.Reader, out io.Writer) error {
mu.Unlock()
return
}
defer func() {
_ = f.Close()
}()

lr, err := fastly.FileLengthReader(f)
if err != nil {
Expand Down Expand Up @@ -317,6 +320,15 @@ func (c *CreateCommand) ProcessDir(in io.Reader, out io.Writer) error {
// NOTE: you can't type assert the error as it's not exported.
// https://github.com/golang/go/issues/54173
if strings.Contains(err.Error(), "net/http: cannot rewind body after connection loss") {
if _, seekErr := f.Seek(0, io.SeekStart); seekErr != nil {
mu.Lock()
processingErrors = append(processingErrors, ProcessErr{
File: filePath,
Err: seekErr,
})
mu.Unlock()
return
}
err = insertKey(opts)
if err == nil {
return
Expand Down
96 changes: 96 additions & 0 deletions pkg/commands/kvstoreentry/create_processdir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package kvstoreentry

import (
"context"
"errors"
"io"
"os"
"path/filepath"
"testing"

"github.com/fastly/go-fastly/v19/fastly"

"github.com/fastly/cli/pkg/argparser"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/mock"
)

func TestProcessDirClosesUploadedFiles(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "foo.txt")
if err := os.WriteFile(filePath, []byte("FOO\n"), 0o600); err != nil {
t.Fatal(err)
}

var capturedBody fastly.LengthReader
cmd := &CreateCommand{
Base: argparser.Base{
Globals: &global.Data{
APIClient: &mock.API{
InsertKVStoreKeyFn: func(_ context.Context, in *fastly.InsertKVStoreKeyInput) error {
if in.Key == "foo.txt" && capturedBody == nil {
capturedBody = in.Body
}
return nil
},
},
Flags: global.Flags{AutoYes: true, NonInteractive: true},
},
},
dirPath: dir,
dirConcurrency: 50,
}

if err := cmd.ProcessDir(nil, io.Discard); err != nil {
t.Fatalf("ProcessDir: %v", err)
}
if capturedBody == nil {
t.Fatal("expected InsertKVStoreKey LengthReader body to be captured")
}
if _, err := capturedBody.Read(make([]byte, 1)); !errors.Is(err, os.ErrClosed) {
t.Fatalf("expected upload file to be closed after ProcessDir, got read err: %v", err)
}
}

func TestProcessDirRetriesAfterConnectionLossWithRewoundBody(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "foo.txt")
if err := os.WriteFile(filePath, []byte("FOO\n"), 0o600); err != nil {
t.Fatal(err)
}

calls := 0
cmd := &CreateCommand{
Base: argparser.Base{
Globals: &global.Data{
APIClient: &mock.API{
InsertKVStoreKeyFn: func(_ context.Context, in *fastly.InsertKVStoreKeyInput) error {
calls++
if calls == 1 {
_, _ = io.ReadAll(in.Body)
return errors.New("net/http: cannot rewind body after connection loss")
}
data, err := io.ReadAll(in.Body)
if err != nil {
return err
}
if string(data) != "FOO\n" {
return errors.New("retry body mismatch")
}
return nil
},
},
Flags: global.Flags{AutoYes: true, NonInteractive: true},
},
},
dirPath: dir,
dirConcurrency: 50,
}

if err := cmd.ProcessDir(nil, io.Discard); err != nil {
t.Fatalf("ProcessDir: %v", err)
}
if calls != 2 {
t.Fatalf("expected 2 insert attempts, got %d", calls)
}
}