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
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"
}
}
}
}
}
Hello 🙂
I am finding a similar issue, but in an SAP Web IDE full stack environment, do you think this would be possible to manage it with your solution ?
I am sending a POST call towards an outside API, that is supposed to send me back a json containing a link towards a chat. The CORS policy blocks it.
unfortunately, I am a UX dev and this is not my domain of expertise 🙂 this is why I seek your help !
Hello Alix PELLETIER ,
I'm not familiar with Web IDE and BAS (BTW, are you using Business Application Studio, BAS?) but I would expect that this would be the ideal solution, because the app router was developed especially for user-centric scenarios. But I haven't tried in Web IDE
Cheers,
Carlos