Technical Articles
Consuming destinations in cloud foundry using axios in a nodejs application
Hi all,
There are a lot of http clients available to consume HTTP service in Javascript.
There are a lot of discussions on which one is the best, I love them all, but in the end you have to use only one.
My personal favorite is axios. it’s well documented, supports many features and is easy to learn.
That’s why I decided to make the life of the axios users in the SAP Cloud Foundry easier and created a sap-cf-axios library.
Let’s start with explaining all different parts we need to make an http call.
Destinations
Destinations are defined in the cloud cockpit.
We use these destinations to configure our connection:
- a base url
- authentication configuration
- proxy configuration
Using the destination in a nodejs applications requires a binding to a destination service. If the destination is pointing to an on premise system we have to connect to this service via the cloud connector. To call a service over the cloud connector we have to adapt our proxy settings in our http client. To read the configuration of the proxy and the authentication information, we need to bind our application to a connectivity service.
In my example here I define 3 services (UAA, destination and connectivity) and I bind the services to my approuter and service.
ID: sap-cf-axios-example
_schema-version: "2.1"
description: Example sap cf axios
version: 0.0.1
modules:
- name: sca_approuter
type: approuter.nodejs
path: approuter
parameters:
disk-quota: 256M
memory: 256M
requires:
- name: sca_uaa
- name: sca_destination_service
- name: sca_api
group: destinations
properties:
forwardAuthToken: true
name: sca_api
strictSSL: false
url: '~{url}'
- name: sca_srv
type: nodejs
path: srv
parameters:
memory: 512M
disk-quota: 256M
provides:
- name: sca_api
properties:
url: ${default-url}
requires:
- name: sca_uaa
- name: sca_connectivity_service
- name: sca_destination_service
resources:
- name: sca_destination_service
type: destination
parameters:
service-plan: lite
shared: true
- name: sca_connectivity_service
type: connectivity
parameters:
service-plan: lite
shared: true
- name: sca_uaa
type: com.sap.xs.uaa
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
Axios
https://github.com/axios/axios
When we use axios in nodejs we have to create an AxiosRequestConfig object that specifies the properties like method, url, payload, headers of the call we need to send to our service.
The return of the axios function is a promise with the AxiosResponse object where we can read the response header and the body among other things. Let’s give an example of an odata service served on https://my.site/myService.
If we want to post a Book entity to this service that requires authentication we can use this code:
const axios = require('axios');
const response = await axios({
method: "post",
url: "/BookSet",
baseUrl: "https://localhost:3002"
data: {
title: "Using Axios in SAP Cloud Foundry",
author: "Joachim Van Praet"
},
headers: {
"content-type": "application/json"
},
auth: {
username: 'jowa',
password: 's00pers3cret'
}
})
Axios with destinations
To use axios with the destination and connectivity service you can install the sap-cf-axios in your application.
npm install -s sap-cf-axios
To keep the use of axios as close as possible to the standard. I decided to create an axios instance for each destination. For each destination a request interceptor will override the data in the AxiosRequestConfig object with the configuration in the destination before starting the request.
Let’s take the same example as before. Let’s post a new Book to the service.
const {SapCfAxios} = require('sap-cf-axios');
const destinationName = "MyDestinationNameInCloudFoundry";
const axios = SapCfAxios(destinationName);
const response = await axios({
method: "post",
url: "/BookSet",
data: {
title: "Using Axios in SAP Cloud Foundry",
author: "Joachim Van Praet"
},
headers: {
"content-type": "application/json"
}
})
As you can see we deleted the baseUrl and auth property from our call. These properties will be read from the destination and injected by the sap-cf-axios library. If it is an onpremise service the proxy settings will also be injected by the library.
Use it yourself
Let’s try this thing!
I created a nodejs service to run locally. You can find it in here: https://github.com/jowavp/show-request
just run:
npm install
npm run start
The service starts on port 3002. The response of the service is a representation of the request parameters that are sent to this service.
let’s try it with postman locally. This is the request:
POST /BookSet HTTP/1.1
Host: localhost:3002
Content-Type: application/json
Authorization: Basic am93YTpzMDBwZXJzM2NyZXQ=
User-Agent: PostmanRuntime/7.19.0
Accept: */*
Cache-Control: no-cache
Postman-Token: ab328757-526e-4ab3-9464-d454a9abd9a1,157664fa-0efa-41ea-a97a-f440e836eb81
Host: localhost:3002
Accept-Encoding: gzip, deflate
Content-Length: 89
Connection: keep-alive
cache-control: no-cache
{
"title": "Using Axios in SAP Cloud Foundry",
"author": "Joachim Van Praet"
}
The response of this request is the following:
{
"method": "POST",
"baseUrl": "",
"uri": "/BookSet",
"host": "localhost",
"headers": {
"content-type": "application/json",
"authorization": "Basic am93YTpzMDBwZXJzM2NyZXQ=",
"user-agent": "PostmanRuntime/7.19.0",
"accept": "*/*",
"cache-control": "no-cache",
"postman-token": "ab328757-526e-4ab3-9464-d454a9abd9a1",
"host": "localhost:3002",
"accept-encoding": "gzip, deflate",
"content-length": "89",
"connection": "keep-alive"
},
"body": {
"title": "Using Axios in SAP Cloud Foundry",
"author": "Joachim Van Praet"
}
}
nothing fancy here because there is nothing happening with our request. It is send from postman to the service, so what we put in postman we will see in the response of our service. But things will get interesting when we use sap-cf-axios in cloud foundry to call our local service. First we have to configure the service in cloud connector so we can access it in cloud foundry.
Then we define a destination in cloud foundry that points to our local service using it’s virtual hostname.
When this configuration is done. We deploy a simple nodejs service that will call this destination with sap-cf-axios. https://github.com/jowavp/sap-cf-axios-example
const express = require('express');
const app = express();
const {SapCfAxios} = require('sap-cf-axios');
// the destination that we want to use is named 'DUMMY'
const axios = SapCfAxios('DUMMY');
app.use(express.json());
const handleRequest = async (req, res) => {
try {
const response = await axios({
method: 'POST',
url: '/BookSet',
data: {
title: "Using Axios in SAP Cloud Foundry",
author: "Joachim Van Praet"
},
headers: {
"content-type": "application/json"
/* if you are using an oauth2* authentication type or principal propagation
* for your destination
* please add your JWT token in the authorization header of this request
**/
// authorization: <user JWT Token>
}
});
res.send(response.data);
} catch (error) {
res.reject(error);
}
}
app.all('*', handleRequest);
const port = process.env.PORT || 3000;;
app.listen(port, function () {
console.log('myapp listening on port ' + port);
});
you can deploy this application by running: (you need cli & mbt installed)
npm install
npm run build
npm run deploy
Once this application is deployed, we can open the url of the approuter to test it.
When we now look to the response. we see:
{
"method": "POST",
"baseUrl": "",
"uri": "/BookSet",
"host": "localhost",
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
"authorization": "Basic am9hY2hpbTpzMDBwZXJzM2NyZXQ=",
"sap-connectivity-scc-location_id": "my_cloud_connector_location_id",
"user-agent": "axios/0.19.0",
"content-length": "73",
"connection": "close",
"host": "localhost:3002",
"x-forwarded-host": "dummy:3000"
},
"body": {
"title": "Using Axios in SAP Cloud Foundry",
"author": "Joachim Van Praet"
}
}
As you can see sap-cf-axios added the authorization header and handled the proxy configuration to call the service on our local environment.
So, I hope this sap-cf-axios will make your life easier in consuming destinations in SAP cloud foundry. There are still some features missing that I will add when I need them. 🙂
kr,
Joachim
Hey Joachim!
This is a great article and it covers exactly some of the issues we're facing in the project my colleagues and I are working at the moment, so it couldn't be more timely. So thanks for that.
We're really looking forward for the authentication principal propagation to on-premise systems feature, as it would help us with our use case.
Best wishes,
Jordi
Thanks Jordi!
In the meanwhile I updated the library (v0.0.14) and it should be able to consume destinations with principal propagation. You just have to forward your current logon token to the request in the authorization header:
To allow principal propagation the user needs the authorization scope “uaa.user”
another remark: the forwarded JWT token does not contain the properties suggested (mail, name, display_name and login_name). When you configure the client certificate template, you have to change them manually with properties from the forwarded JWT token (e.g. user_name, email)
kr,
Joachim
Hi Joachim,
Thank you for creating the library . It is very useful. I am facing an issue in consuming the destination with OAuth2 credentials. It doesn’t work the same way. I am getting below error . Could you please help.
Error
Hi Manikandan Kannan
Please try again with version 0.1.1 of the library. I was expecting a bearer token from the destination service for this authentication type, but it wasn't there. I implemented it manually.
For sap-cf-axios only an authorization header is needed if you try to authenticate with the current user. If you use client credential flow, you don't need the authorization header.
kr,
Joachim
Hi Joachim,
It works only for basic authentication but if there is OAuth2ClientCredentials authentication it doesnt work. Oauth2 needs Bearer token of the Token Service URL(https://test.authentication.ap10.hana.ondemand.com).
Please suggest.
Hi Manikandan Kannan ,
Are you shure you are using the latest version of the library?
I’ve tested it here on my account with an OAuth2ClientCredentials destination to the Business Rules Service on a NEO account and it works like a charm!
please double check the library version in your package.json.
If it is possible to share your code in a git repo I can take a quick look at it.
kr,
Joachim
Hi Joachim,
Yes I am using the library 0.1.1. I dont have the git code only for the OAuth2 . If the below code is correct . I can try in local separately and provide you. But this is the snippet of the complete code.
Please let me know if anything wrong.
Package.json
"sap-cf-axios":"^0.1.1"
Handlers:
service.cds
you are not sending your payload with your request ...
Can you share the log of the application and the value of response after the request?
I retested my application here and it is really working 🙂
Are you sure your destination is configured correctly?
kr,
Joachim
Hi Joachim ,
I am using the library for long time now,it was working fine till yesterday and it is giving the new issue from today. There was no change in the configuration but not sure why its suddenly giving this error.
Tried to debug but not able to find any issue because there was no configuration change.
Kindly please help.
Version I am using .
"sap-cf-axios": "0.1.3"
Thanks
Hi Manikandan Kannan ,
Seems there is something wrong with the csrf protection handling.
have you added xsrfHeaderName: "x-csrf-token" in the request?
The csrf handling is done with the options method.
If the OPTIONS call is not available you have to handle the csrf token yourself.
To disable the csrf protection set
xsrfHeaderName:null
I'm quite busy at the moment, but I created a version 0.1.5 that may fix your problem.
Can you try?
H Joachim,
We are using this library as the base to get the data for the project and we are stuck now 🙁
I changed it to 0.1.5 but still same issue.
Regquest
Hi Joachim,
Yes, as you mentioned OPTIONS method is not available to call the ECC to fetch CSRF token . You suggesting to use it by our self to fetch instead using library?
It was working fine till yesterday using 0.1.3 with the same library.
I also see axios has published a new version of library 0.19.1 yesterday .
I did not change anything on this. The X-CSRF-Token was using OPTIONS call from the beginning.
I created a version 0.1.7 where you can configure the csrf method in the constructor, I hope this helps …
The last parameter defines the http method used for the CSRF token.
If this is not working, please provide me a code snippet, then I will try to create a similar scenario to test.
Same Error and I am not able to use it 🙁 I think its not only the method which we pass ,since there is a change in the version of the AXIOS library it is throwing error ,because it is not fetching the CSRF token. Axios verison 0.19.0 was fine. Yesterday they updated with 0.19.1 and from then it stopped working.
Hi Joachim,
It dint work either. Yes it was option method before too,but it was working fine because it was going GET(whatever mentioned in the request) to ECC .
This version 0.1.6 goes as GET method but still getting the error.
Code Snippet
Hi Manikandan Kannan ,
The issue was indeed related to the axios 0.19.1 upgrade.
I've updated the sap-cf-axios library to version 0.2.0, this one will solve your issue.
kr,
Joachim
Thank you 🙂
Hi Joachim,
Thank you for all the update to the library as we are closely following it. It is very much useful . We were using the library with the basic authentication and everything was working fine. Now we switched to the principal propagation and it is giving us the error. We use cloud connector to connect to the ECC and we are getting the error as
{ error: [32m’unauthorized'[39m,
2020-03-09T20:44:24.072+0000 [APP/PROC/WEB/0] OUT error_description:
2020-03-09T20:44:24.072+0000 [APP/PROC/WEB/0] OUT [32m’An Authentication object was not found in the SecurityContext'[39m } },
The way I used the library is .
Hi Manikandan Kannan ,
In the req.attr.token in CAP there is the token, but for the authorization header you have to add the type. Please try with:
kr,
Joachim
Hi Joachim,
When i try this out and push it to cloud foundry I get an error that says
2020-03-27T22:28:52.12+0100 [APP/PROC/WEB/0] ERR const axios = SapCfAxios(destinationName);
2020-03-27T22:28:52.12+0100 [APP/PROC/WEB/0] ERR ^
2020-03-27T22:28:52.12+0100 [APP/PROC/WEB/0] ERR TypeError: SapCfAxios is not a function
anything changed recently?
Some of my observations.
declaration of SapCfAxios ->
App router bearer token is available at req.headers.authorization (this is because I have set forwardAuthToken is true in destinations from AppRouter to Node.js app)
Using this I can make onPremise XSJS or OData calls.
My concern is with XSUAA having breaking changes every so often i think it is better to do an OAuth call with the UAA to get the token and then add it to the authorization header rather than getting the token from request headers.
All in all this is a great library and makes onPremise data consumption very very simple.
Thanks a lot Joachim.
Regards,
Abhi
Hello Abhijith Ravindra,
When i try to push i find the same problem of you "apCfAxios is not a function".
Did you find a solution please?
Hi cherif abir
My second comment should fix the issue.
But this library does not play well with TypeScript for some reason.
Which is why I made the implementation from scratch for it.
Hello Joachim Van Praet ,
I try to connect my nodeJS application to my system backend , i prepare the service on the backend, the cloud connector , the app-router , the nodejs application(with services uaa, destination , connectivity) and i create the destination on scp , i deploy my application on scp but when i execute i find always Gatewaytimed out. and i tested the backend service with a simple approuter and it work correctly and i find the result on the browser.
i try to do this with axios and with sap-cf-axios
Can you help me pleaseeeeeee
1/ this is the connection of cloud connector on scp
2/ this is my destination on scp
3/ this is the service tested with app-router
4/this the function i prepared with axios
5/ this is my function with sap-cf-axios
Hi,
I experience the same problem. When testing locally. From the above codebase
Some of my findings. I have apply the similar code base in a CAP function.
const SapCfAxios = require('sap-cf-axios') only works with default, if you omit this, it will crash.
and when it reach line 29, is just result in a long execution and eventually fails. Our destination 'gw_backend' is a destination configure to NW Gateway access through cloud connector with basic authentication.
Your input is much appreciated.!!!!
This probably will not work as the call made to onprem via. cloud connector is via the connectivity service proxy host.
Ramon
Hi Joachim Van Praet ,
this package is awesome and made my life much easier today.
I am building a decoupled integration using SAP Enterprise Messaging. Now I am able to receive messages on a queue in the app, transform it and post it to make SAP Marketing onPrem System using your npm module.
Cheers
Christian
Hi Joachim Van Praet ,
Have you considered an official SAP Cloud SDK library for your case? It has abstractions for Destinations and Proxies handling similar to the library you're referring to. You can also check out our Github repo: https://github.com/SAP/cloud-sdk.
Additionally, we provide a generator for OData services as well as a set of pre-generated libraries of S/4HANA Cloud and some other SAP services.
Best,
Artem
Hi Joachim Van Praet ,
Does the library support batch call requests? Although my response status is a success, but I get blank response.
Thanks,
Parth
Joachim Van Praet ,
thank you very much for this library! We are usig cf axios extensively in our productive CAP based application.
BR
Rufat
Hi Joachim,
Awesome package .
Is there any way test application locally bcz every time it will difficult to test app after deployment.
Regards,
Alok
Hi Alok,
Take a look here.
If you want to test destinations pointing to onpremise systems you need to open a SSH tunnel and set the internal proxy url (connectivityproxy.internal.cf.eu10.hana.ondemand.com for eu10) in your default-*.json file to localhost.
to open a tunnel (for region eu10):
kr,
Joachim
Thanks Joachim for reply .
Hi Joachim Van Praet
First off, thanks for the awesome blog, I had a question, is there a way to manually handle and inject the proxy settings, etc for connecting to an on-premise system? Our scenario is that we do not want to use the destination to store the url and credentials, we want to store that somewhere else, so I need to be able to in code build up the proxy, authorization, etc to be able to call an on-premise odata service?
Hi Jacques Otto ,
You can read proxy details from the connectivity service.
https://github.com/jowavp/sap-cf-destconn/blob/master/src/connectivity.ts
If you want, you can use this package: https://www.npmjs.com/package/sap-cf-destconn
kr,
Joachim
Thanks for a detailed blog.
I tried calling the destination (Basic Auth) and got the below-mentioned error. Please let me know what step I have missed
error
Hello Joachim,
when I try to import sap-cf-axios, I got the below error:
import SapCfAxios from 'sap-cf-axios'
const axios = SapCfAxios('Northwind')
ERR const axios = SapCfAxios('Northwind');
ERR ^
ERR TypeError: SapCfAxios is not a function
It looks like sap-cf-axios does not support ES6, is that correct?
Best regards,
Yuhong