OData batch request through connectivity service always returns 202 Accepted - sap-cloud-platform

We have a springboot/sap-cloud-sdk (3.34.1) application deployed on SAP CloudFoundry. Our application connect to an on-prem SAP Gateway for OData services and uses the CF destination and connectivity service. In most cases this works ok.
We recently started using batch requests towards our SAP OData service.
Local testing against the SAP gateway shows that the batch requests are handled fine. We get the proper error results when we send an request that should fail and when we send a good request it's also handled fine.
However when we deploy the application on SAP CF, and the request is routed through the connectivity service we always get a HTTP 202 Accepted response. No matter what the SAP Gateway returns. If we do some debugging and tracing on the SAP Gateway, we see the expected requests coming in and also see the expected responses from the SAP Gateway.
So it seems that the connectivity service somehow is unable to pass the responses back to our application.
The picture above shows the components the request pass through. Our PMD App uses the cloud sdk to create the batch requests, resolved the destination and send it, throught the connectivity service, to the SAP Gateway. The gateway returns the proper response, but we never see that response in our app. Instead we always get a 202 Accepted response.
-- Update 2020-12-15 16:39 --
We're using OData V2. We've done some more testing and it's not the connectivity service. We've only been focusing on the response payloads of the SAP Gateway. But apparently the batch responses are always wrapped in a 202 Accepted response. If we look more closely, than we see that we get the following response:
HTTP/1.1 202 Accepted
content-type: multipart/mixed; boundary=6C34B07793A6EA7C8AAFC5BC339BDAEC0
content-length: 709
dataserviceversion: 2.0
cache-control: no-cache, no-store, must-revalidate
sap-perf-fesrec: 1300458.000000
--6C34B07793A6EA7C8AAFC5BC339BDAEC0
Content-Type: application/http
Content-Length: 1171
content-transfer-encoding: binary
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=utf-8
Content-Length: 1050
dataserviceversion: 1.0
{"error":{"code":"ZCU/100","message":{"lang":"nl","value":"Service 0000000003 0000000010 niet gevonden voor operatie 0410"},"innererror":{"application":{"component_id":"","service_namespace":"/SAP/","service_id":"ZCU_PE_ORDER_SRV","service_version":"0001"},"transactionid":"23F2932D54040110E005FD84A23B406E","timestamp":"20201215151501.0634490","Error_Resolution":{"SAP_Transaction":"Run transaction /IWFND/ERROR_LOG on SAP Gateway hub system (System Alias ) 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)","Batch_SAP_Note":"See SAP Note 1869434 for details about working with $batch (https://service.sap.com/sap/support/notes/1869434)"},"errordetails":[{"code":"ZCU/100","message":"Service 0000000003 0000000010 niet gevonden voor operatie 0410","propertyref":"","severity":"error","target":""},{"code":"/IWBEP/CX_MGW_BUSI_EXCEPTION","message":"Fout bij wijzigen PE order.","propertyref":"","severity":"error","target":""}]}}}
--6C34B07793A6EA7C8AAFC5BC339BDAEC0--
And somehow the content of the response is not read properly by the SAP Cloud SDK.
In our code we send 1 changeset with the request. The following methods are the core of our batch call. The batchUpdatePEOrderById executes the request. The other methods are just helpers to prepare the batch request.
We expected f.get(0) would unwrap the results of the first changeset, or at least in our example request would result in a Try.failure(), but it always results in a Try.success()
private Either<DomainError, ExternalId> batchUpdatePEOrderById(final ExternalId id, List<ServiceChange> serviceChanges, final String jwtToken) {
final HttpDestination sapMatrix = httpDestinationProvider.providePrincipalPropagationDestination(jwtToken);
// See https://sap.github.io/cloud-sdk/docs/java/features/odata/use-typed-odata-v2-client-in-sap-cloud-sdk-for-java#batch-requests
var f = prepareBatchRequest(id, serviceChanges)
.executeRequest(sapMatrix);
return f.get(0).toEither()
.bimap(error -> getDomainError(id, error), result -> {
log.warn("Updated PE-Order {} successfully: {}", id, result.getCreatedEntities());
return id;
});
}
private ZCUPEORDERSRVServiceBatch prepareBatchRequest(ExternalId id, List<ServiceChange> serviceChanges) {
var batch = peOrderService.batch();
var changeSet = batch.beginChangeSet();
// Split service changes and add to batch operation
var newServices = mapServiceChanges(ChangeType.ADDED, serviceChanges, id);
var updatedServices = mapServiceChanges(ChangeType.QUANTITY_CHANGED, serviceChanges, id);
var deletedServices = mapServiceChanges(ChangeType.DELETED, serviceChanges, id);
if (!newServices.isEmpty()) {
buildServiceChangeSet(changeSet, newServices, ChangeType.ADDED);
}
if (!updatedServices.isEmpty()) {
buildServiceChangeSet(changeSet, updatedServices, ChangeType.QUANTITY_CHANGED);
}
if (!deletedServices.isEmpty()) {
buildServiceChangeSet(changeSet, deletedServices, ChangeType.DELETED);
}
return changeSet.endChangeSet();
}
private void buildServiceChangeSet(ZCUPEORDERSRVServiceBatchChangeSet changeSet, final List<Dienst> services, final ChangeType changeType) {
switch (changeType) {
case ADDED:
services.forEach(changeSet::createDienst);
break;
case QUANTITY_CHANGED:
services.forEach(changeSet::updateDienst);
break;
case DELETED:
services.forEach(changeSet::deleteDienst);
break;
default:
changeSet.endChangeSet();
}
}
Any ideas what could be wrong here?
Thanks,
Danny

This bug is fixed as of SAP Cloud SDK 3.34.1.
Check out the respective release notes.

Related

Using Spring Cloud Contract in order to test frontend to backend interactions

I would like to use Spring Cloud Contract (https://spring.io/projects/spring-cloud-contract) in order to test frontend to backend interactions: especially to catch such errors as 400 http errors.
I was able to run my stubs with the spring cloud contract stub runner. However I noticed that when the actual backend would return a 400, the running stubs return a 404 not found error.
Here is my contract:
description: |
Signup use case
```
given:
a user signs up
when:
the sign up request is valid
then:
the user is signed up
```
request:
method: POST
url: /api/signup
body:
userAccountType: PARENTS
email: john#example.com
firstName: John
plainPassword: secret
address:
placeId: 'an_id'
description: '10 Downing Street'
headers:
Content-Type: application/json
response:
status: 200
If my frontend (i.e. Angular) just issues a Http POST with, say the email field missing, then I expect the running stubs to return a 400.
I would be grateful if someone could share best practices or tips in order to better leverage Spring Cloud Contract for the purpose of frontend/backend tests.
Although I agree with what Marcin said in the comments...
If you get 404 that means that WireMock couldn't find a stub. That means that your request >was not matched with a WireMock stub.
You should create another contract [for each invalid request] with [a] missing field and >mark it with status code 400
... there might be a way to cheat a little bit with priority
You could create a low-priority contract for any request that hits the correct URL to returns 400. On its own, this means every call to that URL would return a 400.
Then create contracts that hit the right URL with the right parameters to return 200 and the expected response and set their priority to high. Since these contracts overlap, the priority ensures that the 200 gets returned and not the 400.
Any other URL will still return 404.
Disclaimer: I have not actually tried this out myself.

How can I set a specific (Content-Type) HTTP response header by configuring undertow?

Summary
I'm implementing a web service, that is deployed on a Wildfly-15 application server. I'd like to have the Content-Type HTTP response header to include charset=UTF-8. This is necessary for the client to understand my response.
Details
One of my clients always sends its request without specifying charset in Content-Type. Wildfly's web service stack in this case uses the default charset=ISO-8859-1 in the response. See this behavior in undertow code, lines 602-611:
private String extractCharset(HeaderMap headers) {
String contentType = headers.getFirst(Headers.CONTENT_TYPE);
if (contentType != null) {
String value = Headers.extractQuotedValueFromHeader(contentType, "charset");
if (value != null) {
return value;
}
}
return ISO_8859_1;
}
Because my response is, in fact, UTF-8-encoded (and its need to be), this causes trouble on the client side. See the header dumps in the undertow log:
----------------------------REQUEST---------------------------
header=Connection=Keep-Alive
header=SOAPAction=****
header=Accept-Encoding=gzip,deflate
header=Content-Type=text/xml
header=Content-Length=2137
header=User-Agent=Apache-HttpClient/4.1.1 (java 1.5)
header=Host=****
locale=[]
method=POST
....
--------------------------RESPONSE--------------------------
header=Connection=keep-alive
header=Content-Type=text/xml;charset=ISO-8859-1
header=Content-Length=1553
header=Date=Tue, 29 Jan 2019 16:19:38 GMT
status=200
This kind of response leads to the following exception in an IBM Websphere application:
org.springframework.ws.InvalidXmlException: Could not parse XML; nested exception is org.xml.sax.SAXParseException: An invalid XML character (Unicode: 0xffffffff) was found in the element content of the document.
Unfortunately, modifying the client is not an option.
Unsuccessful experiments
My efforts so far went into trying to configure Widlfy's undertow via specifying filters to override HTTP Response headers. I was able to set Content-Encoding only, with the help of Response Header filter. It seems, the Content-Type header is overridden somewhere else along the line.
My second guess was to use Expression Filter with the expression
header(header=Content-Type, value='text/xml;charset=UTF-8')
Unfortunately, it didn't work also.
Question
Can this problem be solved by the configuration of undertow?
Or should I somehow (how?) programmatically set the HTTP response header to include charset=UTF-8?

SOAP Fault response from Wiremock not detected as SOAPFault by API-Connect 2018

When I call the actual SOAP service (using Postman and SoapUI) with an invalid parameter value, it causes a SOAP-Fault response, with HTTP 200 .
I copied the body of the response into a Wiremock response file, whose corresponding mapping file returns HTTP 200.
When I use Postman to invoke the SOAP service and the mocked one, the 'Body' of the responses are identical (apart from headers, as the mocked response doesn't explicitly set any).
When my API invokes the actual SOAP service, the SOAPError is caught, the processing stops and the API is processed as defined in the 'catch' section.
However, when the API invokes the mocked SOAP service, the SOAPError is not detected after 'invoke', processing continues and produces an incorrect response.
This suggests that there is something 'extra' returned in a fault from a real SOAP service, that APIC uses to detect a SOAPError. What is it?
I would add it to the mocked response, if only I knew what it should be.
BTW: The response headers are the same for both valid parameters and the SOAP Fault for an invalid one.
[edit]
Thanks #Jan Papenbrock. Adding "Content-Type = text/xml" sorted it out.
I don't know why I thought I was receiving the same headers from real and mocked responses - total rubbish!
John
[/edit]
Had the same error with WireMock and fixed it with the help of answers to this question. In my case, the Content-Type header was missing.
I suggest you try the following:
Send Content-Type: text/xml as response header (or try application/soap+xml)
Return HTTP status code 500 for the SOAP fault response, according to the specification (note: status 400 did not work for me).
My stub generation looks like this:
static ResponseDefinitionBuilder errorInvalidStopResponse() {
responseWithBodyFile('response-error-invalid-stop.xml')
.withStatus(500)
}
static ResponseDefinitionBuilder responseWithBodyFile(String responseBodyFileName) {
aResponse()
.withStatus(200)
.withHeader("Content-Type", "text/xml")
.withBodyFile(responseBodyFileName)
}

Forwarding a response from another server using JAX-RS

I have an angular client which is making a POST call to my server. This server needs to get a response by calling another server(server2) with a POST call and pass the response from the server2 to the client. I tried the following approaches.
public Response call(){
String server2Url = "http://server2/path"
RestClient restClient = new RestClient();
return Response.fromResponse(restClient.post(server2Url)).build();
}
But in the above case the HTTP status code gets transferred but not the response body. The response body is empty
Then I tried:
public Response call() throws URISyntaxException{
String server2Url = "http://server2/path"
RestClient restClient = new RestClient();
return Response.temporaryRedirect(new URI(server2Url)).build();
}
but the browser client ends up making an OPTIONS call to the server2Url instead of a POST
and I tried.
public Response call() throws URISyntaxException{
String server2Url = "http://server2/path"
RestClient restClient = new RestClient();
return Response.seeOther(new URI(server2Url)).build();
}
but this ends up making a GET call instead of a POST.
How do I make the browser client make a POST call to server2
You can use Html Client from JAX-RS to make your own requests (from server1 to server2) and then return the response from server2 to the angular client.
public Response call() {
String url = "server2 url";
Response response;
try {
response = ClientBuilder
.newClient()
.target(url)
.request()
.post(Entity.json(null), Response.class);
}
catch (Exception e) {
// Whatever you want
return null; // or error
}
// Return the status returned by server 2
return Response.status(response.getStatus()).build();
}
What you are trying to accomplish is covered in the RFC 2616 I just found here.
If the 302 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.
So it looks like this is out of your hands if you´re not implementing the client.
Edit because I was told that RFC 2616 must not be used any longer.
RFC 7231 states that:
302 Found
The 302 (Found) status code indicates that the target resource
resides temporarily under a different URI. Since the redirection
might be altered on occasion, the client ought to continue to use the
effective request URI for future requests.
The server SHOULD generate a Location header field in the response
containing a URI reference for the different URI. The user agent MAY
use the Location field value for automatic redirection. The server's
response payload usually contains a short hypertext note with a
hyperlink to the different URI(s).
Note: For historical reasons, a user agent MAY change the request
method from POST to GET for the subsequent request. If this
behavior is undesired, the 307 (Temporary Redirect) status code
can be used instead.
What is:
307 Temporary Redirect
The 307 (Temporary Redirect) status code indicates that the target
resource resides temporarily under a different URI and the user agent
MUST NOT change the request method if it performs an automatic
redirection to that URI. Since the redirection can change over time,
the client ought to continue using the original effective request URI
for future requests.
The server SHOULD generate a Location header field in the response
containing a URI reference for the different URI. The user agent MAY
use the Location field value for automatic redirection. The server's
response payload usually contains a short hypertext note with a
hyperlink to the different URI(s).
Note: This status code is similar to 302 (Found), except that it
does not allow changing the request method from POST to GET. This
specification defines no equivalent counterpart for 301 (Moved
Permanently) ([RFC7238], however, defines the status code 308
(Permanent Redirect) for this purpose).

How to make a REST call to an Azure Queue

I am able to make a C# library call to a queue using the SDK. However I am unable to make a REST call to the queue.
How shall I proceed? Any code sample will be appreciated.
I am able to make a c# library call to a queue using SDK. However i am unable to make a Rest Call to the queue. How shall i proceed and Any code sample will be appreciated.
Firstly, this link lists the REST operations for working with message queues that Azure Storage provides, please check the link to get detailed informations.
Secondly, here is a sample request to create a queue under the given account, you could construct your request like this.
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(string.Format(CultureInfo.InvariantCulture,
"https://{0}.queue.core.windows.net/{1}",
StorageAccount, queuename));
req.Method = "PUT";
req.Headers.Add("Authorization", AuthorizationHeader);
req.Headers.Add("x-ms-date", mxdate);
req.Headers.Add("x-ms-version", storageServiceVersion);
req.ContentLength = 0;
and please refer to the following code and Authentication for the Azure Storage Services to construct the signature string for generating AuthorizationHeader.
string canonicalizedHeaders = string.Format(
"x-ms-date:{0}\nx-ms-version:{1}",
mxdate,
storageServiceVersion);
string canonicalizedResource = string.Format("/{0}/{1}", StorageAccount, queuename);
string stringToSign = string.Format(
"{0}\n\n\n\n\n\n\n\n\n\n\n\n{1}\n{2}",
requestMethod,
canonicalizedHeaders,
canonicalizedResource);
the request looks like this.
There are examples in the official documentation:
Request:
POST https://myaccount.queue.core.windows.net/messages?visibilitytimeout=30&timeout=30 HTTP/1.1
Headers:
x-ms-version: 2011-08-18
x-ms-date: Tue, 30 Aug 2011 01:03:21 GMT
Authorization: SharedKey myaccount:sr8rIheJmCd6npMSx7DfAY3L//V3uWvSXOzUBCV9wnk=
Content-Length: 100
Body:
<QueueMessage>
<MessageText>PHNhbXBsZT5zYW1wbGUgbWVzc2FnZTwvc2FtcGxlPg==</MessageText>
</QueueMessage>
https://learn.microsoft.com/en-us/rest/api/storageservices/fileservices/put-message