Skip to Content
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

 

 

10 Comments
You must be Logged on to comment or reply to a post.
  • 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)

    {
      
      "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.

     

     

     

    /