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

Add error handling to REST operations in Client #23

Merged
merged 2 commits into from
Jun 7, 2024

Conversation

austin-denoble
Copy link
Contributor

@austin-denoble austin-denoble commented May 29, 2024

Problem

We're currently not including the code with error responses from Client. Additionally, we don't seem to be gracefully handling possible ErrorResponse bodies alongside more generic JSON and string responses.

There's an issue tracking this problem here: #17

Ultimately, we want to surface more useful information to users through errors.

Solution

This PR focuses on improving the errors that are returned from REST operations in Client methods.

  • Decode the http.Response Body once, and then use the []byte value to decode into either errors or response structs.
    • Update decodeIndex and decodeCollection to take in []byte rather than io.ReadCloser. Most of this was done to avoid reading the body to EOF.
  • Add errorResponseMap which is used to build up the error information.
  • Add decodeErrorResponse, handleErrorResponseBody, and formatError to deal with parsing error payloads and turning them into more readable output.
  • Add PineconeError which extends the error interface and wraps errors in a struct consumers are able to use if they'd like via reflection.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Infrastructure change (CI configs, etc)
  • Non-code change (docs, etc)
  • None of the above: (explain here)

Test Plan

Unit tests and a few integration tests have been added to validate handleErrorResponseBody and that we're returning PineconeError from failed network operations.



import "fmt"

type PineconeError struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might be unnecessary, but it's somewhat straightforward to adhere to the error interface while also adding additional metadata and information that callers can use if they want. Otherwise, the Error() output should represent the entirety of the error.

return formatError(errMap)
}

func formatError(errMap errorResponseMap) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is somewhat duplicative and I'm open to changing it. I couldn't really land on a good way of trying to represent things.

This basically just turns the map into a JSON string and stuffs it into the Msg in PineconeError.

Details string `json:"details,omitempty"`
}

func handleErrorResponseBody(resBodyBytes []byte, statusCode int, errMsgPrefix string) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When testing I noticed that we don't always seem to be guaranteed the control.ErrorResponse shape for the body, which would lead to specific errors when trying to decode things.

What I tried to do here was attempt to decode into ErrorResponse, and if that fails we just parse the whole body out as a string.

func (c *Client) ListIndexes(ctx context.Context) ([]*Index, error) {
res, err := c.restClient.ListIndexes(ctx)
if err != nil {
return nil, err
}
defer res.Body.Close()

resBodyBytes, err := io.ReadAll(res.Body)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ran into EOF problems when trying to read the body in multiple places. To avoid that I went with reading the body initially and then passing []byte around. Let me know if this is inefficient or confusing in some way, maybe there's a better way to be doing this.

@@ -27,6 +29,47 @@ func TestClient(t *testing.T) {
suite.Run(t, new(ClientTests))
}

func TestHandleErrorResponseBody(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests are attempting to validate the parsing of various http.Response bodies:

  • ErrorResponse
  • generic JSON
  • string

@@ -285,6 +327,34 @@ func (ts *ClientTests) TestCreatePodIndex() {
require.Equal(ts.T(), name, idx.Name, "Index name does not match")
}

func (ts *ClientTests) TestCreatePodIndexInvalidDimension() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't sure how deep to go with these integration tests that intentionally cause errors. I can add more if they seem useful.

Copy link
Contributor

@haruska haruska left a comment

Choose a reason for hiding this comment

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

Looks good.

There may be places here where you can avoid reading the entire body up front which might be beneficial in the non-error cases. Those would most likely be cases with large response bodies.

You can probably adjust a lot of the defer res.Body.Close() calls and explicitly close after the body read.

These early io.ReadAll blocking IO calls may impact performance. If they were minimized to error cases only, it might be better. I'm unsure if that's feasible though.

…checking statuscode to determine when we need to read to parse errors, or when we return responses. update unit tests
@austin-denoble
Copy link
Contributor Author

These early io.ReadAll blocking IO calls may impact performance. If they were minimized to error cases only, it might be better. I'm unsure if that's feasible though.

Good point, I refactored things to only decode when we're dealing with an error, or we've got something to return. This is similar to what you had setup before. 👍

@austin-denoble austin-denoble merged commit b471e0c into main Jun 7, 2024
3 checks passed
@austin-denoble austin-denoble deleted the adenoble/error-handling branch June 7, 2024 22:50
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.

None yet

2 participants