From 8109f6fbe8c0c2388bc9848d2b47fb4643bd639f Mon Sep 17 00:00:00 2001 From: Daniel Mieg <56156797+DanielMieg@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:41:23 +0200 Subject: [PATCH] OData V4 error message handling (#5013) * OData V4 error message handling * Adapt tests --- pkg/abaputils/abaputils.go | 17 +++++-- pkg/abaputils/sap_com_0948.go | 74 +++++++++++++++++++++++++++--- pkg/abaputils/sap_com_0948_test.go | 74 +++++++++++++++--------------- 3 files changed, 116 insertions(+), 49 deletions(-) diff --git a/pkg/abaputils/abaputils.go b/pkg/abaputils/abaputils.go index 2f43a003c6..fe805037a7 100644 --- a/pkg/abaputils/abaputils.go +++ b/pkg/abaputils/abaputils.go @@ -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. @@ -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 @@ -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 } @@ -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"` diff --git a/pkg/abaputils/sap_com_0948.go b/pkg/abaputils/sap_com_0948.go index ef25db3bb6..b006b6cead 100644 --- a/pkg/abaputils/sap_com_0948.go +++ b/pkg/abaputils/sap_com_0948.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/cookiejar" "reflect" + "regexp" "strings" "time" @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -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 @@ -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() @@ -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") + +} diff --git a/pkg/abaputils/sap_com_0948_test.go b/pkg/abaputils/sap_com_0948_test.go index 629f23c040..c0f33bdf3c 100644 --- a/pkg/abaputils/sap_com_0948_test.go +++ b/pkg/abaputils/sap_com_0948_test.go @@ -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") @@ -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", @@ -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") @@ -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", @@ -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") @@ -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", @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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") @@ -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)