-
Notifications
You must be signed in to change notification settings - Fork 505
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
Adding a better error message for download mode #1492
base: main
Are you sure you want to change the base?
Conversation
When it's missing, we write a short message saying so, and when it's invalid, we do approximately the same thing. Athens has a lot of configuration options, so it's naturally easy to get them wrong or forget something. Nicer error messages should help. If this code is acceptable, I'd like to apply it to a few other config values (in a follow-up PR) that are easy to get wrong. ref gomods#1491, since this checks existence and validates values for download mode, and also reports errors in a more human-friendly way cc/ @bluekeyes
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 love it, i wish all error messages were like this
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.
pretty cool ✌️ -- open for comments :)
pkg/errors/config.go
Outdated
// and it knows how to print out the actual configuration name and other helpful information. | ||
type ConfigErrFn func(string) error | ||
|
||
type configErr struct { |
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.
do we need a new error type? I think we can leverage the existing one. And instead of making a new error type, we can just make a new error Kind like ConfigErrorKind
or something.
pkg/errors/config.go
Outdated
// ConfigErrFn is a function that can create a new configuration error. You pass it a | ||
// message specific to the error you found when you were validating configuration, | ||
// and it knows how to print out the actual configuration name and other helpful information. | ||
type ConfigErrFn func(string) error |
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.
Same here since this is specifically a ConfigErrFn
, we can probably just put it in the Config package itself.
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.
@marwan-at-work I wanted to but can't because the config package imports mode which needs all the config error stuff. Do you think it should be somewhere besides there?
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.
Sorry, I meant the mode
package...which makes me realize ConfigErrFn
makes no sense here, since it's not a Config package error, it's a mode
error :)
So that said, all of my suggestion are meant for keeping this functionality local to wherever the error is happening, in this case inside mode.go
Thanks @arschles ✌️
pkg/errors/config.go
Outdated
// | ||
// downloadModeFn := ConfigError("DownloadMode (ATHENS_DOWNLOAD_MODE)") | ||
// err := doThingsWithDownloadMode(configStruct, downloadModeFn) | ||
func ConfigError(field string, url string) ConfigErrFn { |
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 is pretty cool and functional :) but we can probably move this into the config package, because nobody else is leveraging it and so no need to increase the API surface of the errors package, we can just use the already exposed functionality.
pkg/errors/config.go
Outdated
if url != "" { | ||
slc = append(slc, fmt.Sprintf("See %s for more information.", url)) | ||
} | ||
return &configErr{str: strings.Join(slc, "\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.
I'd just return errors.E(op, strings.Join(slc, "\n"), whateverKindIfNeedBe)
pkg/errors/kinds.go
Outdated
@@ -4,3 +4,9 @@ package errors | |||
func IsNotFoundErr(err error) bool { | |||
return Kind(err) == KindNotFound | |||
} | |||
|
|||
// IsConfigErr returns true if the given err is a configuration error | |||
func IsConfigErr(err error) bool { |
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.
we probably don't need this if we just do errors.Is(err, errors.ConfigErrorKind)
or whatever kind name we'll eventually pick.
But furthermore, I don't think we need a new config error kind, because we don't programmatically do anything about it anyway, we just log the error and exit.
cmd/proxy/actions/app.go
Outdated
@@ -132,6 +133,9 @@ func App(conf *config.Config) (http.Handler, error) { | |||
lggr, | |||
conf, | |||
); err != nil { | |||
if errors.IsConfigErr(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.
so all that we're doing here is choosing not to wrap the error with "error adding proxy routes (message)". I think, instead of doing that, we can just pick a better wrapper-message and then we don't need this exposed API at all, we can just say: "error initializing Athens: %s"
below.
cmd/proxy/actions/app_proxy.go
Outdated
@@ -98,7 +99,10 @@ func addProxyRoutes( | |||
} | |||
st := stash.New(mf, s, stash.WithPool(c.GoGetWorkers), withSingleFlight) | |||
|
|||
df, err := mode.NewFile(c.DownloadMode, c.DownloadURL) | |||
df, err := mode.NewFile(c.DownloadMode, c.DownloadURL, errors.ConfigError( |
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.
though this functional pattern is pretty awesome, it's a bit of an overkill just to add 2 lines of text to an error:
- We never have to customize this line
- We can just add those two lines into the errors themselves inside
mod.NewFIle
- We can still keep it functional by keeping the
mode.NewFile
signature exactly the same, but having this functional pattern internal and private to the package itself so that you don't have to manually do it a few times
cc/ @marwan-at-work, I think I've addressed almost all of your comments here, but let me know if this overall design is what you had in mind. Tests are still failing, I'm fixing those shortly
@marwan-at-work I made some changes to (hopefully) address your comments. let me know what you think ✌️ |
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.
last few nits and good to go 👍
cmd/proxy/main.go
Outdated
@@ -32,6 +33,9 @@ func main() { | |||
if err != nil { | |||
log.Fatalf("could not load config file: %v", err) | |||
} | |||
if err := conf.Validate(errors.Op("main")); err != nil { |
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.
We already have a validateConfig
function here: https://github.com/gomods/athens/blob/master/pkg/config/config.go#L238
It gets automatically called when you call config.Load
So this is a bit confusing :)
In the same fashion, the mode.Validate
function in this case is getting called twice, once here and once in mode.NewFile
.
I think we can just:
- remove this line here
- remove
config.Validate
function - make
mode.Validate
private
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.
@marwan-at-work I added a Validate
function on config structs so that we can nest validations. I'll try to move all of the logic into validate
struct tags though
pkg/download/mode/mode.go
Outdated
) | ||
|
||
// Validate ensures that m is a valid mode. If this function returns nil, you are | ||
// guaranteed that m is valid | ||
func (m Mode) Validate(op errors.Op) error { |
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.
Instead of it taking an op
, just make a mode.Validate
operation under this line and wrap the error with it. This way, people can get the entire call stack by the time the server fails to boot up.
pkg/download/mode/mode.go
Outdated
// NewFile takes a mode and returns a DownloadFile. | ||
// Mode can be one of the constants declared above | ||
// or a custom HCL file. To pass a custom HCL file, | ||
// you can either point to a file path by passing | ||
// file:/path/to/file OR custom:<base64-encoded-hcl> | ||
// directly. | ||
func NewFile(m Mode, downloadURL string) (*DownloadFile, error) { | ||
const downloadModeURL = "https://docs.gomods.io/configuration/download/" |
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 would make this a package level constant (private) as it is being used 3 more times in the pr
pkg/download/mode/mode_test.go
Outdated
@@ -127,12 +129,12 @@ func TestNewFile_err(t *testing.T) { | |||
{ | |||
name: "empty mode", | |||
mode: "", | |||
expected: downloadModeErr, | |||
expected: Mode("").Validate(op).Error(), |
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.
hmm, we probably shouldn't do string matching here: either
- change
expected string
tohasErr bool
- or, change
expected string
toexpected errors.Kind
pkg/errors/errors.go
Outdated
@@ -18,6 +18,7 @@ const ( | |||
KindRateLimit = http.StatusTooManyRequests | |||
KindNotImplemented = http.StatusNotImplemented | |||
KindRedirect = http.StatusMovedPermanently | |||
KindConfigError = -1 |
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 believe the HTTP protocol gives you some room for custom codes
In this case this is a client (user) error so maybe we can give it a code between 451 and 499: https://golang.org/pkg/net/http/#pkg-constants
It doesn't really matter but just being a bit more conventional :)
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.
422 / http.StatusUnprocessableEntity
is a common response when a client provides syntactically valid but semantically invalid objects to an API and could be appropriate here, if you want to stick with valid HTTP codes.
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.
@marwan-at-work fair enough! I'll take @bluekeyes's suggestion and go with 422, just so we can use a constant.
and setting a default
Also removing the op from DownloadFile.Validate
Just checking to see if there's an error. Also adding tests for valid modes
@marwan-at-work I made a few fixes in here. Regarding #1492 (comment), I put all the calls to |
Looks like validate tags don't have a prefix checker. This seems easier than building a custom validator
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.
nice work so far, there's a potential regression for some people in this PR who use full addresses and a few comments/nits.
✌️
pkg/config/config.go
Outdated
// Validate implements Validator | ||
func (c *Config) Validate() error { | ||
const op errors.Op = "Config.Validate" | ||
if !strings.HasPrefix(c.Port, ":") { |
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 this will break people who are binding 0.0.0.0:<port>
-- plus our code in ensurePortFormat
will always add the ":", so I think it's best we remove this validation and if the PORT configuration that the user put is wrong, Go will tell them when the server doesn't boot up.
pkg/config/validator.go
Outdated
// Validator can validate a config struct. If you implement this, | ||
// validate all of the configuration in your struct. It will | ||
// automatically be called when Athens starts | ||
type Validator interface { |
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.
Definitely correct me if I'm wrong but:
Do we need a validator interface? I don't see any code actually dealing with the "interface" aspect of things. All the Validate functions are being called explicitly. AKA nothing is being abstracted here :)
I think we can make a type Validator func() error
-- but even then it might be more for documentation than an actual use case.
Also the documentation here I think might be a little misleading:
If you implement this, validate all of the configuration in your struct. It will automatically be called when Athens starts
To the same point above, we need to explicitly call "Validate" and so there's nothing automatic happening right?
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.
You're right, I got ahead of myself here. My intention was to have the config.Validate
function look for Validator
s in each of its fields, calling validate on each of them. Right now, Validate
is called as necessary on each field that implements it, in various other parts of the code.
I'll leave Validator
out of this PR
// | ||
// Generally these kinds of errors should make Athens crash because it was configured | ||
// improperly | ||
func Config(op Op, field, helpText, url string) error { |
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.
will this ever be called from outside the config
package? why not just define it there and define it locally to minimize the API surface of this package?
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.
Nope. I put it here just because I think it makes more sense to group a configuration error under the errors
package. I'm willing to be convinced otherwise 😄
@@ -53,8 +85,8 @@ type DownloadPath struct { | |||
func NewFile(m Mode, downloadURL string) (*DownloadFile, error) { | |||
const op errors.Op = "downloadMode.NewFile" | |||
|
|||
if m == "" { | |||
return nil, errors.E(op, downloadModeErr) | |||
if err := m.Validate(); err != nil { |
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 this error is now less readable, because before it used to say download mode is not set
but now it will say isn't a valid value
(notice the space in the beginning because there's nothing)
I think in the Sprintf of the validate function we should pass a %q
so it would say "" isn't a valid value
or we should keep the old error here for better messaging.
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.
How about if we pass %q
as you said, and also prefix the error with "Download mode ..."?
The result would then be Download mode "" isn't a valid value
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.
That sounds great
pkg/download/mode/mode.go
Outdated
default: | ||
return nil, errors.E(op, errors.KindBadRequest, fmt.Sprintf(invalidModeErr, m)) | ||
retFile := &DownloadFile{Mode: m, DownloadURL: downloadURL} | ||
if err := retFile.Validate(); err != nil { |
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.
Correct me if I'm wrong but:
The old code that was removed here no longer works because in the old code we are checking the top level Mode, but in this validate function only the internal d.Paths[n].Mode
is checked.
However, since top level Mode is now already checked above, then this Validate function is not necessary because retFile
has no Path
s to begin with.
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.
@marwan-at-work you're right. I left a few things out of this Validate
and some tangential functions
DownloadFile.Validate
needs to checkDownloadURL
for well-formedness- ^^ also needs to call
DownloadPath.Validate
for eachd.Paths[n]
- that does more than just validate its mode Mode.Validate
does not check forcustom:
andfile:
values
I've made changes to address all this
I've fixed all of that
@marwan-at-work I think I changed code or responded to all your comments. Let me know what you think or if I missed something! 💚 |
@arschles thank you for fixing all the comments 💯 💯 will take a look as soon as possible 🙏 |
Take your time @marwan-at-work! This PR is a "slow burn" 😄 |
What is the problem I am trying to address?
There are a lot of configuration options and it's easy to forget something, misconfigure Athens, and get stuck when you make a configuration mistake.
DownloadMode
is a good example because it cannot be empty, and when it is, we just say "it can't be empty" without any extra context.See #1491 and #1336 for more discussion on
DownloadMode
How is the fix applied?
I've added a nicer error message in cases where
DownloadMode
is empty, or it is set to an invalid value.If this code is acceptable, I'd like to apply it to a few other config values (in a follow-up PR) that are easy to get wrong.
Mention the issue number it fixes or add the details of the changes if it doesn't have a specific issue.
ref #1491, since this checks existence and validates values for download mode, and also reports errors in a more human-friendly way.
cc/ @bluekeyes