Repository: honeycombio/terraform-provider-honeycombio
Pull request #526 (2024-08-08 19:17:11)

chore: improve V2 API error reporting
Improves V2 API call error reporting/handling by making use of the JSON:API `source` field ([doc](https://jsonapi.org/format/#error-objects)) in error responses now being sent by the Honeycomb API.

Future work will skip converting these into the RFC7807 'Problem Detail' struct shared with the V1 client and convert them to Terraform diagnostics directly. 

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/526?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) Report
Attention: Patch coverage is `66.66667%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 78.57%. Comparing base [(`f01a3ae`)](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/commit/f01a3aee878cd99adbc03d627a2a8884fc2f50f3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) to head [(`66229d8`)](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/commit/66229d889b20b03cebe62daf03d6649fdb9b4ce5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio).

| [Files](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/526?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) | Patch % | Lines |
|---|---|---|
| [client/errors.go](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/526?src=pr&el=tree&filepath=client%2Ferrors.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio#diff-Y2xpZW50L2Vycm9ycy5nbw==) | 66.66% | [5 Missing :warning: ](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/526?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #526      +/-   ##
==========================================
- Coverage   78.57%   78.57%   -0.01%     
==========================================
  Files          73       73              
  Lines        5606     5619      +13     
==========================================
+ Hits         4405     4415      +10     
- Misses        910      913       +3     
  Partials      291      291              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/honeycombio/terraform-provider-honeycombio/pull/526?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=honeycombio).


Patch:
diff --git a/client/errors.go b/client/errors.go
index cb77a9d..fbba8c3 100644
--- a/client/errors.go
+++ b/client/errors.go
@@ -104,17 +104,22 @@ func ErrorFromResponse(r *http.Response) error {
 			Status: r.StatusCode,
 			Title:  errPayload.Errors[0].Title,
 		}
+
 		if len(errPayload.Errors) == 1 {
-			// If there's only one error we don't need to build up details
-			detailedError.Message = errPayload.Errors[0].Detail
+			// format the detailed error a bit differently if there's only one error
 			detailedError.Type = errPayload.Errors[0].Code
+			detailedError.Details = []ErrorTypeDetail{
+				{
+					Description: errPayload.Errors[0].Title,
+					Field:       parseJSONAPIErrorSource(errPayload.Errors[0].Source),
+				},
+			}
 		} else {
 			details := make([]ErrorTypeDetail, len(errPayload.Errors))
 			for i, e := range errPayload.Errors {
 				details[i] = ErrorTypeDetail{
-					Code:        e.Code,
 					Description: e.Detail,
-					// TODO: field when we have it via pointer
+					Field:       parseJSONAPIErrorSource(e.Source),
 				}
 			}
 			detailedError.Details = details
@@ -137,3 +142,25 @@ func ErrorFromResponse(r *http.Response) error {
 		return detailedError
 	}
 }
+
+// parseJSONAPIErrorSource returns a string representation of the
+// source of a JSON:API Error Source
+func parseJSONAPIErrorSource(e *jsonapi.ErrorSource) string {
+	if e == nil {
+		return ""
+	}
+
+	// the JSON:API specification states that only one of these
+	// fields should be populated so we return the first that is not empty
+	if e.Pointer != "" {
+		return e.Pointer
+	}
+	if e.Parameter != "" { // not currently in use server-side
+		return "parameter " + e.Parameter
+	}
+	if e.Header != "" {
+		return e.Header + " header"
+	}
+
+	return ""
+}
diff --git a/go.mod b/go.mod
index 57849dc..ed0acd1 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/google/go-querystring v1.1.0
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
 	github.com/hashicorp/go-retryablehttp v0.7.7
-	github.com/hashicorp/jsonapi v1.3.1
+	github.com/hashicorp/jsonapi v1.3.2-0.20240802183744-2490a9451c3d
 	github.com/hashicorp/terraform-plugin-framework v1.11.0
 	github.com/hashicorp/terraform-plugin-go v0.23.0
 	github.com/hashicorp/terraform-plugin-mux v0.16.0
diff --git a/go.sum b/go.sum
index 9349785..d50444d 100644
--- a/go.sum
+++ b/go.sum
@@ -73,8 +73,8 @@ github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC16
 github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA=
 github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14=
 github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
-github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo=
-github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
+github.com/hashicorp/jsonapi v1.3.2-0.20240802183744-2490a9451c3d h1:kIMRkf4AL9Zqjlc8aQD2jWBHWri88ncgERDSeln+Ya8=
+github.com/hashicorp/jsonapi v1.3.2-0.20240802183744-2490a9451c3d/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
 github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=


Test patch:
diff --git a/client/errors_test.go b/client/errors_test.go
index f5557db..6588bcc 100644
--- a/client/errors_test.go
+++ b/client/errors_test.go
@@ -206,22 +206,29 @@ func TestErrors_JSONAPI(t *testing.T) {
 	}{
 		{
 			name: "single error",
-			code: http.StatusConflict,
+			code: http.StatusUnauthorized,
 			body: jsonapi.ErrorsPayload{
 				Errors: []*jsonapi.ErrorObject{
 					{
-						Status: "409",
-						Title:  "Conflict",
-						Detail: "The resource already exists.",
-						Code:   "/errors/conflict",
+						Status: "401",
+						Title:  "invalid API key",
+						Code:   "unauthenticated/invalid-key",
+						Source: &jsonapi.ErrorSource{
+							Header: "Authorization",
+						},
 					},
 				},
 			},
 			expectedOutput: client.DetailedError{
-				Status:  409,
-				Type:    "/errors/conflict",
-				Message: "The resource already exists.",
-				Title:   "Conflict",
+				Status: 401,
+				Type:   "unauthenticated/invalid-key",
+				Title:  "invalid API key",
+				Details: []client.ErrorTypeDetail{
+					{
+						Description: "invalid API key",
+						Field:       "Authorization header",
+					},
+				},
 			},
 		},
 		{
@@ -231,25 +238,35 @@ func TestErrors_JSONAPI(t *testing.T) {
 				Errors: []*jsonapi.ErrorObject{
 					{
 						Status: "422",
-						Code:   "/errors/validation-failed",
-						Title:  "The provided input is invalid.",
+						Code:   "validation-failed/invalid",
+						Title:  "field is invalid",
+						Detail: "must be no greater than 10",
+						Source: &jsonapi.ErrorSource{
+							Pointer: "/data/attributes/age",
+						},
 					},
 					{
 						Status: "422",
-						Code:   "/errors/validation-failed",
-						Title:  "The provided input is invalid.",
+						Code:   "validation-failed/invalid",
+						Title:  "field is invalid",
+						Detail: "the length must be between 1 and 10",
+						Source: &jsonapi.ErrorSource{
+							Pointer: "/data/attributes/name",
+						},
 					},
 				},
 			},
 			expectedOutput: client.DetailedError{
 				Status: 422,
-				Title:  "The provided input is invalid.",
+				Title:  "field is invalid",
 				Details: []client.ErrorTypeDetail{
 					{
-						Code: "/errors/validation-failed",
+						Description: "must be no greater than 10",
+						Field:       "/data/attributes/age",
 					},
 					{
-						Code: "/errors/validation-failed",
+						Description: "the length must be between 1 and 10",
+						Field:       "/data/attributes/name",
 					},
 				},
 			},
diff --git a/client/v2/client_test.go b/client/v2/client_test.go
index 2048197..87872ca 100644
--- a/client/v2/client_test.go
+++ b/client/v2/client_test.go
@@ -122,6 +122,14 @@ func TestClient_AuthInfo(t *testing.T) {
 		var de hnyclient.DetailedError
 		require.ErrorAs(t, err, &de)
 		assert.Equal(t, http.StatusUnauthorized, de.Status)
+		assert.Equal(t, "", de.Message)
+		assert.Equal(t, "invalid API key", de.Title)
+		assert.Equal(t, "unauthenticated/invalid-key", de.Type)
+		if assert.Len(t, de.Details, 1) {
+			assert.Equal(t, "Authorization header", de.Details[0].Field)
+			assert.Equal(t, "invalid API key", de.Details[0].Description)
+		}
+		assert.Equal(t, "Authorization header - invalid API key", de.Error())
 	})
 }