Skip to content

Commit

Permalink
OData V4 error message handling (#5013)
Browse files Browse the repository at this point in the history
* OData V4 error message handling

* Adapt tests
  • Loading branch information
DanielMieg authored Aug 16, 2024
1 parent f902f48 commit 8109f6f
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 49 deletions.
17 changes: 12 additions & 5 deletions pkg/abaputils/abaputils.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP
return httpResponse, err
}

// HandleHTTPError handles ABAP error messages which can occur when using OData services
// HandleHTTPError handles ABAP error messages which can occur when using OData V2 services
//
// The point of this function is to enrich the error received from a HTTP Request (which is passed as a parameter to this function).
// Further error details may be present in the response body of the HTTP response.
Expand Down Expand Up @@ -218,10 +218,11 @@ func HandleHTTPError(resp *http.Response, err error, message string, connectionD
return errorCode, err
}

// GetErrorDetailsFromResponse parses OData V2 Responses containing ABAP Error messages
func GetErrorDetailsFromResponse(resp *http.Response) (errorString string, errorCode string, err error) {

// Include the error message of the ABAP Environment system, if available
var abapErrorResponse AbapError
var abapErrorResponse AbapErrorODataV2
bodyText, readError := io.ReadAll(resp.Body)
if readError != nil {
return "", "", readError
Expand All @@ -233,7 +234,7 @@ func GetErrorDetailsFromResponse(resp *http.Response) (errorString string, error
}
if _, ok := abapResp["error"]; ok {
json.Unmarshal(*abapResp["error"], &abapErrorResponse)
if (AbapError{}) != abapErrorResponse {
if (AbapErrorODataV2{}) != abapErrorResponse {
log.Entry().WithField("ErrorCode", abapErrorResponse.Code).Debug(abapErrorResponse.Message.Value)
return abapErrorResponse.Message.Value, abapErrorResponse.Code, nil
}
Expand Down Expand Up @@ -311,12 +312,18 @@ type ConnectionDetailsHTTP struct {
CertificateNames []string `json:"-"`
}

// AbapError contains the error code and the error message for ABAP errors
type AbapError struct {
// AbapErrorODataV2 contains the error code and the error message for ABAP errors
type AbapErrorODataV2 struct {
Code string `json:"code"`
Message AbapErrorMessage `json:"message"`
}

// AbapErrorODataV4 contains the error code and the error message for ABAP errors
type AbapErrorODataV4 struct {
Code string `json:"code"`
Message string `json:"message"`
}

// AbapErrorMessage contains the lanuage and value fields for ABAP errors
type AbapErrorMessage struct {
Lang string `json:"lang"`
Expand Down
74 changes: 67 additions & 7 deletions pkg/abaputils/sap_com_0948.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/cookiejar"
"reflect"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -67,7 +68,7 @@ func (api *SAP_COM_0948) GetExecutionLog() (execLog ExecutionLog, err error) {
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
_, err = handleHTTPError(resp, err, api.failureMessage, connectionDetails)
return execLog, err
}
defer resp.Body.Close()
Expand Down Expand Up @@ -161,7 +162,7 @@ func (api *SAP_COM_0948) GetLogProtocol(logOverviewEntry LogResultsV2, page int)
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
_, err = handleHTTPError(resp, err, api.failureMessage, connectionDetails)
return nil, 0, err
}
defer resp.Body.Close()
Expand All @@ -185,7 +186,7 @@ func (api *SAP_COM_0948) GetLogOverview() (result []LogResultsV2, err error) {
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
_, err = handleHTTPError(resp, err, api.failureMessage, connectionDetails)
return nil, err
}
defer resp.Body.Close()
Expand Down Expand Up @@ -220,7 +221,7 @@ func (api *SAP_COM_0948) GetAction() (string, error) {
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
_, err = handleHTTPError(resp, err, api.failureMessage, connectionDetails)
return "E", err
}
defer resp.Body.Close()
Expand Down Expand Up @@ -248,7 +249,7 @@ func (api *SAP_COM_0948) GetRepository() (bool, string, error, bool) {
swcConnectionDetails.URL = api.con.URL + api.path + api.softwareComponentEntity + api.getRepoNameForPath()
resp, err := GetHTTPResponse("GET", swcConnectionDetails, nil, api.client)
if err != nil {
_, errRepo := HandleHTTPError(resp, err, "Reading the Repository / Software Component failed", api.con)
_, errRepo := handleHTTPError(resp, err, "Reading the Repository / Software Component failed", api.con)
return false, "", errRepo, false
}
defer resp.Body.Close()
Expand Down Expand Up @@ -323,7 +324,7 @@ func (api *SAP_COM_0948) triggerRequest(cloneConnectionDetails ConnectionDetails
}
resp, err = GetHTTPResponse("POST", cloneConnectionDetails, jsonBody, api.client)
if err != nil {
errorCode, err = HandleHTTPError(resp, err, "Triggering the action failed", api.con)
errorCode, err = handleHTTPError(resp, err, "Triggering the action failed", api.con)
if slices.Contains(api.retryAllowedErrorCodes, errorCode) {
// Error Code allows for retry
continue
Expand Down Expand Up @@ -366,7 +367,7 @@ func (api *SAP_COM_0948) initialRequest() error {
// Loging into the ABAP System - getting the x-csrf-token and cookies
resp, err := GetHTTPResponse("GET", headConnection, nil, api.client)
if err != nil {
_, err = HandleHTTPError(resp, err, "Authentication on the ABAP system failed", api.con)
_, err = handleHTTPError(resp, err, "Authentication on the ABAP system failed", api.con)
return err
}
defer resp.Body.Close()
Expand Down Expand Up @@ -431,3 +432,62 @@ func (api *SAP_COM_0948) ConvertTime(logTimeStamp string) time.Time {
}
return t
}

func handleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) (string, error) {

var errorText string
var errorCode string
var parsingError error
if resp == nil {
// Response is nil in case of a timeout
log.Entry().WithError(err).WithField("ABAP Endpoint", connectionDetails.URL).Error("Request failed")

match, _ := regexp.MatchString(".*EOF$", err.Error())
if match {
AddDefaultDashedLine(1)
log.Entry().Infof("%s", "A connection could not be established to the ABAP system. The typical root cause is the network configuration (firewall, IP allowlist, etc.)")
AddDefaultDashedLine(1)
}

log.Entry().Infof("Error message: %s,", err.Error())
} else {

defer resp.Body.Close()

log.Entry().WithField("StatusCode", resp.Status).WithField("User", connectionDetails.User).WithField("URL", connectionDetails.URL).Error(message)

errorText, errorCode, parsingError = getErrorDetailsFromResponse(resp)
if parsingError != nil {
return "", err
}
abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText))
err = errors.Wrap(abapError, err.Error())

}
return errorCode, err
}

func getErrorDetailsFromResponse(resp *http.Response) (errorString string, errorCode string, err error) {

// Include the error message of the ABAP Environment system, if available
var abapErrorResponse AbapErrorODataV4
bodyText, readError := io.ReadAll(resp.Body)
if readError != nil {
return "", "", readError
}
var abapResp map[string]*json.RawMessage
errUnmarshal := json.Unmarshal(bodyText, &abapResp)
if errUnmarshal != nil {
return "", "", errUnmarshal
}
if _, ok := abapResp["error"]; ok {
json.Unmarshal(*abapResp["error"], &abapErrorResponse)
if (AbapErrorODataV4{}) != abapErrorResponse {
log.Entry().WithField("ErrorCode", abapErrorResponse.Code).Debug(abapErrorResponse.Message)
return abapErrorResponse.Message, abapErrorResponse.Code, nil
}
}

return "", "", errors.New("Could not parse the JSON error response")

}
74 changes: 37 additions & 37 deletions pkg/abaputils/sap_com_0948_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestRetry0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")
Expand All @@ -66,7 +66,7 @@ func TestRetry0948(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{ "status" : "R", "UUID" : "GUID" }`,
`{"error" : { "code" : "A4C_A2G/224", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/224", "message" : "Error Text" } }`,
`{ }`,
},
Token: "myToken",
Expand All @@ -80,8 +80,8 @@ func TestRetry0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
api.setSleepTimeConfig(time.Nanosecond, 20*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -95,15 +95,15 @@ func TestRetry0948(t *testing.T) {

client := &ClientMock{
BodyList: []string{
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{ }`,
},
Token: "myToken",
Expand All @@ -124,12 +124,12 @@ func TestRetry0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
api.setSleepTimeConfig(time.Nanosecond, 20*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

api.(*SAP_COM_0948).maxRetries = 20
api.(*SAP_COM_0948).maxRetries = 5

errAction := api.(*SAP_COM_0948).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}"))
assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/228 - Error Text")
Expand All @@ -142,15 +142,15 @@ func TestRetry0948(t *testing.T) {

client := &ClientMock{
BodyList: []string{
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : "Error Text" } }`,
`{ }`,
},
Token: "myToken",
Expand All @@ -171,7 +171,7 @@ func TestRetry0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
api.setSleepTimeConfig(time.Nanosecond, 999*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")
Expand Down Expand Up @@ -200,7 +200,7 @@ func TestClone0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -223,7 +223,7 @@ func TestClone0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")
Expand Down Expand Up @@ -252,7 +252,7 @@ func TestClone0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")
Expand Down Expand Up @@ -317,7 +317,7 @@ func TestPull0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -339,7 +339,7 @@ func TestPull0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -363,7 +363,7 @@ func TestCheckout0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -385,7 +385,7 @@ func TestCheckout0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -409,7 +409,7 @@ func TestGetRepo0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -434,7 +434,7 @@ func TestCreateTag0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -456,7 +456,7 @@ func TestCreateTag0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand All @@ -478,7 +478,7 @@ func TestCreateTag0948(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, err := apiManager.GetAPI(con, repo)
api, err := apiManager.GetAPI(conTest0948, repoTest0948)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0948{}, api.(*SAP_COM_0948), "API has wrong type")

Expand Down Expand Up @@ -565,7 +565,7 @@ func TestGetExecutionLog(t *testing.T) {

apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}

api, _ := apiManager.GetAPI(con, Repository{Name: "/DMO/REPO"})
api, _ := apiManager.GetAPI(conTest0948, Repository{Name: "/DMO/REPO"})

results, errAction := api.GetExecutionLog()
assert.NoError(t, errAction)
Expand Down

0 comments on commit 8109f6f

Please sign in to comment.