Skip to content
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

bug: Prevent nil-pointer when using custom errors from sub function #252

Closed
wants to merge 1 commit into from

Conversation

elgohr
Copy link

@elgohr elgohr commented Jun 2, 2023

Fixing the following scenario: When returning nil as *apiresponses.FailureResponse from a sub function, the broker can't handle it and produces a nil pointer error.

@cf-gitbot
Copy link
Member

We have created an issue in Pivotal Tracker to manage this:

https://www.pivotaltracker.com/story/show/185322738

The labels on this github issue will be updated when the story is started.

@blgm
Copy link
Member

blgm commented Jun 2, 2023

This fixes a curious behaviour that I can't remember coming across, although it is mentioned in the Go FAQ.

I have a slight hesitation in adding reflect.ValueOf(err).IsNil() to the error checking process. This is because it:

  • adds complexity
  • reflect is slow (though given HTTP is being used, it may not make a practical difference)
  • IsNil() can panic in some situations, so we might be fixing one problem only to introduce another
  • it's not idiomatic Go. For example, I can't see examples of this in the Go standard library.

According to the Go FAQ:

It's a good idea for functions that return errors always to use the error type in their signature (as we did above) rather than a concrete type such as *MyError, to help guarantee the error is created correctly. As an example, os.Open returns an error even though, if not nil, it's always of concrete type *os.PathError.

I'm in two minds about this. On one hand, I like the idea of brokerapi being robust and handling situations like this.

On the other hand, using reflect in error checks is not idiomatic Go. For example, filepath.WalkDir() in the standard library does just does an err == nil check, and would presumably be susceptible to the same issue. So it could be argued that this is a very contrived situation and the real error is that subFunctionWithCustomError() should return error and not a concrete type.

These are just my personal thoughts, and I could be persuaded in either direction. I'd be interested to hear what other folks think, and whether there are good examples or precedents to follow.

@elgohr
Copy link
Author

elgohr commented Jun 2, 2023

I would be more comfortable with something slow (when using custom errors) than crashing.
Whether this is idiomatic go or not had been discussed a lot (https://groups.google.com/g/golang-nuts/c/wnH302gBa4I).
To my point of view form follows function.

blgm added a commit that referenced this pull request Jun 6, 2023
Ideally folks should not create a nil apiresponses.FailureResponse, but
it is possible, and when that happens, calling methods on the nil
apiresponses.FailureResponse should not panic.

Relates to #252
@blgm
Copy link
Member

blgm commented Jun 6, 2023

I was reflecting on this, and part of the challenge for me is that if we replace:

if err != nil {

with

err != nil && !reflect.ValueOf(err).IsNil() {

in one place, then potentially this change should be made in all Go code. And even the basic tutorials should be updated. I see that there's a debate about whether this is a design bug in Go, and I don't want to get into that debate because Go is not about to change. I understand how the change fixes the bug that you observed, but I hope you can understand that I'm hesitant about something like this as it would look unusual to a seasoned Go developer.

So I tried to think of alternatives that don't have such broad ecosystem implications. Clearly there was a problem that led to this PR being created, and it would be good to make that problem better.

My guess is that this originally came from a function that looked a bit like this:

func(_ context.Context, _ string, _ domain.ProvisionDetails, _ bool) (domain.ProvisionedServiceSpec, error) {
  var err *apiresponses.FailureResponse
  // some stuff that assigns to err if there was an error
  return domain.ProvisionedServiceSpec{}, err
}

This function always causes a panic (unless err is assigned).

I think there are two problems here:

  1. A nil apiresponses.FailureResponse panics when any method is called
  2. While writing var err *apiresponses.FailureResponse is completely understandable, it should actually be written var err error, and the panic would not occur. This is counterintuitive, but it's called out as a gotcha in the Go FAQ and various blogs about Go. Ideally a linter (like go vet) could identify issues like this and highlight them in the IDE, but I haven't seen such a linter yet.

I've put together a draft PR that addresses (1), so that if a nil apierrors.FailureResponse() is created, then it will not panic when methods on it are called. In the situation above, it would result in a Provision operation always failing with an "uninitialized FailureResponse" error, which would hopefully lead someone to revise their code.

What do you think? I realise it's not exactly what you wanted, but hopefully it's a step in the right direction.

@blgm
Copy link
Member

blgm commented Jun 6, 2023

Also I think another bug is this function definition in brokerapi:

func NewFailureResponse(err error, statusCode int, loggerAction string) *FailureResponse

It should be:

func NewFailureResponse(err error, statusCode int, loggerAction string) error

And then it would discourage folks from having *apiresponses.FailureRespose pointers in the first place. This one is harder to fix and would likely need to wait until v11.

@elgohr
Copy link
Author

elgohr commented Jun 6, 2023

I guess the change in NewFailureResponse should already help a lot. Regarding the nil-check within the error I would prefer a static error that is well documented.

@blgm
Copy link
Member

blgm commented Jun 6, 2023

@elgohr

Regarding the nil-check within the error I would prefer a static error that is well documented.

Could you expand on that a bit. I want to be sure that I've understood you correctly.

blgm added a commit that referenced this pull request Jun 7, 2023
The function NewFailureResponse() creates an error. The [Go FAQ](https://go.dev/doc/faq#nil_error) says:

> It's a good idea for functions that return errors always to use the error type in their signature (as we did above) rather than a concrete type such as *MyError, to help guarantee the error is created correctly. As an example, os.Open returns an error even though, if not nil, it's always of concrete type *os.PathError.

Returning the concrete type can result in subtle failures. For example,
consider the code:
```go
func something() error {
	var err *apiresponses.FailureResponse
	if 1 != 1 {
		err = apiresponses.NewFailureResponse(errors.New("something bad has happened with the universe"), http.StatusInternalServerError, "log-key")
	}
	return err
}

func main() {
	if err := something(); err != nil {
		fmt.Printf("bad thing: %s\n", err.Error())
	}
}
```

You might expect this to print nothing since 1 does not equal 1. But
actually it panics. This is because the nil
*apiresponses.FailureResponse is not equal to `nil` because it has a
type, but it does have a nil pointer so it panics.

If we replace `var err *apiresponses.FailureResponse` with `var err error`
then everything works as expected. By returning an `error` type from
NewFailureResponse(), the failing scenario doesn't compile.

As with many fixes, this could be considered to be a breaking change.
But it didn't require code changes on the brokers that I tested with,
and if it does break anyone, then it's likely because they have risky
code that has the potential to panic in a counterintuitive way. So
overall I think the benefit of making this change it worth the risk.

Resolves #252
Resolves #256
@blgm
Copy link
Member

blgm commented Jun 7, 2023

My current plan is to resolve this with PR #257

I speculated that maybe this problem first occurred for you when writing some code like:

func(_ context.Context, _ string, _ domain.ProvisionDetails, _ bool) (domain.ProvisionedServiceSpec, error) {
  var err *apiresponses.FailureResponse
  if someOperationFailed() {
		err = apiresponses.NewFailureResponse(errors.New("something bad has happened with the universe"), http.StatusInternalServerError, "log-key")
  }
  return domain.ProvisionedServiceSpec{}, err
}

The PR changes the return type of NewFailureResponse() to error so the code above would no longer compile, and you'd most likely write var err error instead of having any *apiresponses.FailureResponse types in your code. It has the advantages of:

  • making NewFailureResponse() be in line with what the Go FAQ recommends for error return types
  • it means that traditional if err != nil checks will work as expected without needing reflection

Because it can break compilation in some circumstances, I'm going to get some second opinions on this as the aim is to fix problems without creating new problems.

zucchinidev pushed a commit that referenced this pull request Sep 10, 2023
The function NewFailureResponse() creates an error. The [Go FAQ](https://go.dev/doc/faq#nil_error) says:

> It's a good idea for functions that return errors always to use the error type in their signature (as we did above) rather than a concrete type such as *MyError, to help guarantee the error is created correctly. As an example, os.Open returns an error even though, if not nil, it's always of concrete type *os.PathError.

Returning the concrete type can result in subtle failures. For example,
consider the code:
```go
func something() error {
	var err *apiresponses.FailureResponse
	if 1 != 1 {
		err = apiresponses.NewFailureResponse(errors.New("something bad has happened with the universe"), http.StatusInternalServerError, "log-key")
	}
	return err
}

func main() {
	if err := something(); err != nil {
		fmt.Printf("bad thing: %s\n", err.Error())
	}
}
```

You might expect this to print nothing since 1 does not equal 1. But
actually it panics. This is because the nil
*apiresponses.FailureResponse is not equal to `nil` because it has a
type, but it does have a nil pointer so it panics.

If we replace `var err *apiresponses.FailureResponse` with `var err error`
then everything works as expected. By returning an `error` type from
NewFailureResponse(), the failing scenario doesn't compile.

As with many fixes, this could be considered to be a breaking change.
But it didn't require code changes on the brokers that I tested with,
and if it does break anyone, then it's likely because they have risky
code that has the potential to panic in a counterintuitive way. So
overall I think the benefit of making this change it worth the risk.

Resolves #252
Resolves #256
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants