Skip to content
Open
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
14 changes: 9 additions & 5 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,9 +517,13 @@

// 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)

Check failure on line 526 in ctx.go

View workflow job for this annotation

GitHub Actions / lint / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we better sanitize filename for security reasons

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename comes from the Client Request, so they already know it.

}
Comment on lines 524 to 527
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying err from fileheader.Open() is formatted with %v, so it is not wrapped. This breaks errors.Is/As for callers and makes it harder to detect the root cause programmatically. Wrap the underlying error (use %w) while still including the filename in the message (Go supports multiple %w).

Copilot uses AI. Check for mistakes.
defer file.Close() //nolint:errcheck // not needed

Expand All @@ -529,25 +533,25 @@
}

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)

Check failure on line 536 in ctx.go

View workflow job for this annotation

GitHub Actions / lint / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
Comment on lines 524 to 537
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says errors should include both filename and path, but the open/read error paths only include fileheader.Filename (the path argument isn’t mentioned until the store failure case). Either include path in these earlier error messages too, or adjust the PR description/tests to match the intended behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 535 to 537
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fasthttp.ErrBodyTooLarge is included using %v, so the returned error no longer wraps it. Existing callers/tests that rely on errors.Is(err, fasthttp.ErrBodyTooLarge) will fail. Use %w for fasthttp.ErrBodyTooLarge (and keep wrapping ErrFileRead).

Copilot uses AI. Check for mistakes.

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

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)

Check failure on line 544 in ctx.go

View workflow job for this annotation

GitHub Actions / lint / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buf.ReadFrom errors are formatted with %v, so they are not wrapped and can’t be matched with errors.Is/As. Wrap the underlying read error with %w (while still wrapping ErrFileRead).

Suggested change
return fmt.Errorf("%w: %q: %v", ErrFileRead, fileheader.Filename, err)
return fmt.Errorf("%w: %q: %w", ErrFileRead, fileheader.Filename, err)

Copilot uses AI. Check for mistakes.
}

if buf.Len() > maxUploadSize {
return fmt.Errorf("failed to read: %w", fasthttp.ErrBodyTooLarge)
return fmt.Errorf("%w: %q: %v", ErrFileRead, fileheader.Filename, fasthttp.ErrBodyTooLarge)

Check failure on line 548 in ctx.go

View workflow job for this annotation

GitHub Actions / lint / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
Comment on lines 547 to 549
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When buf.Len() > maxUploadSize, fasthttp.ErrBodyTooLarge is again included with %v instead of being wrapped. This prevents errors.Is(err, fasthttp.ErrBodyTooLarge) from working. Wrap it with %w (and keep wrapping ErrFileRead).

Copilot uses AI. Check for mistakes.

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)

Check failure on line 554 in ctx.go

View workflow job for this annotation

GitHub Actions / lint / lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The storage SetWithContext error is formatted with %v, so the underlying storage error is not wrapped. Wrap the original error with %w so callers can errors.Is/As on the storage-specific error type (while still wrapping ErrFileStore).

Suggested change
return fmt.Errorf("%w: %q to %q: %v", ErrFileStore, fileheader.Filename, path, err)
return fmt.Errorf("%w: %q to %q: %w", ErrFileStore, fileheader.Filename, path, err)

Copilot uses AI. Check for mistakes.
}

return nil
Expand Down
36 changes: 36 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts that the error message contains the filename, but it doesn’t validate the "path" part mentioned in the PR description. If the intent is to include both filename and destination path for debugging context, add an assertion for the path as well (and update the implementation accordingly so the failing code path includes it).

Suggested change
require.Contains(t, err.Error(), "test-file.png")
require.Contains(t, err.Error(), "test-file.png")
require.Contains(t, err.Error(), "test-path")

Copilot uses AI. Check for mistakes.
}

// go test -run Test_Ctx_SaveFileToStorage_ContextPropagation
func Test_Ctx_SaveFileToStorage_ContextPropagation(t *testing.T) {
t.Parallel()
Expand Down
8 changes: 8 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Loading