Skip to Content
Technical Articles
Author's profile photo Wouter Lemaire

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

 

 

Assigned Tags

      15 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Helmut Tammen
      Helmut Tammen

      Hi Wouter, nice post. Thx for sharing

      Author's profile photo Wadim Halik
      Wadim Halik

      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?

       

      Author's profile photo Juan Felipe Zorrilla Ocampo
      Juan Felipe Zorrilla Ocampo

      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.

      Author's profile photo Wouter Lemaire
      Wouter Lemaire
      Blog Post Author

      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

      Author's profile photo Jorge Sancho Royo
      Jorge Sancho Royo

      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)

      {
        
        "test-resources" : {
          "target" : "http://openui5.hana.ondemand.com",
          "changeOrigin" : true
        },
        "sap" :{
          "target" : "http://sapes5.sapdevcenter.com",
          "auth" : "<user>:<pass>",
          "changeOrigin" : true
        }
      
      }

      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: /i18n/i18n.properties
      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

              "dataSources": {
      			"mainService": {
      				"uri": "/sap/opu/odata/iwbep/GWSAMPLE_BASIC/",
      				"type": "OData",
      				"settings": {
      					"odataVersion": "2.0"
      				}
                  },
                  "testService" : {
                      "uri" : "/test-resources/sap/ui/documentation/sdk/products.json",
                      "type": "JSON"
                  }
              }

      PS2: by the way, I'm just playing only with the UI5 Server Extension option 2 explained in this blog.

       

       

       

      Author's profile photo Wouter Lemaire
      Wouter Lemaire
      Blog Post Author

      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

      Author's profile photo Ethan Jewett
      Ethan Jewett

      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.

      Author's profile photo Jorge Sancho Royo
      Jorge Sancho Royo

      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

      {
        
        "test-resources" : {
          "target" : "https://openui5.hana.ondemand.com",
          "changeOrigin" : true
        },
        "sap" :{
          "target" : "https://sapes5.sapdevcenter.com",
          "auth" : "<user>:<pass>",
          "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"

      Author's profile photo Bassel El-Bizri
      Bassel El-Bizri

      You saved my life, I've been having the same issues and trying to solve it without success since 2 months

       

      Million thanks!!!

      Author's profile photo Arun Krishnamoorthy
      Arun Krishnamoorthy

      @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.

      Author's profile photo Somnath Paul
      Somnath Paul

      Hello

      With this option

       "changeOrigin" : true

      I am having the issue: self signed certificate in certificate chain

      code: 'SELF_SIGNED_CERT_IN_CHAIN'

      Any suggestion you would like to share?

      • Thanks, Somnath
      Author's profile photo Somnath Paul
      Somnath Paul

      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:

      1. npm config set strict-ssl=false
      2. export NODE_TLS_REJECT_UNAUTHORIZED=0
      • Thanks, Somnath
      Author's profile photo Jeremy Chabert
      Jeremy Chabert

      Just finished to read it and experiment it following the instruction. It works like a charm.

      Great blog !

      Thanks Wouter

      Author's profile photo Sergey Gonchar
      Sergey Gonchar

      It works great! Thanks so much for your post!

      Author's profile photo rama anne
      rama anne

      Great blog

      I hope to read understand and implement the same Iam using nginx reverse proxy at the moment

      thanks

      Rama anne