Repository: opensearch-project/opensearch-go (go)

Issue:
[BUG] opensearchapi.Client.Document.Delete bugged error handling (v4)
### What is the bug?
The function `opensearchapi.Client.Document.Delete` has bugged error handling. When it returns an error, for example when attempting to delete a non-existing document, the function rightly returns an error, but this error, instead of being of type `*opensearch.StructError`, is a `*fmt.wrapError` with message `msg`: `"opensearch error response could not be parsed as error: {\"_index\":\"my-index\",\"_id\":\"my-id\",\"version\":4,\"result:\":\"not_found\", <ET CETERA . . . > }"`
The "inner error" at least has the correct "result": "not_found", but this inner error has been wrapped with this parsing error.

### How can one reproduce the bug?
This bug can be reproduced by calling opensearchapi.Client.Document.Delete function on any non-existing document ID.

### What is the expected behavior?
The expectation is to receive an error that can be cast as `*opensearch.StructError` using `errors.As(err, &myStructErr)`, or at the very least, the error should not be wrapped around this parsing error saying the error response could not be parsed as error.

### What is your host/environment?
MacOS Sonoma 14.5

Pull request description:
Improves ParseError response when server response is an unknown json
### Description
In ParseError, when json response from server is unknown, it used to return a string response (`*fmt.wrapError`). This is now changed to return a StringError struct (`*opensearch.StringError`).  

### Issues Resolved
Closes #582

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0cdae0..8c6c62a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 ## [Unreleased]
 
 ### Dependencies
-- Bump `github.com/aws/aws-sdk-go-v2/config` from 1.27.23 to 1.27.26 ([#584](https://github.com/opensearch-project/opensearch-go/pull/584), [#588](https://github.com/opensearch-project/opensearch-go/pull/588))
+- Bump `github.com/aws/aws-sdk-go-v2/config` from 1.27.23 to 1.27.27 ([#584](https://github.com/opensearch-project/opensearch-go/pull/584), [#588](https://github.com/opensearch-project/opensearch-go/pull/588), [#593](https://github.com/opensearch-project/opensearch-go/pull/593))
 - Bump `github.com/aws/aws-sdk-go` from 1.54.12 to 1.54.19 ([#583](https://github.com/opensearch-project/opensearch-go/pull/583), [#590](https://github.com/opensearch-project/opensearch-go/pull/590))
 
 ### Added
@@ -48,6 +48,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - Fixes wrong response parsing for security get requests ([#572](https://github.com/opensearch-project/opensearch-go/pull/572))
 - Fixes opensearchtransport ignores request context cancellation when `retryBackoff` is configured ([#540](https://github.com/opensearch-project/opensearch-go/pull/540))
 - Fixes opensearchtransport sleeps unexpectedly after the last retry ([#540](https://github.com/opensearch-project/opensearch-go/pull/540))
+- Improves ParseError response when server response is an unknown json ([#592](https://github.com/opensearch-project/opensearch-go/pull/592))
 
 ### Security
 
diff --git a/UPGRADING.md b/UPGRADING.md
index e9d3e74..3e54731 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -11,6 +11,46 @@
 
 # Upgrading Opensearch GO Client
 
+## Upgrading to >= 5.0.0
+Version 5.0.0 returns `*opensearch.StringError` error type instead of `*fmt.wrapError` when response received from the server is an unknown JSON. For example, consider delete document API which returns an unknown JSON body when document is not found.
+
+Before 5.0.0:
+```go
+docDelResp, err = client.Document.Delete(ctx, opensearchapi.DocumentDeleteReq{Index: "movies", DocumentID: "3"})
+if err != nil {
+	fmt.Println(err)
+	
+	if !errors.Is(err, opensearch.ErrJSONUnmarshalBody) && docDelResp != nil {
+		resp := docDelResp.Inspect().Response
+		// get http status
+		fmt.Println(resp.StatusCode)
+		body := strings.TrimPrefix(err.Error(), "opensearch error response could not be parsed as error: ")
+		errResp := opensearchapi.DocumentDeleteResp{}
+		json.Unmarshal([]byte(body), &errResp) 
+		// extract result field from the body 
+		fmt.Println(errResp.Result)
+	}
+}
+```
+
+After 5.0.0:
+```go
+docDelResp, err = client.Document.Delete(ctx, opensearchapi.DocumentDeleteReq{Index: "movies", DocumentID: "3"})
+if err != nil {
+	// parse into *opensearch.StringError
+	var myStringErr *opensearch.StringError
+	if errors.As(err, &myStringErr) {
+		// get http status
+		fmt.Println(myStringErr.Status)
+		errResp := opensearchapi.DocumentDeleteResp{}
+		json.Unmarshal([]byte(myStringErr.Err), &errResp)
+		// extract result field from the body
+		fmt.Println(errResp.Result)
+	}
+}
+```
+
+
 ## Upgrading to >= 4.0.0
 
 Version 4.0.0 moved the error types, added with 3.0.0, from opensearchapi to opensearch, renamed them and added new error types.
diff --git a/error.go b/error.go
index 89795f2..5dc23d5 100644
--- a/error.go
+++ b/error.go
@@ -175,7 +175,7 @@ func ParseError(resp *Response) error {
 		return parseError(body, &apiError)
 	}
 
-	return fmt.Errorf("%w: %s", ErrUnknownOpensearchError, string(body))
+	return &StringError{Status: resp.StatusCode, Err: string(body)}
 }
 
 func parseError(body []byte, errStruct error) error {
diff --git a/go.mod b/go.mod
index 6385a99..8e62b40 100644
--- a/go.mod
+++ b/go.mod
@@ -5,21 +5,21 @@ go 1.21
 require (
 	github.com/aws/aws-sdk-go v1.54.19
 	github.com/aws/aws-sdk-go-v2 v1.30.3
-	github.com/aws/aws-sdk-go-v2/config v1.27.26
+	github.com/aws/aws-sdk-go-v2/config v1.27.27
 	github.com/stretchr/testify v1.9.0
 	github.com/wI2L/jsondiff v0.6.0
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d
 )
 
 require (
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.26 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.22.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
 	github.com/aws/smithy-go v1.20.3 // indirect
diff --git a/go.sum b/go.sum
index 5d10443..cf08ca7 100644
--- a/go.sum
+++ b/go.sum
@@ -2,10 +2,10 @@ github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbV
 github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
 github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
 github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
-github.com/aws/aws-sdk-go-v2/config v1.27.26 h1:T1kAefbKuNum/AbShMsZEro6eRkeOT8YILfE9wyjAYQ=
-github.com/aws/aws-sdk-go-v2/config v1.27.26/go.mod h1:ivWHkAWFrw/nxty5Fku7soTIVdqZaZ7dw+tc5iGW3GA=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.26 h1:tsm8g/nJxi8+/7XyJJcP2dLrnK/5rkFp6+i2nhmz5fk=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.26/go.mod h1:3vAM49zkIa3q8WT6o9Ve5Z0vdByDMwmdScO0zvThTgI=
+github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
+github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
@@ -18,8 +18,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvG
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.3 h1:Fv1vD2L65Jnp5QRsdiM64JvUM4Xe+E0JyVsRQKv6IeA=
-github.com/aws/aws-sdk-go-v2/service/sso v1.22.3/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=


Test patch:
diff --git a/error_test.go b/error_test.go
index 5af8152..908abd8 100644
--- a/error_test.go
+++ b/error_test.go
@@ -120,6 +120,22 @@ func TestError(t *testing.T) {
 		_ = fmt.Sprintf("%s", testError)
 	})
 
+	t.Run("StringErrorUnknownJSON", func(t *testing.T) {
+		resp := &opensearch.Response{
+			StatusCode: http.StatusNotFound,
+			Body: io.NopCloser(
+				strings.NewReader(`{"_index":"index","_id":"2","matched":false}`),
+			),
+		}
+		assert.True(t, resp.IsError())
+		err := opensearch.ParseError(resp)
+		var testError *opensearch.StringError
+		require.True(t, errors.As(err, &testError))
+		assert.Equal(t, http.StatusNotFound, testError.Status)
+		assert.Contains(t, testError.Err, "{\"_index\":\"index\",\"_id\":\"2\",\"matched\":false}")
+		_ = fmt.Sprintf("%s", testError)
+	})
+
 	t.Run("Error", func(t *testing.T) {
 		resp := &opensearch.Response{
 			StatusCode: http.StatusBadRequest,
@@ -189,14 +205,6 @@ func TestError(t *testing.T) {
 				},
 				WantedErrors: []error{opensearch.ErrJSONUnmarshalBody, opensearch.ErrUnknownOpensearchError},
 			},
-			{
-				Name: "unknown json",
-				Resp: &opensearch.Response{
-					StatusCode: http.StatusNotFound,
-					Body:       io.NopCloser(strings.NewReader(`{"_index":"index","_id":"2","matched":false}`)),
-				},
-				WantedErrors: []error{opensearch.ErrUnknownOpensearchError},
-			},
 			{
 				Name: "io read error",
 				Resp: &opensearch.Response{