Technical Articles
UI5 Tooling – Custom Server Middleware Proxy Extension
A few months ago, just before UI5con I think, SAP released the official productive version of the new UI5 tools. This was of course a big topic at UI5con with several sessions about it. During the summer I found some time to check out this new toolset and give it a try in Visual Studio Code. I immediately faced one of the basic challenges, I was able to test my app but couldn’t access my OData service. This is normally taken care of the SAP Web IDE…
After some investigation, I found two possible solutions. One is better then the other but currently, you’ll probably need them both:
- Proxy server that forwards the request of your app to the UI5 server and the other to your backend server
- A UI5 CLI Server Proxy extension: The new tool allows you to extend the server middleware which made it possible intercept the requests from the client and redirect the OData requests to your backend.
In the OpenSAP course, SAP used the npm module CORS anywhere: https://www.npmjs.com/package/cors-anywhere . They showed it in the OpenSAP Course Evolved webapps: https://open.sap.com/courses/ui52/items/5u41osEJNuR54XkpeJHMG7 . I didn’t use this one because of the following reasons:
- It requires to change the path in the manifest.json of your app when using the proxy. This means that you always need to change it before deploying to your system or change this in your CI setup.
- I also was not able to call a service with authentication but that could just be me…
Nevertheless, I think these proxy options are way better because you don’t need to change anything to deploy the app to the system.
Option 1: Proxy server
This option will host a proxy server by using express. It requires the node modules “http-proxy” and “express”. Define these modules as devDependencies in the package.json and run “npm install” to use them”.
Next to that, add the script to start the proxy script.
Add proxy in the root folder of your project:
And add the following code:
This code will do the following:
- Load all the required npm modules
- Start a proxy
- Define route to the host were the UI5 app is running
- Load routes to backend services from another config file “odata.json”
- Intercept all the requests from the hosted proxy and forward it to the right route
- It will use the route in the config file if it matches the pattern of the request url
- Otherwise it will use the default app host
- Run an express server for the proxy
Full code:
const express = require('express'),
httpProxy = require('http-proxy'),
fs = require('fs'),
proxy = new httpProxy.createProxyServer();
const appRoute = {
target: 'http://localhost:5000'
};
const routing = JSON.parse(fs.readFileSync('./odata.json'));
var allowCrossDomain = function(req, res) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'X-Requested-With, Accept, Origin, Referer, User-Agent, Content-Type, Authorization, X-Mindflash-SessionID');
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.header(200);
} else {
var dirname = req.url.replace(/^\/([^\/]*).*$/, '$1');
var route = routing[dirname] || appRoute;
console.log(req.method + ': ' + route.target + req.url);
proxy.web(req, res, route);
}
};
var app = express();
app.use(allowCrossDomain);
app.listen(8005);
console.log("Proxy started on http://localhost:8005");
Additional config file with the backend system properties:
Each route requires a part of the request like for example if you have a request “/opu/odata/sap/ZGW_UI5CON_SRV/”, the route should be defined with “opu”, “odata” or like I did “SAP”. The route requires at least a target but also allows you to add authentication “username:password” :
The app with the odata service can now be tested by running the UI5 app with the UI5 CLI:
UI5 serve
Next to that, it also requires to run the proxy.
Npm run proxy
Small remark: in case that UI5 serve uses a different port, you should change this port in the appRoute object of the proxy.
Open the proxy host in the browser and you’ll see the app with the data!
This setup can be improved with the npm module “npm-run-all”. This allows you to start the UI5 host and proxy host together. (don’t forget to run “npm install” after adding the devDependency)
Option 2: UI5 Server Extension
As I was digging deeper into the extensibility of the UI5 tooling I found a way nicer solution. With the middleware extenstion of the UI5 server, you can add your own logic. This means that the logic that’s been hosted by the express server can be implemented in the UI5 server. You can find more information about these extensions in the UI5 Tooling documentation: https://sap.github.io/ui5-tooling/pages/extensibility/CustomServerMiddleware/
First step, define the extension for the server in the “ui5.yaml” file with the following settings:
- Give it a name
- Use it in the definition and in the implementation part
- Set it to run before the “serveResources”
- kind and type are always the same for a middleware extension
- Add a path to your implementation
Create the file for your implementation and add the same logic as in the other proxy. The only differences are:
- It doesn’t need a default app route
- Also, no express server is needed
- If it doesn’t uses the proxy, you need to use the run() function.
In the end, it only requires the same logic that was used in the express interceptor.
Full code:
module.exports = function ({
resources,
options
}) {
const fs = require('fs');
const httpProxy = require('http-proxy');
const proxy = new httpProxy.createProxyServer();
const odata = fs.readFileSync('./odata.json');
const routing = JSON.parse(odata);
return function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'X-Requested-With, Accept, Origin, Referer, User-Agent, Content-Type, Authorization, X-Mindflash-SessionID');
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.header(200);
console.log(req.method + '(options): ' + req.url);
next();
return;
}
var dirname = req.url.replace(/^\/([^\/]*).*$/, '$1'); //get root directory name eg sdk, app, sap
if (!routing[dirname]) {
console.log(req.method + ': ' + req.url);
next();
return;
}
console.log(req.method + ' (redirect): ' + routing[dirname].target + req.url);
proxy.web(req, res, routing[dirname], function (err) {
if (err) {
next(err);
}
});
}
};
It uses the same config as the other proxy for the backend systems:
You can use this by just using the UI5 tooling and run “ui5 serve”
It will have the same result as the first option but only one server will be hosted:
Conclusion
I think we all agree that the second option is the best! But, we will also need the first one… Why? Just in case you want to test the result of the build and directly run the app from the “dist” folder. Putting this all together.
Testing webapp folder:
- Just use the UI5 tools
- This will run the proxy inside the UI5 server
Testing the dist folder
- Combine npm module “serve” in combination with the proxy or run it as one command
- Don’t forget to add the “serve” module to your devDependencies + npm install
- The serve module uses a different port which needs to be changed in the config of the proxy:
- You can now run the app from the dist folder with proxy with only one command:
Hopefully UI5 will also support to serve the dist folder in the future and we can use the same extension for the webapp and dist folder.
Tip
We could also add the build process to the start command:
This will run build and after that serve the app together with the proxy
Full project: https://github.com/lemaiwo/UI5ToolsExampleApp
Hi Wouter, nice post. Thx for sharing
So I followed your tutorial, but my proxy only gets 503 Service not available as response. I found the field x-sap-icm-err-id: ICMENOSYSTEMFOUND in the response header.
Access to my service works fine when using the browser or orion.
Any Ideas?
Hi Wouter, thanks for this blog is really helpful. I would like to get some help as it seems I can't access Northwind OData using the localhost and I don't know how to fix it.
Do you mind giving me a hand on this?
Thanks and regards.
Juan.
That’s why you need the proxy. Fill in the hostname of the northwind service in the target and leave auth empty. Also replace “sap” by the root path of the service and you should be fine.
kr, Wouter
Hi all,
I’m developing an UI5 app locally via VSCode, just for learning purposes, and while searching for a way of consuming OData services locally in the same way WebIDE does via destinations I came across with this blog which helped me a lot, but unfortunatelly I’m having dificulties when trying to consume an OData service published in http://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC/
The thing is that, although I have specified in the odata.json file the real host for the OData service to consume (look at the “sap” entry)
I’m having all time the CORS policy error
“Access to XMLHttpRequest at ‘https://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC/$metadata’ (redirected from ‘http://localhost:8080/sap/opu/odata/iwbep/GWSAMPLE_BASIC/$metadata’) from origin ‘http://localhost:8080’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.“
As you can see in the odata.json file I have configured also one final destination for another URL (in that case not an OData service but a simple JSON file) and with that one everything works ok.
Next some traces where the redirection seems to be done correctly
GET (redirect): http://openui5.hana.ondemand.com/test-resources/sap/ui/documentation/sdk/products.json
GET (redirect): http://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC/$metadata
GET (redirect): http://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC/$metadata
GET: /view/App.view.xml
GET: /view/SalesOrderList.view.xml
What I’m doing wrong?
Thanks in advance.
PS: in case it could help, these are the dataSources in the manifest.json file
PS2: by the way, I'm just playing only with the UI5 Server Extension option 2 explained in this blog.
Might be because the server does not allow this via http? Can you try the update version on npm: https://www.npmjs.com/package/ui5-middleware-route-proxy ?
If that also doesn't work, you can try this one: https://www.npmjs.com/package/ui5-middleware-simpleproxy
I think you just need to use the https URL as your target. The http is getting redirected to https, which your browser tries to access directly instead of via the proxy, resulting in the CORS error, I’d guess.
Ethan Jewett I thougth I did try it but it seems no 😛
I managed to consume the ES5 OData service by the following configuration, just HTTPS and changeOrigin = true.
If I don't change the origin I get a certification error "Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname/IP does not match certificate's altnames: Host: localhost." which is normal, so... the thing is just using HTTPS and changeOrigin -> true
Thanks!!!
PS: finally I didn't use "https://www.npmjs.com/package/ui5-middleware-route-proxy"
You saved my life, I've been having the same issues and trying to solve it without success since 2 months
Million thanks!!!
@Jorge Sancho Royo @ Wouter Lemaire
I have followed the Approach 1 and configured the proxy. In the App Route name I have the port as localhost:8080 instead of 5000. After i run the proxy, i can see the oData service executing localhost:8005/sap/opu/odata. But when i try to call the odata service from UI5 application
http://localhost:8080/sap/opu/odata/sap/ODATASERVICE/$metadata is being called and this does not load the metadata.
In the Proxy.js file, i just changed the port from 5000 to 8080.
Hello
With this option
I am having the issue: self signed certificate in certificate chain
code: 'SELF_SIGNED_CERT_IN_CHAIN'
Any suggestion you would like to share?
Hello,
In case someone else also facing the same problem thought to update my question along with the answer:
I resolved by running two commands as below:
Just finished to read it and experiment it following the instruction. It works like a charm.
Great blog !
Thanks Wouter
It works great! Thanks so much for your post!
Great blog
I hope to read understand and implement the same Iam using nginx reverse proxy at the moment
thanks
Rama anne