SAP/cloud-sdk

Service with input params responds with error with a pregenerated Client.

anupriyaSap opened this issue · 17 comments

[OPTIONAL] Survey
Please, help us making the SAP Cloud SDK better by taking our survey.
It takes less than 3 minutes :)

Issue Description

I am referring the link - https://api.sap.com/api/A_SUPPLIEROPLSCORESAV_CDS/cloud-sdk/Java
and using the pregenerated client for the service - SupplierEvaluationScoreService
I am able to invoke the method - getAllSAP__Currency as mentioned in the example without issues. However, I am getting the following error when I invoke the method - getSupplierOplScoresAVParametersByKey from the same service.

List<SupplierOplScoresAVResult> list = service.getSupplierOplScoresAVParametersByKey("EUR", "YEARTODATE").executeRequest(destination).fetchResults();

I am expecting the same result as what I will get when I invoke it through postman with the following url -
/sap/opu/odata/sap/A_SUPPLIEROPLSCORESAV_CDS/A_SupplierOplScoresAV(P_DisplayCurrency=%27EUR%27,P_DateFunction=%27YEARTODATE%27)/Results?$format=json

While debugging the code, I see that the uri is built like this -
/sap/opu/odata/sap/A_SUPPLIEROPLSCORESAV_CDS/A_SupplierOplScoresAV(P_DateFunction='YEARTODATE',P_DisplayCurrency='EUR'), which is correct.
However, the api responds back with the following error message

{
    "error": {
        "code": "CX_SADL_GW_PARAMETER_ENTITY",
        "message": {
            "lang": "en",
            "value": "Entity 'A_SUPPLIEROPLSCORESAV': Parameter entity set 'A_SupplierOplScoresAV' cannot be used for read"
        },
        "innererror": {
            "application": {
                "component_id": "BC-ESI-ESF-GW",
                "service_namespace": "/SAP/",
                "service_id": "A_SUPPLIEROPLSCORESAV_CDS",
                "service_version": "0001"
            },
            "transactionid": "AF0DC6FADCD50310E0063B1B0869AE12",
            "timestamp": "20230102130134.8976360",
            "Error_Resolution": {
                "SAP_Transaction": "For backend administrators: use ADT feed reader \"SAP Gateway Error Log\" or run transaction /IWFND/ERROR_LOG on SAP Gateway hub system and search for entries with the timestamp above for more details",
                "SAP_Note": "See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)"
            },
            "errordetails": [
                {
                    "ContentID": "",
                    "code": "CX_SADL_GW_PARAMETER_ENTITY",
                    "message": "Entity 'A_SUPPLIEROPLSCORESAV': Parameter entity set 'A_SupplierOplScoresAV' cannot be used for read",
                    "propertyref": "",
                    "severity": "error",
                    "transition": false,
                    "target": ""
                },
                {
                    "ContentID": "",
                    "code": "/IWBEP/CX_MGW_BUSI_EXCEPTION",
                    "message": "An exception was raised",
                    "propertyref": "",
                    "severity": "error",
                    "transition": false,
                    "target": ""
                }
            ]
        }
    }
}

Important information:

  • Your code
  • Expected outcome
  • Actual outcome
  • Steps attempted to resolve the issue
  • In case of issues with any of our VDMs:
    • What happens when a request is performed directly via an HTTP client tool such as Postman?
    • In case that succeeds please provide details on the request such as URL, query parameters, header parameters, prerequisite requests (e.g. CSRF token), etc.
  • Potentially missing information (open questions you might have)

Impact / Priority

This is a high priority issue for us.

Affected development phase: Development

Impact: Blocked

Error Message

Some({"error":{"code":"CX_SADL_GW_PARAMETER_ENTITY","message":{"lang":"en","value":"Entity 'A_SUPPLIEROPLSCORESAV': Parameter entity set 'A_SupplierOplScoresAV' cannot be used for read"},"innererror":{"application":{"component_id":"BC-ESI-ESF-GW","service_namespace":"/SAP/","service_id":"A_SUPPLIEROPLSCORESAV_CDS","service_version":"0001"},"transactionid":"AF0DC6FADCD50310E0063B1B0869AE12","timestamp":"20230102130134.8976360","Error_Resolution":{"SAP_Transaction":"For backend administrators: use ADT feed reader "SAP Gateway Error Log" or run transaction /IWFND/ERROR_LOG on SAP Gateway hub system and search for entries with the timestamp above for more details","SAP_Note":"See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)"},"errordetails":[{"ContentID":"","code":"CX_SADL_GW_PARAMETER_ENTITY","message":"Entity 'A_SUPPLIEROPLSCORESAV': Parameter entity set 'A_SupplierOplScoresAV' cannot be used for read","propertyref":"","severity":"error","transition":false,"target":""},{"ContentID":"","code":"/IWBEP/CX_MGW_BUSI_EXCEPTION","message":"An exception was raised","propertyref":"","severity":"error","transition":false,"target":""}]}}})

Application Logs
Some({"error":{"code":"CX_SADL_GW_PARAMETER_ENTITY","message":{"lang":"en","value":"Entity 'A_SUPPLIEROPLSCORESAV': Parameter entity set 'A_SupplierOplScoresAV' cannot be used for read"},"innererror":{"application":{"component_id":"BC-ESI-ESF-GW","service_namespace":"/SAP/","service_id":"A_SUPPLIEROPLSCORESAV_CDS","service_version":"0001"},"transactionid":"AF0DC6FADCD50310E0063B1B0869AE12","timestamp":"20230102130134.8976360","Error_Resolution":{"SAP_Transaction":"For backend administrators: use ADT feed reader "SAP Gateway Error Log" or run transaction /IWFND/ERROR_LOG on SAP Gateway hub system and search for entries with the timestamp above for more details","SAP_Note":"See SAP Note 1797736 for error analysis (https://service.sap.com/sap/support/notes/1797736)"},"errordetails":[{"ContentID":"","code":"CX_SADL_GW_PARAMETER_ENTITY","message":"Entity 'A_SUPPLIEROPLSCORESAV': Parameter entity set 'A_SupplierOplScoresAV' cannot be used for read","propertyref":"","severity":"error","transition":false,"target":""},{"ContentID":"","code":"/IWBEP/CX_MGW_BUSI_EXCEPTION","message":"An exception was raised","propertyref":"","severity":"error","transition":false,"target":""}]}}})

Project Details

  • SAP Cloud SDK Version:
  • Link to GitHub repository:
  • Project type, for example:
    • CAP Project
    • SAP Cloud SDK Maven Archetype
    • None of the above:
  • Platform:
    • Cloud Foundry
    • Neo
    • None of the above:

Checklist

  • Checked out the documentation and Stack Overflow
  • Description provided with all relevant information
  • Attached debug logs
  • Attached dependency tree
  • Provided the SAP Cloud SDK version & link to relevant source code

Hi @anupriyaSap,

thanks for reaching out to us!

When comparing the request URLs, I found that the one created by the SDK is missing the last path segment (/Results), which is seemingly causing the error - at least I was able to reproduce the issue locally in my Postman.

I'm currently investigating why the segment is not created when using the pre-generated client library.
I'll update you as soon as I found somethings.

Best regards,
Johannes

Thanks for checking the issue. Can you also please suggest how to add the top, skip, format and inlinecount parameters to the service? I do not see the relevant methods to add for that in the service.

Hi @anupriyaSap, as @Johannes-Schneider mentioned above the issue is most likely with the missing /Results path segment.

When running .executeRequest(destination).fetchResults() you are actually doing two requests.

  • .executeRequest(destination) is the first one and targets only the entity itself.
  • fetchResults() is the second one and targets the nested property /Results.

However, my guess is the service does not allow the first request, causing the failure. Currently the Cloud SDK does not have a dedicated API for OData V2 to do the second request directly, without the first. But in this case you can probably "skip" the first request like this:

SupplierOplScoresAVParameters params = SupplierOplScoresAVParameters.builder()
                .p_DateFunction("YEARTODATE")
                .p_DisplayCurrency("EUR")
                .build();
params.attachToService(SupplierEvaluationScoreService.DEFAULT_SERVICE_PATH, destination);
List<SupplierOplScoresAVResult> supplierOplScoresAVResults = params.fetchResults();

This solution should work but has the downside that sending top, skip and inlinecount is not possible via this API. We have an API for OData V4 that does this logic better: https://sap.github.io/cloud-sdk/docs/java/features/odata/v4-vdm-client#navigation-properties. We have an item in the backlog to create something similar for OData V2, but there is no timeline for it currently.


In case the query options are absolutely required for you, you can also build the request via the low-level API.

That could look something like this:

Map<String, Object> key = Maps.newHashMap();
key.put("P_DisplayCurrency", "EUR");
key.put("P_DateFunction", "YEARTODATE");ODataResourcePath path = ODataResourcePath.of("A_SupplierOplScoresAV", ODataEntityKey.of(key, ODataProtocol.V2))
                .addSegment("Results");

ODataRequestReadByKey readByKey = new ODataRequestReadByKey(SupplierEvaluationScoreService.DEFAULT_SERVICE_PATH, path, null, ODataProtocol.V2);
readByKey.addQueryParameter("$top", "10");

ODataRequestResultGeneric resultGeneric = readByKey.execute(HttpClientAccessor.getHttpClient(destination));
List<SupplierOplScoresAVResult> results = resultGeneric.asList(SupplierOplScoresAVResult.class);

Arguably, the getSupplierOplScoresAVParametersByKey method shouldn't exist, because it doesn't work. However, the generator that generates this method needs to handle all OData services and this is somewhat of an edge case. In most cases, some method like this is required. That is why this case is currently not handled by the generator, i.e. the method is being generated regardless.


I hope this helps & sorry for the inconvenience. Please let me know if you have further questions on any of the above points :)

Hi,

Thanks for your response. I was trying as you suggested and added another query parameter. However, the code returns an empty array, whereas the same call from postman returns records. While debugging, it seems the parameters are being set correctly. Can you please help?

ODataRequestReadByKey readByKey = new DataRequestReadByKey(SupplierEvaluationScoreService.DEFAULT_SERVICE_PATH,
				path, null, ODataProtocol.V2);
readByKey.addQueryParameter("$inlinecount", "allpages");
readByKey.addQueryParameter("$top", "1");
ODataRequestResultGeneric resultGeneric = readByKey.execute(HttpClientAccessor.getHttpClient(destination));
List<SupplierOplScoresAVResult> results = resultGeneric.asList(SupplierOplScoresAVResult.class);

The results is an empty array.
The postman url is:

/sap/opu/odata/sap/A_SUPPLIEROPLSCORESAV_CDS/A_SupplierOplScoresAV(P_DisplayCurrency=%27EUR%27,P_DateFunction=%27PREVIOUSYEARTODATE%27)/Results?$format=json&$inlinecount=allpages&$top=1

You are welcome! Could you please share the full code, including how you assemble the ODataEntityKey?

Also, having debug or wire logs would be helpful so that we can see the request the Cloud SDK actually sends out. You can also use readByKey.getRelativeUri(UriEncodingStrategy.REGULAR) to print out the encoded URI.

Finally, from using inlinecount and top=1 it looks like you are mostly interested in the count. Is that the case? If so, you can do that potentially more efficiently by directly issuing an ODataRequestCount.

Hi, Thanks for your response. I made some changes and I am able to see the results successfully now. I am also able to use the ODataRequestCount to get the count. Thanks for pointing that out.

Our requirement is to get the count in and then in the subsequent calls we also need to skip some and get some records and subsequently parse the returned json and create an object. So, say for example I want the record from 100 to 200, I am planning to use the following code --

ODataRequestReadByKey readByKey = new ODataRequestReadByKey(SupplierEvaluationScoreService.DEFAULT_SERVICE_PATH,
path, null, ODataProtocol.V2);
readByKey.addQueryParameter("$skip", "100");
readByKey.addQueryParameter("$top", "100");

Please let me know if there is a better way to do this?

Great to hear that it worked!

What you are describing looks like you want to achieve pagination. That can be done with top and skip as you outlined, but it's not ideal. Because with top and skip there is no guarantee that the data won't change in between requests.

There is a dedicated pagination feature though. The result object has a method .iteratePages(SupplierOplScoresAVResult.class) that you can probably use. Alternatively, you could use getNextLink() and tryGetNextPage(). You can also send a header Prefer: odata.maxpagesize=100 to ask the server for a specific page size. Of course, this only works if the server actually supports pagination.

Hi. Thanks for your response.

I am not able to figure out how to set the header (to set the max number of records I want back in the response at a time).
I am trying this, but it gives me all the records
ODataRequestReadByKey readByKey = new DataRequestReadByKey(SupplierEvaluationScoreService.DEFAULT_SERVICE_PATH,
path, null, ODataProtocol.V2);
readByKey.addHeader("Prefer", "odata.maxpagesize= 1000");
ODataRequestResultGeneric resultGeneric = readByKey.execute(HttpClientAccessor.getHttpClient(destination));

I would also want to get the data for the next subsequent set of data asynchronously. I don't see how the above will allow that.

In case of service failure, will the retry be initiated on its own or do we have to do something to trigger the retry?

Also, my ODataRequestCount fails with "Unable to read OData response." when I try and invoke the method-getInlineCount. I understand that the following is the best way after that?
readByKey.addQueryParameter("$inlinecount", "allpages");
readByKey.addQueryParameter("$top", "1");

Okay, so it looks like you have a whitespace in the header, it should be readByKey.addHeader("Prefer", "odata.maxpagesize=1000");. Not sure if this makes a difference, but worth a try maybe.

Otherwise it could be that the server doesn't support pagination. But I would maybe try again without the whitespace...


You could also implement it asynchronously using getNextLink() and tryGetNextPage(). E.g. after processing the first result you invoke a method asynchronously that will iterate over the remaining pages using these two. Check out our documentation on how to conveniently run asynchronous operations with the Cloud SDK.


For retries, the Cloud SDK will not retry your OData requests automatically. This is because retrying highly depends on the semantic of the OData requests i.e. of your business logic. Instead, you can easily add retries by using the resilience API. There is also a tutorial on how to use the API here.


Finally, for the count, could you please share the code you are using to invoke the count and read the response? And also the full exception that is thrown together with the response payload?

Even after removing the whitespace in the header, it does not return the specified number of results, but returns all the results.


Since from above we can assume that the server does not support pagination, is it possible to implement the request asynchronously? I tried the getNextLink() and tryGetNextPage(), but they do not return anything.


Will check retry. Thanks for the detailed documentation.


Code for OdataRequestCount

Map<String, Object> key = Maps.newHashMap();
key.put("P_DisplayCurrency", "EUR");
key.put("P_DateFunction", "PREVIOUSYEARTODATE");
ODataResourcePath path = ODataResourcePath.of("A_SupplierOplScoresAV", ODataEntityKey.of(key, ODataProtocol.V2)) .addSegment("Results");
ODataRequestCount count = new ODataRequestCount(SupplierEvaluationScoreService.DEFAULT_SERVICE_PATH, path,
null, ODataProtocol.V2);
ODataRequestResultGeneric resultCount = count.execute(HttpClientAccessor.getHttpClient(destination));
System.out.println("resultcount is" + resultCount.getInlineCount());

Exception Message is---

2023-01-12 12:34:48.234 WARN 15772 --- [nio-8082-exec-2] c.s.c.s.d.o.c.ODataResponseDeserializer : JSON path JsonPath(nodes=[d]) could not be resolved for 2460 at position 0.

Okay, yes in that case it looks like pagination is not enabled/supported on the server side.

Yes, you can still do it asynchronously. Just run the individual requests containing top and skip in a new thread. Of course, the risk that the data changes while you iterate over it like this increases the longer this iteration takes...

Yeah, you don't access the result of count via inlineCount. Instead, use result.as(Long.class) :)

Hello Colleagues, the backend service is from ABAP Odata and abap support pagination with getNextLink and it is available for this service, could you please check it.

Hi,
As stated above, the pagination support is available. Is there any other way we can restrict the number of records other than readByKey.addHeader("Prefer", "odata.maxpagesize=1000")?

At least I wouldn't know how. The OData V2 documentation doesn't specify anything, only in OData V4 this header is explicitly defined. Maybe you can open an issue on the component MM-PUR-HUB-CSC (hope that is the right one) to ask about this..

Hi @anupriyaSap,

Has there been any progress with regard to custom page sizes for your service? The original issue is done, right?

Kind regards
Alexander

Hi,
Yes, the original issue is done. As for the page size, we are still exploring the ways to achieve it.

Thanks.
Anupriya.

Hi Anupriya,

We're sorry to hear the page sizes continue to keep you busy. As far as we understand this detail is highly dependent on the OData service implementation and with the information given, we cannot assist further from the client side. Therefore I'm closing the ticket for now. In case you want to share an update, please feel free to (re)open the ticket and/or continue commenting.

Good luck & kind regards
Alexander