Table of contents:
Recently I discovered a bug in one of my SAPUI5 applications which I implemented for a client. However, the bug only occurred on Safari running on Mac OS X (Mac OS X 10.9.4, Safari 7.0.6). Back at home I debugged very deep into SAPUI5 and datajs.js (the OData JavaScript library used by SAPUI5). Here are my findings and a solution for fixing the issues I was facing with SAPUI5 1.18.6 and the corresponding datajs.js version. At the time of writing this tutorial the latest version of datajs.js was version 1.1.2, which holds all the issues described in this tutorial.
I have an SAPUI5 application which executes OData calls to SAP Gateway 2.0. My CREATE_ENTITY calls are validated on the backend inside my ABAP code. Potential errors are returned to the frontend (including a corresponding HTTP Status code). As an example let’s assume we have modeled an Employee entity Employees Set called Employees on the backend which has exactly three properties: employeeId, firstName and lastName. firstName and lastName have to be filled, else the backend rejects the creation of the entity and returns the error information to the client.
Our SAPUI5/JavaScript code to create an entity would look something like this:
var oNewEmployee, oModel; oNewEmployee = { firstName : "", lastName : "" }; oModel = new sap.ui.model.odata.ODataModel("/sap/opu/odata/sap/employee.svc/", true); oModel.create('/Employees', oNewEmployee, null, function(oData, oResponse){ sap.ui.commons.MessageBox.show("Employee was created", sap.ui.commons.MessageBox.Icon.SUCCESS, "SUCCESS", [sap.ui.commons.MessageBox.Action.OK], null, sap.ui.commons.MessageBox.Action.OK ); }, function(oError){ var oErrorData, oResponse, sContentType; oErrorData = oError.oData; oResponse = oErrorData.response; sContentType = oResponse.headers['content-type']; //FIXME This causes an error on OS X Safari! //… } );
Inside the error handler (see code snippet above) I need to access the Content-Type of the Response-Header which we receive from the SAP Gateway. Based on that information I can decide what to do next (not relevant here). The code above works perfectly on all browsers with one exception: Mac OS X Safari. While oResponse.headers['content-type'] is executed just fine on all the other browsers Safari on Mac OS X throws an exception. I have found out that oResponse.headers['Content-Type'] works on Safari, however, I was confused because I know that according to the HTTP specs header field names are case-insensitive (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html). That means when you access your header fields in either a case-sensitive or case-insensitive way you should always get the same result. Here is a jQuery example to prove it (I am too lazy for plain old JavaScript):
$.ajax({ type: "GET", url: "/sap/opu/odata/sap/employee.svc/Employees('12345')?$format=json", success: function(data, textStatus, request){ alert(request.getResponseHeader('Content-type')); }, error: function (request, textStatus, errorThrown) { alert(request.getResponseHeader('Content-type')); } });
In the code above you could also replace Content-type with for example conTent-tYpe - the result will still be the same. Now let’s see what actually happens in Google Chrome and Safari if you call request.getAllResponseHeaders() on a Mac for the same URL using jQuery:
$.ajax({ type: "GET", url: "/sap/opu/odata/sap/employee.svc/Employees('12345')?$format=json", success: function(data, textStatus, request){ alert(request.getAllResponseHeaders()); }, error: function (request, textStatus, errorThrown) { alert(request.getAllResponseHeaders()); } });
The following screenshots illustrate the result for Chrome and Safari on MAC OS X as well as for Internet Explorer 9 on Windows 7:
Can you already see the difference in the screenshots above? As I have debugged I found out that I receive lower-case header fields from the SAP Gateway. However, in the debugging tools of Max OS X Safari I can see that Safari seems to change this. For example, this is what I see in the Dev Tools of Chrome (left) and Safari (right) on my Mac for the same OData Request:
So it seems that Safari changes header field names to something that is “probably” better readable for human beings, i.e. 'content-type' is changed to 'Content-Type'. Let’s keep this in mind for later.
After debugging SAPUI5 down to the corresponding datajs.js OData calls I have found some lines of code that are suspicious. First of all, in datajs.js SAP has changed the original code here and there, i.e. the function readResponseHeaders has been modified by SAP. This function is actually from the datajs.js library and it is called to extract the headers of an Ajax response and to put them into a map (which is an associative array). Have a look into datajs.js to see where readResponseHeaders is called. As you will see in the datajs.js code the array filled by readResponseHeaders is passed into a response object which is then actually forwarded in the chain to my SAPUI5 error handler:
//somewhere in datajs.js //… var headers = []; readResponseHeaders(xhr, headers); var response = { requestUri: url, statusCode: statusCode, statusText: statusText, headers: headers, body: xhr.responseText }; //…
Now let’s return to the finding that Safari changes 'content-type' to 'Content-Type'. In datajs.js I have found a handy function, which normalizes the headers:
//daja.js var normalHeaders = { "accept": "Accept", "content-type": "Content-Type", "dataserviceversion": "DataServiceVersion", "maxdataserviceversion": "MaxDataServiceVersion" }; var normalizeHeaders = function (headers) { /// <summary>Normalizes headers so they can be found with consistent casing.</summary> /// <param name="headers" type="Object">Dictionary of name/value pairs.</param> for (var name in headers) { var lowerName = name.toLowerCase(); var normalName = normalHeaders[lowerName]; if (normalName && name !== normalName) { var val = headers[name]; delete headers[name]; headers[normalName] = val; } } };
I don’t exactly know why the function normalizeHeaders(headers) is not called when creating the response object (i.e. see Code Box 4 above). But what I know is that we have the following options to fix the Mac OS X Safari issue (choose the one you prefer):
//… var headers = []; readResponseHeaders(xhr, headers); normalizeHeaders(headers); var response = { requestUri: url, statusCode: statusCode, statusText: statusText, headers: headers, body: xhr.responseText }; //…
sContentType = oResponse.headers['Content-Type'];
//implementation is copied from datajs.js (we could also do it without prototype) sap.ui.model.odata.ODataModel.prototype._normalHeaders = { "accept": "Accept", "content-type": "Content-Type", "dataserviceversion": "DataServiceVersion", "maxdataserviceversion": "MaxDataServiceVersion" }; sap.ui.model.odata.ODataModel.prototype._normalizeHeaders = function(headers){ for (var name in headers) { var lowerName = name.toLowerCase(); var normalName = this._normalHeaders[lowerName]; if (normalName && name !== normalName) { var val = headers[name]; delete headers[name]; headers[normalName] = val; } } };
function _handleError(oError) {
if (fnError) { this._normalizeHeaders(oError.response.headers); fnError(oError); }
sContentType = oResponse.headers['Content-Type'];
function(oError){ //implementation is copied from datajs.js (we could also do it without prototype) var oErrorData, oResponse, aHeaders, name, sContentType, normalHeaders, lowerName, normalName, val; oErrorData = oError.oData; oResponse = oErrorData.response; aHeaders = oResponse.headers normalHeaders = { "accept": "Accept", "content-type": "Content-Type", "dataserviceversion": "DataServiceVersion", "maxdataserviceversion": "MaxDataServiceVersion" }; for (name in aHeaders) { lowerName = name.toLowerCase(); normalName = normalHeaders[lowerName]; if (normalName && name !== normalName) { val = aHeaders [name]; delete aHeaders [name]; aHeaders [normalName] = val; } } oErrorData = oError.oData; oResponse = oErrorData.response; sContentType = aHeaders['Content-Type']; //This works now even in Max OS X Safari //… }
I wrote this tutorial to describe what can go wrong if you pass response headers through an own array in your public APIs. To make it short, the learnings are: