-
Notifications
You must be signed in to change notification settings - Fork 379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CI: enable errorlint #2512
CI: enable errorlint #2512
Conversation
When commit 849dd70 was written, Go did not have an ability to wrap multiple errors. Since Go 1.20 it's possible by either using multiple %w, or via errors.Join (which discards nil errors). In a sense, this is what the code was doing before commit 849dd70, except we don't add extra spaces or parentheses -- as joined error, when converted to a string, is represented with newlines added between elements. Signed-off-by: Kir Kolyshkin <[email protected]>
1. Use os.ReadDir to remove a few lines of code. 2. Don't wrap errors from os, they already contain file name. Signed-off-by: Kir Kolyshkin <[email protected]>
1. Don't add context (such as file name and operation) to errors from "os" as they already have this information. 2. Use %w for errors, including multiple errors (works Go 1.20). Signed-off-by: Kir Kolyshkin <[email protected]>
This needs to be "require" rather than "assert", as if error is returned, test needs to fail now. Otherwise, close will panic. Signed-off-by: Kir Kolyshkin <[email protected]>
The current code is unnecessarily complicated: - check if a file/directory exists before opening it unnecessary; - it uses three helper functions not used anywhere else; - directory contents is read twice. Untangle all this, making the code simpler. Signed-off-by: Kir Kolyshkin <[email protected]>
Use errors.As to convert errors. Move the code that converts JSONFormatError to NewInvalidSignatureError to a separate helper, and use it everywhere. Remove the intermediary functions where they became unnecessary. Signed-off-by: Kir Kolyshkin <[email protected]>
This enables errorlint to golangci-lint configuration, and silences last warnings found (three of these are to be removed once a new version of errorlint will be used). Signed-off-by: Kir Kolyshkin <[email protected]>
Let me know if you want me to separate the commit titled |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
I think there are valuable parts in here (e.g. starting to use %w
in code written before it was available), but on the most frequent themes, this is IMHO just not an improvement.
- The Go error “trees” combining multiple error causes make everything worse
- We can’t stop reporting file paths because
fs.PathError
’s output is ambiguous - Every single warning from the linter was either silenced with a comment, or has resulted in an unwanted change to how errors are reported. Meanwhile, the linter does not report the legacy code which should have been using
%w
. In total, the linter is a 100% negative for the project AFAICS, and shouldn’t be enabled.
} else { | ||
retErr = fmt.Errorf(" (dest: %v)", err) | ||
} | ||
retErr = errors.Join(retErr, fmt.Errorf("dest: %w", err)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using the standard-library errors.Join
is basically never valuable.
- If propagated all the way to the CLI, the formatting with embedded newlines doesn’t work well with the (admittedly naive, but extremely widespread) practice of just including an error string in a single-line error message (in the worst case, in the middle). And even if the CLI code wanted to nicely format the individual error values, that’s only possible if the value returned by
errors.Join
is not wrapped itself (because that wrap-around-Join
probably reformats the an error message and the CLI layer has no idea whether it is correct to discard that updated error message formatting).- IOW, if nothing else, I think
errors.Join
should be replaced by carefully designing the resulting text. (Compareinternal/multierr.Format
).
- IOW, if nothing else, I think
- For Go-native handling, I don’t know what
errors.Is(errors.Join(…))
means. The only reasonable case I can think of is that there is a single underlying error cause, and a multi-errorIs
is used to match that cause against multiple error types, e.g. for compatibility with various APIs. If the error value actually reports multiple independent error situations / causes (as in here), I don’t know useful action the caller could ever take based on anerrors.Is
operation.- This is particularly bad if multiple conditions are combined:
errors.Is(err, RemoteServerError) && !errors.Is(err, RemoteServerOverloadedErr)
breaks if the two branches can match two different error causes.
- This is particularly bad if multiple conditions are combined:
And in particular, in these cleanup situations, the caller almost certainly doesn’t want to react to the cause of the Close
failure if retErr
has already been set. So I think type-erasing the err
and only including the string is the more correct thing to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done that in #2519.
@@ -190,7 +190,7 @@ func (br *bodyReader) Read(p []byte) (int, error) { | |||
return n, fmt.Errorf("%w (after reconnecting, server did not process a Range: header, status %d)", originalErr, http.StatusOK) | |||
default: | |||
err := registryHTTPResponseToError(res) | |||
return n, fmt.Errorf("%w (after reconnecting, fetching blob: %v)", originalErr, err) | |||
return n, fmt.Errorf("%w (after reconnecting, fetching blob: %w)", originalErr, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See errors.Join
elsewhere — I don’t see why it’s valuable for the callers’ errors.Is
to match against two different causes here. Only reporting the primary failure cause seems to me far more consistent.
@@ -99,7 +99,7 @@ func parseHTTPErrorResponse(statusCode int, r io.Reader) error { | |||
} | |||
|
|||
func makeErrorList(err error) []error { | |||
if errL, ok := err.(errcode.Errors); ok { | |||
if errL, ok := err.(errcode.Errors); ok { //nolint:errorlint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this should happen at all, please include the actual warning being silenced, throughout.
@@ -798,7 +798,7 @@ func (n *bufferedNetworkReader) read(p []byte) (int, error) { | |||
select { | |||
case b = <-n.readyBuffer: | |||
if b.err != nil { | |||
if b.err != io.EOF { | |||
if b.err != io.EOF { //nolint:errorlint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning exactly io.EOF
is a clearly documented API, so this just indicates an impractical linter to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
errorlint has a list of exceptions for that, listing functions that can return literal io.EOF
but the mechanism doesn't work for some of the more complex setups (including, apparently, this one, when an error is part of a struct which is passed through a channel).
@@ -34,7 +34,7 @@ func NewWriter(sys *types.SystemContext, path string) (*Writer, error) { | |||
// only in a different way. Either way, it’s up to the user to not have two writers to the same path.) | |||
fh, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) | |||
if err != nil { | |||
return nil, fmt.Errorf("opening file %q: %w", path, err) | |||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, I just realized fs.PathError
does not quote the path.
Can’t be helped, really — even if we wanted to wrap all os
/io
/… calls with something that tries to fix that up after-the-fact, it’s impossible to do reliably because fs.PathError
could itself be wrapped somewhere inside the standard library.
@$#!@$#!@ Go and its worse-is-better designs @$#!@$#!@
So, ultimately, I think all these Errorf(%q: %w)
should stay. They are redundant but at least they can unambiguously identify weird inputs and attacks. (Sadly, even if we instituted a project-wide policy to always do that, there’s little hope of getting 100% coverage including dependencies.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that using %q
(rather than %s
or %v
) when logging errors should solve the issue of non-printable characters in file names. The only missing piece would be quotes around the name but it is probably very obvious where we the file name begins and ends (even with extra spaces): https://go.dev/play/p/OmxkoY3DJwS
So I guess if we use %q for errors everywhere we log/print them (which is probably a doable task), we can omit wrapping errors from os
for the sake of adding a file name.
Oh BTW this was not caused by warnings from errorlint
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I guess if we use %q for errors everywhere we log/print them (which is probably a doable task), we can omit wrapping errors from os for the sake of adding a file name.
Except, of course, double quotes will not be needed in this case. Hmm...
Overall, I think, while it's nice to have non-printable characters quoted, it's not crucial to have. If we're worried about attacks with weird inputs, those inputs need to be validated.
Do you think opening a Go proposal to quote the file name in '(*PathError).Error` makes sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that using
%q
(rather than%s
or%v
) when logging errors should solve the issue of non-printable characters in file names.
I don’t think that’s ever going to happen at the top level, e.g. because printing an error which does use %q
itself would look horrible.
it is probably very obvious where we the file name begins and ends (even with extra spaces):
Consider /home/known-user-name/looks/like/an/innocuous/typo: no such file or directory\nexpected failure, please ignore:
. (Even worse, if we had errors.Join
routinely generating error texts that do include \n
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, wrapping and joining indeed makes things less comprehensible. The only real solution to that, I guess, is structured logging.
I also played with having an os
package which wraps all the standard os
functionality, adding path quoting in (*PathError).Error
and (*LinkError).Error
methods. It sorta works, but I don't like this approach, so I'm dropping the ball here.
@@ -239,7 +239,7 @@ func (s *storageImageDestination) putBlobToPendingFile(stream io.Reader, blobinf | |||
filename := s.computeNextBlobCacheFile() | |||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_EXCL, 0600) | |||
if err != nil { | |||
return private.UploadedBlob{}, fmt.Errorf("creating temporary file %q: %w", filename, err) | |||
return private.UploadedBlob{}, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it’s somewhat valuable to explain why the user is suddenly seeing a path what wasn’t in the input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
if dirExists { | ||
isEmpty, err := isDirEmpty(ref.resolvedPath) | ||
if err != nil { | ||
dir, err := os.OpenFile(ref.resolvedPath, syscall.O_DIRECTORY|syscall.O_NOFOLLOW|os.O_RDONLY, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code must remain cross-platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking, neither syscall flags are needed here.
O_DIRECTORY
is not needed here since readdir below will fail anywayO_NOFOLLOW
is here to avoid security issues when a malicious symlink is placed in the directory. If we trust the directory, we can skip this.
Do you want me to drop these, or have these two flags used when on Unix only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading a bit more carefully: in the first place, it’s not defined at all that syscall.O_
constants are valid in calls to os.OpenFile
.
I don’t see what threat the O_NOFOLLOW
is protecting against. [Opening a directory and using *at
for all operations would be a thought, but that’s not what we are doing, and even then I don’t why O_NOFOLLOW
would be necessary.]
So I’m fine with dropping both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done that in #2520
OK I will now try to salvage the good parts from here. |
Done salvaging, this one is no longer needed. |
This improves error reporting and handling, whether it makes sense, enables errorlint, and silences some of its warnings.
See individual commits for details.