Skip to Content
Technical Articles

UI5: solving CORS issue – during local development – with app router

Quicklinks:
Quick Guide
Sample Project

Dear desperate friends,
I’m sharing this because I was same desperate like you.
Started happily writing little UI5 app, learned about remote OData service and data binding, and all of that, all sounds nice, but when trying to open the app in local browser
-> error
The console filled with that red ugly error text.
Problem is CORS
Nice Explanations can be easily found in the internet, but usable solutions, difficult

In my case: local development, of course, at the beginning I want to work locally on my app

First attempt: open the index.html file directly in browser.
The error:
Access… at ‘file:///C:/…/manifest.json… from origin ‘null’ has been blocked by CORS policy:
Cross origin requests only supported …: http…

Then I tried with local proxy server which was already helpful earlier
The error is now:
Access at ‘http://localhost:4004/…’ from origin ‘http://localhost:8080’… blocked by CORS:
Response to preflight request doesn’t pass access control check:
No ‘Access-Control-Allow-Origin’ header present…

The typical error which we all hate and which makes our life difficult
There are several solutions/workarounds possible, I wanted to try the solution which is based on:
app router
This solution is both elegant and reusable:
It makes sense because most UI5 apps anyways will make use of app router sooner or later, also when deploying to the cloud.
See here how it works

Prerequisites

OData service:
We’re using CAP (Cloud Application Programming Model) to create an OData service, because it’s quick and easy to run locally
We cannot explain CAP here, so if you aren’t familiar with CAP, please check the Getting Started section
Alternatively, if you have an existing remote OData service, you can also use that as well

UI5:
I’m not introducing UI5 here, because anyways, I’m not familiar with it.
You can use your own UI5 application, or you can create the silly app as described in this tutorial

App Router:
Please go through this blog to learn how to install app router locally

Overview

In this tutorial, we want to showcase local development scenario.
However, remote OData service can be used as well

At the end, the apps can be deployed, if desired (see appendix 1)

We’ll be going through the following steps:
1. Create OData service and run locally
2. Create UI5 app and try to run locally (error)
3. Create App Router and run locally
4. Adapt UI5 app and successfully run locally
5. Optional: deploy

1. Create OData Service

(If you already have an OData service, local or remote, you can skip this chapter)

1.1.  Create Project Structure

Create Root folder for our project: C:\tmp_cors
Inside, create another root folder for our CAP project: prodsrv
Inside, create package.json file
In prodsrv folder, create db folder for data model
In db folder, create file with name schema.cds
In db folder, create folder with name csv
In csv folder, create file with name com.example.business-Products.csv
Back in prodsrv folder, create srv folder (next to db folder)
In srv folder, create file with name service.cds

Summarizing the folder structure:

C:\tmp_cors
prodsrv
db
csv
Products.csv
schema.cds
srv
service.cds
package.json

Or see screenshot:

1.2. Copy File content

As you can see, our OData service is very simple and the file content minimalistic.
Refer to appendix for the full file content

package.json

Here we define the dependency to CAP and a sqlite database which we need to start with some sample data

schema.cds

Our very simplistic data model:

entity Products{
    key productID : String;
    name : String;
}

Products.csv

Initialize database with some silly sample data, otherwise we don’t see anything in the ui

service.cds

In our service definition, we expose a service with silly name “ProductService”

using Products as prods from '../db/schema';

service ProductService {
  entity Products as projection on prods
}

1.3. Install

Open command prompt, jump into C:\tmp_cors\prodsrv and run
npm install
see:

Note:
If you get errors related to database, you might need to run
npm install sqlite3 -D

1.4. Start Server

In prodsrv folder, run
cds deploy
This will initialize the local database with the sample data of csv file.
As a result, a .db file is generated in our project folder

In same folder, run
cds run
This will generate all required artifacts from the model files, connect to the database, and after very short time, the server is up and running

1.5. Test OData Service

Open the following URLs in browser:
http://localhost:4004 to get an overview of available URLs
http://localhost:4004/product to view the service document
http://localhost:4004/product/Products to view the list of sample products
In fact, we can see the 2 entries of products which we entered in the csv file

2. Create UI5 App

(If you have a UI5 app, you can skip this chapter)
Now that our OData service is running locally, we can build a UI5 app
It should just contain a list which is bound to the Products entity set of our OData service
As a consequence, the UI5 will fire a GET request to our server.
And this is what causes problems…

2.1. Create Project Structure

The base root folder for our project already exists: C:\tmp_cors
Inside, create another root folder for our UI5 app: produi
Inside, create a folder webapp
Inside webapp folder, create the following 4 files:
App.view.xml
Component.js
index.html
manifest.json

The folder structure looks like this:

C:\tmp_cors
produi
webapp
App.view.xml
Component.js
index.html
manifest.json

See screenshot

2.2. Copy File content

According to our OData service, our UI5 application is very simple and silly as well.
It contains  just what we need in order to show the error
Refer to appendix for the full file content

manifest.json

The app descriptor defines a model, to be used by the ui, and a data source which points to our local server
(Here you can also enter a remote URL pointing to your OData service)

{
	"sap.app": {
		"dataSources": {
			"products_datasource": {
				"uri": "http://localhost:4004/product/",
				...
	"sap.ui5": {
		"models": {	
			"prodmodel": {
				"dataSource": "products_datasource",
				...

App.view.xml

The view descriptor contains a view definition with a list:
Here we can see the binding to the entity set “Products” of our defined model

<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
	...
			<List items="{path : 'prodmodel>/Products'}"> 		

index.html

The usual main entry point for a web application

Component.js

The usual component class

2.3. Open the Application

When opening the index.html in a browser, we expect to see the running app.
However, we see an empty screen and in the logs (press F12 to open the developer tools of browser) we see the error message:

Access to XMLHttpRequest at ‘file:///C:/tmp_cors/produi/webapp/manifest.json?sap-language=EN’ from origin ‘null’ has been blocked by CORS policy:
Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

Intermediate Chapter: try proxy server

In my previous blog, I showed how to solve CORS issue with proxy server.
This is useful as long as we don’t call a remote service.
If we try starting proxy server and opening our app at localhost:8080, the error changes to:

Access …’//localhost:4004…’ from origin ‘//localhost:8080’ has been blocked by CORS policy:
No ‘Access-Control-Allow-Origin’ header present on requested resource.

That’s interesting, but useless…

3. Solution: Add App Router

(If you already have an app router you can skip this chapter – you can even skip this blog…)
App Router usually serves as main entry point for an application
It can handle user authentication and forward jwt token to defined routes
Routes are based on destinations
You can go through this blog and this one to get familiar with app router.
And this blog explains how to set up the app router for local development, which is exactly what we need today:

3.1. Create files

We don’t create a separate project for app router. That would be possible, but for our use case, we just add the required files to our existing ui5 app folder

Step into the folder  C:\tmp_cors\produi and create these 3 files
package.json
xs-app.json
default-env.json

The folder structure looks now like this:

3.2. Copy File Content

Refer to appendix for the full file content

package.json

As described in the other blogs, the app router is a node.js module. By defining a dependency and by adding a start script, we can easily install and start it

{
  "dependencies": {
    "@sap/approuter": "^6.8.2"
  },
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  }
}

xs-app.json

In the app router descriptor file, we define a route to the destination which represents our OData service
This route will be used by our ui5 app (don’t forget to adapt it, as described below)
Furthermore, we define a route for the entry point of our application, which is the index.html file
This route is used also in the path of the welcome file.
Defining a welcome file is useful, because like that we don’t need to type the full path to index.html. It will automatically be opened

{
  "welcomeFile": "home/index.html",
  "authenticationMethod": "none",
  "routes": [
    {
      "source": "^/home/(.*)$",
      "target": "$1",
      "localDir": "webapp"
    },
    {
      "source": "^/route_to_prodsrv/(.*)$",
      "target": "$1",
      "destination": "env_destination_prodsrv",
      "csrfProtection": false
    }
  ]
}

default-env.json

This file must have this name, as it is hardcoded in the app router
If the file is present, the app router will parse it
As such, we can define environment variables here, and that’s what we need for our local scenario
We need only one destination, it points to our local OData service:

{
    "destinations" : [
          {
              "name": "env_destination_prodsrv",
              "url": "https://prodsrv.cfapps.eu10.hana.ondemand.com"
          }
    ]  
}

3.3. Install App Router

Installation is again based on package.json
Open command prompt, jump into the folder C:\tmp_cors\produi and execute
npm install
Et voilà: app router is there

3.4. Start App Router

In command prompt, same directory (next to package.json) execute
npm start
This will start the server, write some logs to the console, at the end it says:
Application router is listening on port: 5000

3.5. Test App Router

Now that the app router is running, we can try the route which we defined above:
http://localhost:5000/route_to_prodsrv/

Note:
Make sure not to forget the trailing slash, otherwise the route doesn’t match

As a result, the products collection is displayed

Note:
Make sure that the OData service is running, see chapter 1.4

4. Adapt UI5 App

(If you’ve already adapted your app, you can skip this blog)
Now that we have the app router app and running, sorry, up and running, and we have checked that the route is working as desired, we can use it from our ui5 app

Remember: in the manifest.json file of the ui5 app, we hardcoded the URL of the data source.
Now it is time to change that.

4.1. Adapt manifest.json file

Change datasource uri from

"uri": "http://localhost:4004/product/",

to

"uri": "http://localhost:5000/route_to_prodsrv/product/", 

Note:
Don’t forget the trailing slash

While this solution already works…
mmmmm…
However….
there’s still a hardcoded URL, it points to the app router host.
We have an even better solution:

We can directly use a route
See:

"uri": "/route_to_prodsrv/product/",

This is of course much better and will work fine when deploying the app to the SAP Cloud Platform

4.2. Open the App

In your browser, you can now just type
localhost:5000
App router will detect that a welcome file is declared and thus it will forward to the “home”-route which we defined and which points to the index.html file
As a result, we can see our app
And it displays the list of products, not errors:

Summary

We’ve solved the CORS issue (during local development) by
adding app router our UI5 modulea
and adapting the manifest.json file of our UI5 app

Links

CAP: https://cap.cloud.sap/

SAP UI5:
Docu: https://sapui5.hana.ondemand.com/
Blog: Solving CORS with local proxy server

App Router:
SAP Help Portal: Application Router documentation entry page
Blog: Intro learnings for app router
Blog: more learnings
Blog: local app router

Quick Reference

Solve the CORS issue (during local development) by

1. adding 3 files to UI5 module:
package.json to install app router
xs-app.json to configure app router
default-env.json required for local app router

2: Adapt the manifest.json file of UI5 app:
data source points to app router

Appendix 1: Deploy to SAP Cloud Platform

Let’s do one optional exercise:
Once our project evolves, our OData service might get ready for deployment to the SAP Cloud Platform
But our app development still not ready, so we want to continue locally, but call the deployed OData service
How to do that?

1. Add manifest to prodsrv

In folder C:\tmp_cors\prodsrv create a file with name manifest.yml
The content:

---
applications:
- name: prodsrv
  memory: 128M
  buildpacks:
  - nodejs_buildpack

Note:
You might need to change the “name” to anything unique, otherwise deployment might fail

Then deploy the application
In folder C:\tmp_cors\prodsrv , run
cf push

In my example, this is the service URL after deployment
https://prodsrv.cfapps.eu10.hana.ondemand.com/product

2. Adapt ui5 app

Now we want to use that service from our app
To do so, we only need to change the destination (in default-env.json)

{
    "destinations" : [
          {
              "name": "env_destination_prodsrv",
              "url": "https://prodsrv.cfapps.eu10.hana.ondemand.com"
          }
    ]  
}

Note
After changes in configuration files of app router, make sure to restart app router

Note
In our tutorial, we stop the local CAP service, to make sure that the data is really fetched from remote service

3. Deploy UI5 app

In folder C:\tmp_cors\produi create a file with name manifest.yml
and content:

---
applications:
- name: produi
  env:
    destinations: >
      [
          {
              "name": "env_destination_prodsrv",
              "url": "https://prodsrv.cfapps.eu10.hana.ondemand.com"
          }
      ]

Deploy, then open the app at
https://produi.cfapps.eu10.hana.ondemand.com
It will open the app and show the products
You can check the logs with
cf logs produi –recent
You will see that our app router has received and forwarded requests

Appendix 2: All Project Files

1. CAP Project

manifest.yml

---
applications:
- name: prodsrv
  memory: 128M
  buildpacks:
  - nodejs_buildpack

package.json

{
  "dependencies": {
    "@sap/cds": "3.20.1",
    "express": "^4.17.1"
  },
  "devDependencies": {
    "sqlite3": "^4.2.0"
  },
  "scripts": {
    "start": "cds run"
  },
  "cds": {
    "requires": {
      "db": {
        "kind": "sqlite",
        "model": [
          "db",
          "srv"
        ]
      }
    }
  }
}

schema.cds

entity Products{
    key productID : String;
    name : String;
}

Products.csv

productID;name
HT-1000;Laptop
HT-1001;Smartphone

service.cds

using Products as prods from '../db/schema';

service ProductService {
  entity Products as projection on prods
}

2. UI5 Project

default-env.json

{
    "destinations" : [
          {
              "name": "env_destination_prodsrv",
              "url": "http://localhost:4004"
          }
    ]  
}

manifest.yml

---
applications:
- name: produi
  env:
    destinations: >
      [
          {
              "name": "env_destination_prodsrv",
              "url": "https://prodsrv.cfapps.eu10.hana.ondemand.com"
          }
      ]

package.json

{
  "dependencies": {
    "@sap/approuter": "^6.8.2"
  },
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  }
}

xs-app.json

{
  "welcomeFile": "home/index.html",
  "authenticationMethod": "none",
  "routes": [
    {
      "source": "^/home/(.*)$",
      "target": "$1",
      "localDir": "webapp"
    },
    {
      "source": "^/route_to_prodsrv/(.*)$",
      "target": "$1",
      "destination": "env_destination_prodsrv",
      "csrfProtection": false
    }
  ]
}

App.view.xml

<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
   <App id="app">
      <pages>
         <Page title="List of Products">
            <content>
               <List items="{path : 'prodmodel>/Products'}"> 		
                  <items>
                     <ObjectListItem
                        title="{prodmodel>productID}: {prodmodel>name}">
                     </ObjectListItem>
                  </items>
               </List>
            </content>
         </Page>
      </pages>
   </App>
</mvc:View>

Component.js

sap.ui.define(["sap/ui/core/UIComponent"], 
function (UIComponent) {
	return UIComponent.extend("com.local.prodapp.Component", {
		metadata : {
			manifest: "json"
		},
		init : function () {
			UIComponent.prototype.init.apply(this, arguments);
		},
	});
});

index.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<script
		id="sap-ui-bootstrap"
		src="https://ui5.sap.com/resources/sap-ui-core.js"
		data-sap-ui-theme="sap_belize"
		data-sap-ui-resourceroots='{"com.local.prodapp": "./"}'
		data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
		data-sap-ui-compatVersion="edge"
		data-sap-ui-async="true">
	</script>
</head>
<body class="sapUiBody" id="content">
	<div data-sap-ui-component data-name="com.local.prodapp" data-id="container" data-settings='{"id":"prodapp"}'></div>
</body>
</html>

manifest.json

{
	"sap.app": {
		"dataSources": {
			"products_datasource": {
				"uri": "/route_to_prodsrv/product/",
				"type": "OData",
				"settings": {
				  "odataVersion": "4.0"
				}
			}
		}		
	},
	"sap.ui5": {
		"rootView": {
			"viewName": "com.local.prodapp.App",
			"type": "XML"
		},
		"dependencies": {
			"libs": {
				"sap.ui.core": {},
				"sap.m": {}
			}
		},
		"models": {	
			"prodmodel": {
				"dataSource": "products_datasource",
				"settings": {
					"synchronizationMode" : "None",
					"groupId": "$direct"
				  }	
			}				
		}
	}
}

 

 

 

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.