Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sap.ui.model.odata.v4.ODataModel cannot be used with a standard odata4 service without CSRF token support #2288

Closed
cjohn001 opened this issue Nov 13, 2018 · 21 comments
Assignees

Comments

@cjohn001
Copy link

OpenUI5 version:
openui5-sdk-1.58.5

Browser/version (+device/version):
Google Chrome Version 70.0.3538.77 (Offizieller Build) (64-Bit)

Any other tested browsers/devices(OK/FAIL):
n/a

URL (minimal example if possible):
n/a

User/password (if required and possible - do not post any confidential information here):
n/a

Steps to reproduce the problem:

  1. Connect to an odata4 service without X-CSRF-Token support. I am using TEIID 11.2 on Wildfly 11.0

What is the expected result?

  1. odata service accessible
  2. it should be possible to disable CSRF token requests, similar to the sap.ui.model.odata.v2.ODataModel

What happens instead?
I see two issues here.

  1. OpenUI5 sends a HEAD request to the services main url, which is not specified in the odata v4 spec
Request URL: http://localhost:9001/odata4/svc/my_nutri_diary/
Request Method: HEAD
Status Code: 405 Method Not Allowed
Remote Address: [::1]:9001
Referrer Policy: no-referrer-when-downgrade
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: de,de-DE;q=0.9,en;q=0.8
Authorization: Basic SU1TVXNlcjpJTVM0Zm9ydW0l
Connection: keep-alive
Cookie: sidebar_collapsed=false; cycle_analytics_help_dismissed=1; __utmz=111872281.1539128843.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=111872281.767670437.1539128843.1541866362.1541870562.42
DNT: 1
Host: localhost:9001
Referer: http://localhost:9001/index2.html
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
X-CSRF-Token: Fetch
X-Requested-With: XMLHttpRequest
Any other information? (attach screenshot if possible)
  1. Usage of X-CSRF-Token is not a defined precondition according to the odata v4 standard. For OpenUI5 to be useful with other services than SAP Gateway it should be possible to disable the token requests like it is in the sap.ui.model.odata.v2.ODataModel
@ThomasChadzelek ThomasChadzelek self-assigned this Nov 14, 2018
@ThomasChadzelek
Copy link
Member

Hello @cjohn001 !

Thanks a lot for your interest in OData V4. Apart from the failed network request, do you experience any other issues?

From our perspective, X-CSRF-Token is not a prerequisite. We usually try to fetch it early on, but everything should work fine without it (as long as the server does not require it). You might want to turn off earlyRequests, which triggers the early request of $metadata and X-CSRF-Token. Both will be requested on demand, then.

We apologize for any inconvenience caused.

Best regards,
Thomas

@cjohn001
Copy link
Author

cjohn001 commented Nov 15, 2018

Hello Thomas,
thanks for your feedback. I have switched off earlyRequests and also had to set "groupId": "$direct" instead of "$auto". The file is now loaded without http errors. Is it possible, that I am missing a http header in the server response, that $direct is not working? However, this is only the second most important topic :) I do not have the basic model up and running yet. Once the metadata file has been loaded, I run into parsing errors. I have added a collection in the following way.

<sap.m.List width="100%" items="{/Account}">
 <items>
 <DisplayListItem label="{uuidUser}" class="mndTransparencyStyle"/>
</items>
</sap.m.List>  

And I am now ending up with the following errors

sap-ui-core.js:179 2018-11-15 00:03:17.985199 Failed to update cache for binding sap.ui.model.odata.v4.ODataListBinding: /Account - TypeError: Cannot read property 'indexOf' of null
    at V.a.resolveAlias (http://localhost:9001/resources/sap/ui/core/library-preload.js:3689:46)
    at V.a.processAnnotation (http://localhost:9001/resources/sap/ui/core/library-preload.js:3682:89)
    at V.processElement (http://localhost:9001/resources/sap/ui/core/library-preload.js:3851:50)
    at V.a.traverse (http://localhost:9001/resources/sap/ui/core/library-preload.js:3695:203)
    at V.a.traverse (http://localhost:9001/resources/sap/ui/core/library-preload.js:3695:459)
    at V.a.traverse (http://localhost:9001/resources/sap/ui/core/library-preload.js:3695:459)
    at V.a.traverse (http://localhost:9001/resources/sap/ui/core/library-preload.js:3695:459)
    at V.a.traverse (http://localhost:9001/resources/sap/ui/core/library-preload.js:3695:459)
    at V.a.traverse (http://localhost:9001/resources/sap/ui/core/library-preload.js:3695:459)
    at V.a.convertXMLMetadata (http://localhost:9001/resources/sap/ui/core/library-preload.js:3662:364) sap.ui.model.odata.v4.ODataParentBinding

I also attach the metadata file:

<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
<edmx:Reference Uri="http://localhost/odata4/static/org.teiid.v1.xml">
<edmx:Include Namespace="org.teiid.v1" Alias="teiid"/>
</edmx:Reference>
<edmx:Reference Uri="http://localhost/odata4/static/org.apache.olingo.v1.xml">
<edmx:Include Namespace="org.apache.olingo.v1" Alias="olingo-extensions"/>
</edmx:Reference>
<edmx:DataServices>
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="svc.1.my_nutri_diary" Alias="my_nutri_diary">
<EntityType Name="Account">
<Key>
<PropertyRef Name="uuidUser"/>
</Key>
<Property Name="uuidUser" Type="Edm.String" Nullable="false" MaxLength="36">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`uuidUser`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.CASE_SENSITIVE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.SIGNED">
<Bool>false</Bool>
</Annotation>
</Property>
<Property Name="idProfile" Type="Edm.Int64" Nullable="false">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`idProfile`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<NavigationProperty Name="Avatar_fkProfileInAvatar" Type="my_nutri_diary.Avatar" Nullable="false"/>
<NavigationProperty Name="BodyWeight_fkProfileInBodyWeight" Type="Collection(my_nutri_diary.BodyWeight)"/>
<NavigationProperty Name="Profile_fkProfileInBiometricProfile" Type="my_nutri_diary.Profile" Nullable="false"/>
<Annotation Term="teiid.NAMEINSOURCE">
<String>`Account`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</EntityType>
<EntityType Name="Avatar">
<Key>
<PropertyRef Name="fkProfile"/>
</Key>
<Property Name="fkProfile" Type="Edm.Int64" Nullable="false">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`fkProfile`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<Property Name="AvatarImg" Type="Edm.Stream">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`AvatarImg`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.SIGNED">
<Bool>false</Bool>
</Annotation>
</Property>
<NavigationProperty Name="fkProfileInAvatar" Type="my_nutri_diary.Account" Nullable="false">
<ReferentialConstraint Property="fkProfile" ReferencedProperty="idProfile"/>
</NavigationProperty>
<Annotation Term="teiid.NAMEINSOURCE">
<String>`Avatar`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</EntityType>
<EntityType Name="BodyWeight">
<Key>
<PropertyRef Name="idBodyWeight"/>
</Key>
<Property Name="idBodyWeight" Type="Edm.Int64" Nullable="false">
<Annotation>
<String>
We need a surrogate key here as Teiid requires a primary key on each table. fkProfile is not unique here as we can will have multiple weight measurements per person. Moreover, combining it with WeightMeasurementDateTime makes the index to compute intensive.
</String>
</Annotation>
<Annotation Term="teiid.NAMEINSOURCE">
<String>`idBodyWeight`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<Property Name="fkProfile" Type="Edm.Int64" Nullable="false">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`fkProfile`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<Property Name="WeightMeasurementDateTime" Type="Edm.DateTimeOffset" Nullable="false" Precision="4">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`WeightMeasurementDateTime`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.SIGNED">
<Bool>false</Bool>
</Annotation>
</Property>
<Property Name="Weight" Type="Edm.Int16" Nullable="false">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`Weight`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<NavigationProperty Name="fkProfileInBodyWeight" Type="my_nutri_diary.Account" Nullable="false">
<ReferentialConstraint Property="fkProfile" ReferencedProperty="idProfile"/>
</NavigationProperty>
<Annotation Term="teiid.NAMEINSOURCE">
<String>`BodyWeight`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</EntityType>
<EntityType Name="Profile">
<Key>
<PropertyRef Name="fkProfile"/>
</Key>
<Property Name="fkProfile" Type="Edm.Int64" Nullable="false">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`fkProfile`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<Property Name="BodyHeight" Type="Edm.Int64">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`BodyHeight`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</Property>
<Property Name="Gender" Type="Edm.String" MaxLength="1">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`Gender`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.CASE_SENSITIVE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.SIGNED">
<Bool>false</Bool>
</Annotation>
</Property>
<Property Name="BirthDate" Type="Edm.Date">
<Annotation Term="teiid.NAMEINSOURCE">
<String>`BirthDate`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
<Annotation Term="teiid.SIGNED">
<Bool>false</Bool>
</Annotation>
</Property>
<NavigationProperty Name="fkProfileInBiometricProfile" Type="my_nutri_diary.Account" Nullable="false">
<ReferentialConstraint Property="fkProfile" ReferencedProperty="idProfile"/>
</NavigationProperty>
<Annotation Term="teiid.NAMEINSOURCE">
<String>`Profile`</String>
</Annotation>
<Annotation Term="teiid.UPDATABLE">
<Bool>true</Bool>
</Annotation>
</EntityType>
<EntityContainer Name="my_nutri_diary">
<EntitySet Name="Account" EntityType="my_nutri_diary.Account">
<NavigationPropertyBinding Path="Avatar_fkProfileInAvatar" Target="Avatar"/>
<NavigationPropertyBinding Path="BodyWeight_fkProfileInBodyWeight" Target="BodyWeight"/>
<NavigationPropertyBinding Path="Profile_fkProfileInBiometricProfile" Target="Profile"/>
</EntitySet>
<EntitySet Name="Avatar" EntityType="my_nutri_diary.Avatar">
<NavigationPropertyBinding Path="fkProfileInAvatar" Target="Account"/>
</EntitySet>
<EntitySet Name="BodyWeight" EntityType="my_nutri_diary.BodyWeight">
<NavigationPropertyBinding Path="fkProfileInBodyWeight" Target="Account"/>
</EntitySet>
<EntitySet Name="Profile" EntityType="my_nutri_diary.Profile">
<NavigationPropertyBinding Path="fkProfileInBiometricProfile" Target="Account"/>
</EntitySet>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>

@ThomasChadzelek
Copy link
Member

The "Term" attribute is missing here:
<Annotation> <String> We need a surrogate key here as Teiid requires a primary key on each table. fkProfile is not unique here as we can will have multiple weight measurements per person. Moreover, combining it with WeightMeasurementDateTime makes the index to compute intensive. </String> </Annotation>

@cjohn001
Copy link
Author

Hello Thomas,
many thanks! You saved my day. Now things are working. I have just one blocking issue left to come up with a smart solution :) The batch processing does not work yet with my wildfly server setup. I always get the following error on the post message. Do you know if this multipart/mixed header that is missing, is just a configuration issue I have with wildfly? Or is this some special functionality which is likely not supported from wildfly? Thanks for your help!

Request URL: http://localhost:9001/odata4/svc/my_nutri_diary/$batch
Request Method: POST
Status Code: 406 Not Acceptable
Remote Address: [::1]:9001
Referrer Policy: no-referrer-when-downgrade
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:9001
cache-control: no-cache, no-store, must-revalidate
connection: close
content-length: 124
content-type: application/json;odata.metadata=minimal
date: Thu, 15 Nov 2018 19:48:50 GMT
expires: 0
odata-version: 4.0
pragma: no-cache
server: WildFly/9
x-powered-by: Undertow/1
Accept: multipart/mixed
Accept-Encoding: gzip, deflate, br
Accept-Language: de
Authorization: Basic SU1TVXNlcjpJTVM0Zm9ydW0l
Connection: keep-alive
Content-Length: 346
Content-Type: multipart/mixed; boundary=batch_id-1542311330723-11
Cookie: sidebar_collapsed=false; cycle_analytics_help_dismissed=1; __utmz=111872281.1539128843.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=111872281.767670437.1539128843.1541866362.1541870562.42
DNT: 1
Host: localhost:9001
MIME-Version: 1.0
OData-MaxVersion: 4.0
OData-Version: 4.0
Origin: http://localhost:9001
Referer: http://localhost:9001/index2.html
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1
X-CSRF-Token: Fetch
X-Requested-With: XMLHttpRequest
--batch_id-1542311330723-11
Content-Type:application/http
Content-Transfer-Encoding:binary

GET Account?$select=uuidUser&$skip=0&$top=100 HTTP/1.1
Accept:application/json;odata.metadata=minimal;IEEE754Compatible=true
Accept-Language:de
Content-Type:application/json;charset=UTF-8;IEEE754Compatible=true

--batch_id-1542311330723-11--

@ThomasChadzelek
Copy link
Member

Hello @cjohn001 !

BEWARE: Authorization: Basic can reveal your user and password. It's not encrypted, just encoded. I would remove this header before I post publicly.

You refer to "this multipart/mixed header that is missing". That part is not clear to me. Why do you think that a header is missing? I could not understand that from the given information.

"406 Not Acceptable" means that the server cannot provide a response in a format which the client requested. Maybe it's Accept: multipart/mixed, maybe it's the Accept-Encoding: gzip, deflate, br header. That might appear in your server's log. (I see you are already discussing this at https://developer.jboss.org/thread/279113).

Best regards,
Thomas

@cjohn001
Copy link
Author

Hello Thomas,
thanks for pointing this with the password out. I know it is just Base64 encoded. I was just to lazy to remove it, as the account is just a test account running in a local vm without public access ;)
Indeed I was in parallel discussion the issue with the red hat guys, as I was not sure if it is a client or a server problem I am running into. Your assumption seems to be correct. The answer in the other forum said, that the wildfly server can process mutlipart messages but does not accept the Accept: multipart/mixed header. They suggest removing it and transmitting the message without the header. Than it should work. As I am not yet very familiar with javascript and OpenUI5, I am wondering if you might have a suggestion on how I could remove the header from the odata.v4 model. Do I need to change the OpenUI5 sources to get things running or might there be a more gentle approach? I have seen that the odata.v2 model has functions to add (and overwrite?) headers. But I have not found anything to remove a header yet or a similar approach for adding headers in a odata.v4 model. Thanks for your help. By the way, you were also right with the annotations which is a server error for which I filled a bug report.
Best regards,
Christoph

@ThomasChadzelek
Copy link
Member

Hello Christoph!

Our V4 OData model can only handle $batch as multipart (currently). Thus Accept: multipart/mixed is a perfectly fine header from our point of view. If wildfly knows how to handle multipart, it should learn to deal with this header as well. That's the piece that I recommend to fix, not UI5.

Best regards,
Thomas

@cjohn001
Copy link
Author

cjohn001 commented Jan 3, 2019

Hello Thomas,
sorry to contact you via this way but I found no different option. In the meantime I got the batch feature of OpenUI working against a Teiid-Wildfly / Olingo server, thanks to the help of some guys at RedHat. I was discussing with them if it would be possible to also extend Teiid/Olingo to support the earlyRequest feature of the OpenUI5 odata.v4 model and they would offer their support. I was wondering, if you could provide us with some more details on what is needed for this communication to happen. In my logs I can only see that OpenUI5 sends a head request on the main service URL. As head requests are not supported by Teiid yet, I cannot see what furthermore would be involved. Is there a more detailed description available for the earlyRequest feature which you could provide us? Currently, I have only found the 2 sentences in the odata models constructor description for the earlyRequest parameter. Thanks for your help.
Best regards,
Christoph

@ThomasChadzelek
Copy link
Member

Hello Christoph!

No worries. You're welcome.

The "earlyRequests" parameter is pretty simple. It will send GET requests for /$metadata and all annotation XML files (see parameter "annotationURI"). It will also send a HEAD request to with header "X-CSRF-Token" : "Fetch" in order to fetch the security token. If Teiid/Olingo does not implement CSRF protection by requiring such a header, there should be no need to properly answer the HEAD request at all. Else it would be nice to return the right CSRF token value in the response's headers.

Please see Cross-Site Request Forgery Protection and Gateway protection against Cross-Site Request Forgery attacks for some background infos.

Do not hesitate to ask for further details in case I missed anything.

Best regards,
Thomas

@cjohn001
Copy link
Author

cjohn001 commented Jan 7, 2019

Hello Thomas,
thanks for the detailed answer and for offering us support. This will help a lot. I will add a link to this thread in the Teiid Change Request which I will now create. If further questions should arise we will be grateful to come back on you regarding your support offer :)

I have one further topic for which I would like to ask you for help. I have seen your recording from UI5Con last year where you presented your work on the odata v4 model. So probably you are the best person to ask. I have a for myself hard to answer topic, regarding the correct use of the odata v4 model API. The tutorials, the documentation and my tries to get help in the openui5 forums have not helped me to solve the issue yet. As a matter of principle, I have different views in my OpenUI5 app which show the same odata v4 model data. As for as I understood and can observe, the odata v4 model does not support automatic syncing of changes between different views yet. To my understanding this is the expected behavior of synchronizationMode = none as well. I therefore try to implement syncing with custom logic and try to use the binding API for the odata v4 model from Javascript code.


Technical background: It is not clear to me, if

  • the ui controls just need to be redrawn after a data change in a different view and simply do not get an update event,
  • or if they have to reload the data from the model first, and then need to be redrawn. This is to say, it is not clear to me, if the controls store a local copy of the data from the odata model or it might even be the case that an odata model instance is created per contextbinding.

The question arising from this is, how can I enforce my controls to redraw/update data.


For an ODataContextBinding I could solve the topic to some extend. Not clear to me if my solution is the best possible one, as it leads to a probably unnecessary http request to update the odata model, before the control in the view is updated/redrawn. I would assume that such a http request should not be required in case there is a single underlaying odata model below all controls, which I assume is the case. I add my solution here in case others with the same question read this thread and also to discuss, if my solution is really the best way to do things.

Example for a Javascript based creation/refresh of a context binding:

// create a context binding, lets say in a controller of a View1
var oModel = this.getView().getModel();
// via a globally available context binding I do the syncing between the different views
gConfOptsContextBinding = oModel.bindContext("/ConfigOptions(" + gProfileID + ")");
// now I can async request a dataset
var oLengthUnit = gConfOptsContextBinding.getBoundContext().requestObject("MUnitLength");

//If I change the model now in a controller in a different View2, which is initialized to the same data as
//follows:
var oControl = this.getView().byId("theControl");
oControl.bindElement("/ConfigOptions(" + gProfileID + ")");

// the control can do an update on the data now and afterwards needs to call:
var oControlContextBinding = control. getBindingContext().submitBatch("auto").then(function(){
// once this is done it needs to call
gConfOptsContextBinding.refresh();
});

//Back in View1, I then implicitly reload the data from the server and rerender the ui control once I again call:
var oLengthUnit = gConfOptsContextBinding.getBoundContext().requestObject("MUnitLength");


Now the topic for which I have not found a solution at all is, how I can handle ODataListBindings from Javascript.

I can initialize a ODataListBinding as:
var WeightListBinding = oModel.bindList("/WeightMeasurements");
WeightListBinding.initialize(); // not clear if I need this?
WeightListBinding.refresh(); // not clear if I need this?
WeightListBinding.getContexts(); // from the documentation I would expect, that this should load the list item contexts from the server. However, I cannot observe a http request in the debugger.
The odata model stays empty for the path. So what am I doing wrong?

It would be great, if you could provide a code snippet to show, how I would correctly use the Binding API for an ODataListBinding. Thanks a lot for your help!

Best regards,
Christoph

@ThomasChadzelek
Copy link
Member

Hello Christoph!

It might be better to move the new discussion to a different issue, but I leave that up to you. If you do, please link it from here and I will follow-up there.

You are right, we have considered "cache synchronization" early on and thus introduced the synchronizationMode flag, but there is almost nothing implemented so far. It is a bit tricky to explain because you need to understand the concept of when (or why) a binding sends its own data requests. If it does, it stores the received data in a data structure we call "cache" and informs the attached controls, e.g. via a "change" event. The controls then call s.th. like PropertyBinding#getValue and get the data from that cache.

In your code, when you call bindContext("/...") or bindElement("/...") you create a v4.ODataContextBinding with an absolute binding path (starting with a slash). Each of these bindings will send its own data requests and that is why you experience the missing synchronization. The easiest solution would be to send just a single data request and reuse that binding's cache across different views. Then no synchronization is needed and you save HTTP requests. (Of course, it is only a good idea if you need more or less the same data, e.g. $select=*, and not totally different projections.)

You can call oControl.setBindingContext(gConfOptsContextBinding.getBoundContext()) to achieve this reuse. This should simplify your first example.

The list binding is a bit more complicated. Currently, it is meant to be used by a single control only and does not offer a good API for controller code. You already noticed that there is no counterpart for requestObject(). First of all, we should understand if you can do the reuse trick here as well. For that, we would need a better understanding of your data, which views you have and what they have in common resp. what differences they have. Until then, let me shed some light on how a control roughly uses a context binding, because that is what you need to do as a controller as well. Disclaimer: Not all of these methods are public!

In the simplest case, call getContexts(0, Infinity) to request all data. It should trigger a request automatically if the group ID is "$auto", else submitBatch is needed as usual. It returns an empty array with an "annotation" dataRequested : true. You should add a "change" listener to the binding and will be called back once the response arrives. Then call getContexts(0, Infinity) again and you get one v4.Context instance for each data row. Call getObject() on these or use them as a binding context for a detail view.

WeightListBinding.initialize(); // not needed
WeightListBinding.refresh(); // not needed

Best regards,
Thomas

P.S.: I wondered about this line of code: control. getBindingContext().submitBatch("auto")... getBindingContext() should return a v4.Context, but there is no submitBatch() there. Looks like .getModel() is missing in between.
P.P.S.: And then I thought "auto" looks a bit misleading. There is "$auto" which means you need not call submitBatch(), but w/o the $ this acts the same as "car" ;-) And then I think it is misleading to call it nearly the same as "$auto", but still call submitBatch() manually. But that's just a matter of taste.

@boghyon
Copy link
Contributor

boghyon commented Feb 24, 2019

@ThomasChadzelek It would be nice if there is an option to request only the $metadata early when earlyRequests is enabled. Currently, it doesn't seem to be possible. Here is an example: https://embed.plnkr.co/qatUyq/?show=manifest.json,preview

  1. In manifest.json, scroll down to sap.ui5/models/""/settings and add "earlyRequests": true
  2. Once the preview is reloaded, the service throws the error "405 Method Not Allowed" as described in the initial issue description.

The sample makes use of the TripPin service, which is the only registration-free V4 service available out there AFAIK. But it doesn't support handling CSRF tokens. I.e.: All demos with TripPin have to turn earlyRequests completely off.


In one of the previous comments:

You might want to turn off earlyRequests, which triggers the early request of $metadata and X-CSRF-Token. Both will be requested on demand, then.

I'm not sure if this is true. If earlyRequests is turned off, only the $metadata is requested on demand. The token is not requested at all.

@ThomasChadzelek
Copy link
Member

Hello @boghyon!

I agree that the error message "405 Method Not Allowed" in your developer tools is not nice, but then again it should not really hurt. It is no reason why the application should not work. If you experience issues, please describe them in more detail.

Regarding the 2nd question: if TripPin does not support CSRF tokens, then there is no need for the v4.ODataModel to request one on demand. It is never missing. What would you expect to happen?

Best regards,
Thomas

@boghyon
Copy link
Contributor

boghyon commented Feb 25, 2019

Hi @ThomasChadzelek

There are two issues I see with leaving the earlyRequests enabled:

  1. The service is used in many Q&A platforms (e.g. Stack Overflow) and people wonder why there are error messages in the console every time. I guess I can just point them to this discussion and tell them that the error doesn't do much, which would be nice if that's true.. (see 2).
  2. With earlyRequests enabled, the application actually fails to load the list (stuck in the busy state). BUT this happens only sometimes:
    1. As described in sap.ui.model.odata.v4.ODataModel cannot be used with a standard odata4 service without CSRF token support #2288 (comment), add "earlyRequests": true to the model settings in manifest.json
    2. Reload the preview panel several times until the app fails to load the list.

  • Windows 10
  • Chrome 72.0.3626.119

About the 2nd part: I was referring to the sentence "Both will be requested on demand" (#2288 (comment)). But in the app, I see only that the $metadata is requested when earlyRequests is turned off. Why aren't there any token requests in that case? How does v4.ODataModel determine when to ask for tokens and when not?

@ThomasChadzelek
Copy link
Member

Hello @boghyon !

I have to look into the issue with earlyRequests enabled...

About the 2nd part: the v4.ODataModel would fetch a CSRF token on demand if any request fails with status code 403 and header X-CSRF-Token: required. This is a server's way to tell the client that it really needs a CSRF token. The request is then repeated transparently for the application.

Best regards,
Thomas

@ThomasChadzelek
Copy link
Member

Hello @boghyon !

I tried to reproduce the failure via https://embed.plnkr.co/qatUyq/?show=manifest.json,preview, but saw it only once when I clicked that "Refresh live preview" button very quickly many times. W/o such a nervous finger, everything was fine except for an "Uncaught (in promise) Error: 405 Method Not Allowed" which has been fixed already for 1.63

If you can reproduce it, please attach more details from console or network tab. Sorry.

Best regards,
Thomas

@boghyon
Copy link
Contributor

boghyon commented Jul 23, 2019

Hello @ThomasChadzelek,

W/o such a nervous finger, everything was fine

Unfortunately, in JSBin, v4.ODataModel fails way too often if earlyRequests is enabled while using a service that doesn't support handling CSRF tokens (TripPin): https://jsbin.com/fudizuq/edit?js,output
The date can be shown only after refreshing the page several times, which I doubt users would actually do since the demo looks broken to begin with.


Succeeds sometimes

output jsbin com_huhuvuroru

with the previously mentioned error message:

Error 405 (Method Not Allowed)
HEAD https://...../ 405 (Method Not Allowed)

which is fine.

Breaks quite often

output jsbin com_huhuvuroru (crash)

with another error message:

Error 405 (Method Not Allowed) + Could not refresh security token
Uncaught (in promise) Error: Could not refresh security token: 405 Method Not Allowed

The app fails to work.


With earlyRequests: false, everything works as expected.

@uhlmannm
Copy link
Member

Hello @boghyon,

I could reproduce your issue and will check how that can be improved.

Generally, I would not recommend to use earlyRequests if the server does not require the x-csrf token as a pointless request goes out in this case.

Best regards
Mathias.

@uhlmannm
Copy link
Member

uhlmannm commented Jul 29, 2019

Hello @boghyon ,

your issue is caused by the following functionality. A request with method !== "GET" will wait for a pending x-csrf token request and be executed only if the related promise is resolved. This introduces the randomness you observe. If the token request is not pending, everything works fine.
Having written that, let me again stress that flag earlyRequests should be used only if the server requires x-csrf tokens and root metadata and x-csrf token are requested early.

To code against the special case of TripPin not supporting HEAD requests does not seem to be a good solution. We expect that productive services can handle HEAD requests.

The real solution would be to allow to specifically request only the root metadata early. This is a feature request.

Could you please explain whether and when you would require that feature for productive applications?

Best regards
Mathias.

@boghyon
Copy link
Contributor

boghyon commented Jul 29, 2019

Hi @uhlmannm

I see, thanks for the reply. So if the service would actually handle HEAD requests properly as expected but without the x-csrf support, then the application would at least keep working without breaking. Is that right?

The real solution would be to allow to specifically request only the root metadata early. This is a feature request.

Well, if my assumption above is true, I think it's ok not to have the feature.

earlyRequests should be used only if the server requires x-csrf tokens

It's a bit of a mystery to me, why fetching $metadata early requests CSRF tokens too even though OData specification doesn't necessarily mention that services must support handling CSRF tokens. But again, one error message that doesn't break the application shouldn't hurt much.

@uhlmannm
Copy link
Member

Hi @boghyon ,

I see, thanks for the reply. So if the service would actually handle HEAD requests properly as expected but without the x-csrf support, then the application would at least keep working without breaking. Is that right?

Yes, the HEAD request needs to be successful but it does not need to contain the x-csrf-token header.

earlyRequests should be used only if the server requires x-csrf tokens

It's a bit of a mystery to me, why fetching $metadata early requests CSRF tokens too even though OData specification doesn't necessarily mention that services must support handling CSRF tokens. But again, one error message that doesn't break the application shouldn't hurt much.

Well ... the two points came up together and SAP services have to do something about cross side request forgery. So this is basically how it ended up behind the same parameter without the possibility to just have one of the two.

Have you tried the following in your Component.js to enforce an early request of metadata without parameter earlyRequests?
this.getModel().getMetaModel().requestObject("/");

Best regards
Mathias.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants