diff --git a/ctx.go b/ctx.go index 92ce2f69591..b8d58403ee0 100644 --- a/ctx.go +++ b/ctx.go @@ -517,9 +517,13 @@ func (*DefaultCtx) SaveFile(fileheader *multipart.FileHeader, path string) error // SaveFileToStorage saves any multipart file to an external storage system. func (c *DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error { + if fileheader == nil { + return ErrFileHeaderNil + } + file, err := fileheader.Open() if err != nil { - return fmt.Errorf("failed to open: %w", err) + return fmt.Errorf("%w: %q: %v", ErrFileOpen, fileheader.Filename, err) } defer file.Close() //nolint:errcheck // not needed @@ -529,7 +533,7 @@ func (c *DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path st } if fileheader.Size > 0 && fileheader.Size > int64(maxUploadSize) { - return fmt.Errorf("failed to read: %w", fasthttp.ErrBodyTooLarge) + return fmt.Errorf("%w: %q: %v", ErrFileRead, fileheader.Filename, fasthttp.ErrBodyTooLarge) } buf := bytebufferpool.Get() @@ -537,17 +541,17 @@ func (c *DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path st limitedReader := io.LimitReader(file, int64(maxUploadSize)+1) if _, err = buf.ReadFrom(limitedReader); err != nil { - return fmt.Errorf("failed to read: %w", err) + return fmt.Errorf("%w: %q: %v", ErrFileRead, fileheader.Filename, err) } if buf.Len() > maxUploadSize { - return fmt.Errorf("failed to read: %w", fasthttp.ErrBodyTooLarge) + return fmt.Errorf("%w: %q: %v", ErrFileRead, fileheader.Filename, fasthttp.ErrBodyTooLarge) } data := append([]byte(nil), buf.Bytes()...) if err := storage.SetWithContext(c.Context(), path, data, 0); err != nil { - return fmt.Errorf("failed to store: %w", err) + return fmt.Errorf("%w: %q to %q: %v", ErrFileStore, fileheader.Filename, path, err) } return nil diff --git a/ctx_test.go b/ctx_test.go index 7190f2b9662..98f4de1831f 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -5281,6 +5281,42 @@ func (s *mockContextAwareStorage) Close() error { return nil } +func Test_Ctx_SaveFileToStorage_NilFileHeader(t *testing.T) { + t.Parallel() + + app := New() + storage := memory.New() + + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + err := ctx.SaveFileToStorage(nil, "test", storage) + + require.Error(t, err) + require.ErrorIs(t, err, ErrFileHeaderNil) +} + +func Test_Ctx_SaveFileToStorage_ErrorMessageContainsFilename(t *testing.T) { + t.Parallel() + + app := New(Config{BodyLimit: 10}) // small limit to force error + storage := memory.New() + + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + fileHeader := createMultipartFileHeader( + t, + "test-file.png", + bytes.Repeat([]byte{'a'}, 100), // bigger than limit + ) + + err := ctx.SaveFileToStorage(fileHeader, "test-path", storage) + + require.Error(t, err) + require.Contains(t, err.Error(), "test-file.png") +} + // go test -run Test_Ctx_SaveFileToStorage_ContextPropagation func Test_Ctx_SaveFileToStorage_ContextPropagation(t *testing.T) { t.Parallel() diff --git a/error.go b/error.go index 93c6f7cbcea..41c1050535a 100644 --- a/error.go +++ b/error.go @@ -80,3 +80,11 @@ type ( // UnsupportedValueError exposes json.UnsupportedValueError to describe unsupported values encountered during encoding. UnsupportedValueError = json.UnsupportedValueError ) + +// File errors +var ( + ErrFileHeaderNil = errors.New("file: file header is nil") + ErrFileOpen = errors.New("file: failed to open file") + ErrFileRead = errors.New("file: failed to read file") + ErrFileStore = errors.New("file: failed to store file") +)