Announcement

Collapse
No announcement yet.
X
  • Filter
  • Time
Clear All
new posts

    isc.RPCManager.resendTransaction() "loses" HTTP headers

    We are currently using the relogin feature from the framework (with loginRequiredMarker, loginSuccessMarker et al).

    We just realized that isc.RPCManager.resendTransaction() does NOT re-use all the HTTP headers the original called used.

    We do use custom "X-[blah blah blah]" headers along with "Range" header. None of those are being sent back to the back-end when we call isc.RPCManager.resendTransaction(). We tried using resendTransaction() with OR without the "transactionNum" param.

    What do we need to do to have the headers sent to the back-end ?

    Thanks !

    Version: SmartClient_v91p_2015-03-19_Pro

    #2
    Just to let you know, an engineer is assigned to look into this. We will post back here when we have something more to say, or if we need more information.

    Thanks,
    Isomorphic Software Support

    Comment


      #3
      Glad to hear someone will look into the issue !

      In order to manage my expectations, can you please provide a rough ETA for the feedback ?

      Thanks !

      Comment


        #4
        Our initial testing does not reproduce this problem, in either 10.1 or 9.1 versions of the framework. Our little test program looks like this - are you doing something very different?
        Code:
        var c = 0;
        
        isc.RPCManager.handleError = function(req, resp) {
            if (c++ > 3) return;
            this.suspendTransaction(this.getTransactions()[0]);
            this.resendTransaction();
        }
        
        // "testResendHeaders" is a deliberately-broken DataSource - this fetch will always return an error
        testResendHeaders.fetchData({}, null, {httpHeaders:{"X-Test-Header":"xvalue",Range:"tangevalue"}});

        Comment


          #5
          We do not explicitly call suspendTransaction(); it gets called auto-magically through the framework.

          We do:

          RPCManager.addClassProperties
          ({
          loginRequired : function(transactionNum, rpcRequest, rpcResponse)
          {
          // We create and login window/dialog and display it in modal mode (OurLoginWindow below).
          }
          };

          var OurLoginWindow = isc.defineClass('OurLoginWindow', 'Window');
          OurLoginWindow.addProperties
          ({
          doLogin : function()
          {
          // Method invokes isc.RPCManager.sendRequest() -- with containsCredentials : true -- in order to start a secure session on the back-end.
          // The callback for isc.RPCManager.sendRequest() is loginReply() method below.
          },

          loginReply : function(rpcResponse)
          {
          // We perform the login routine/house-keeping...

          //and finally call:
          isc.RPCManager.delayCall("resendTransaction");
          }
          }

          The code snippets above summarizes our relogin implementation.

          The example you provided is much simpler because it doesn't perform any RPC operation between the call to suspendTransaction() and resendTransaction(). Can I call to RPCManager.sendRequest() "corrupt" the request properties ?

          Thanks !

          Comment


            #6
            We have made further, more detailed attempts to reproduce this problem, but without success.
            Code:
            isc.DataSource.create({
                ID: "loginRequired",
                dataURL: "isomorphic/login/loginRequiredMarker.html"
            });
            
            var c = 0;
            isc.RPCManager.loginRequired = function(trxId, req, resp) {
                if (c++ > 3) return;
                handleLogin();
            }
            
            function handleLogin() {
                worldDS.fetchData(null, function() {
                    isc.RPCManager.delayCall("resendTransaction");
                }, {containsCredentials: true, willHandleErrors: true});
            }
            
            // Not using "Range" because it causes weird error responses for normal static HTML fetches
            loginRequired.fetchData({}, null, {httpHeaders:{"X-Test-Header":"xvalue","X-Range":"rangevalue"}});
            This test case is closer to your problem case in quite a few respects: it doesn't manually suspend the transaction but rather allows the loginRequired system to do so, it adds an intervening DSRequest with containsCredentials specified to simulate your authentication step, and it does things in RPC and delayCall() callbacks similar to the process you describe. In this testcase and every other we have tried, the headers from the original DSRequest are resent however many times the transaction is resent. This is true in current 10.1, current 9.1 and in the 9.1 build of 3/21/2015 (which is the closest we can easily get to the version you are using). To take this any further, we are going to need a complete sample that demonstrates the failure.

            Thanks,
            Isomorphic Support

            Comment


              #7
              I understand the changes you made to bring your example closer to mine; thanks for that !

              However, a big difference is your example only relies on calls to DataSource.fetchData() whereas our code relies on explicit calls to isc.RPCManager.sendRequest().

              Could that make a difference ?

              I couldn't figure out a way to use the code snippet you provided in the previous post and trace into it. What did you use for the definition of "worldDS" ? I'd like to use the debug version of the framework to dig into the transaction resuming using your example and our "real code" to try to see where the code behaves differently.

              Thanks !

              PS: I can't come up with an example that is isolated from our app since this is highly tied into our back-end and we do not currently have a version that is exposed to the outside world. :-(

              Comment


                #8
                worldDS is a standard DataSource shipped with SmartClient - the point of running fetchData() on it was simply to introduce an RPCManager request and callback into the flow. Any DataSource that returns a non-error response could be used to achieve exactly the same end. The actual definition of worldDS was done with a <loadDS>, which just got clipped off the top of that snippet. It read
                Code:
                <isomorphic:loadDS ID="worldDS"/>

                Comment


                  #9
                  Based on the provided feedback, I continued digging into our code to see what could mess up the headers.

                  I think I put the finger on the problem, but I have a hard-time explaining the reason for it. I'd really like you to help me understand the framework so I can have high confidence that I identified the root cause of the problem.

                  All our datasources inherit from our BaseDataSource class. Here are some extracts from the code that contains the undesired behaviour:
                  Code:
                  var BaseDataSource = isc.defineClass('BaseDataSource', 'DataSource');
                  
                  BaseDataSource.addClassProperties
                  ({
                      HTTP_HEADER_RANGE : 'Range',
                      HTTP_HEADER_MAX_ROW_COUNT : 'X-MaxRowCount',
                      HTTP_HEADER_SORTING : 'X-Sorting',
                  ....
                  });
                  
                  BaseDataSource.addProperties
                  ({
                      dataFormat : 'json',
                      operationBindings :
                      [
                          { operationType : 'fetch', dataProtocol : 'getParams', dataFormat : 'json',
                              requestProperties : { httpMethod : 'GET', contentType : 'application/json',
                                  httpHeaders : { 'Accept' : 'application/json' }}},
                  
                          { operationType : 'remove', dataProtocol : 'getParams', dataFormat : 'json',
                              requestProperties : { httpMethod : 'DELETE', contentType : 'application/json',
                                  httpHeaders : { 'Accept' : 'application/json' }}},
                  
                          { operationType : 'add', dataProtocol : 'postMessage', dataFormat : 'json',
                              requestProperties : { httpMethod : 'POST', contentType : 'application/json',
                                  httpHeaders : { 'Accept' : 'application/json' }}},
                  
                          { operationType : 'update', dataProtocol : 'postMessage', dataFormat : 'json',
                              requestProperties : { httpMethod : 'PUT', contentType : 'application/json',
                              httpHeaders : { 'Accept' : 'application/json' }}}
                      ],
                  ....
                  
                      //--------------------------------------------------------------------------------------------------------------------------------
                      transformRequest : function(dsRequest)
                      {
                          this.transformRequestForAccessRights(dsRequest);
                          this.transformRequestForErrorHandling(dsRequest);
                          this.transformRequestForDataScrolling(dsRequest);
                          this.transformRequestForDataSorting(dsRequest);
                  
                          return dsRequest.data;
                      },
                  
                      //--------------------------------------------------------------------------------------------------------------------------------
                      // For fetch requests, include appropriate request information in order
                      // for backend to return the appropriate data range.
                      //--------------------------------------------------------------------------------------------------------------------------------
                      transformRequestForDataScrolling : function(dsRequest)
                      {
                          if (dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_RANGE])
                              delete dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_RANGE];
                  
                          if (dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_MAX_ROW_COUNT])
                              delete dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_MAX_ROW_COUNT];
                  
                          // if paging is not requested, don't add HTTP Range header to request
                  
                          if (dsRequest['dataFetchMode'] && dsRequest['dataFetchMode'] == 'basic')
                              return;
                  
                          if (this.isFetchRequest(dsRequest))
                          {
                              if(dsRequest.startRow != undefined && dsRequest.endRow != undefined)
                              {
                                  var startRow = dsRequest.startRow;
                                  var endRow = dsRequest.endRow;
                  
                                  dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_RANGE] = 'items=' + startRow + '-' + endRow;
                              }
                              else if(dsRequest.maxRowCount != undefined)
                              {
                                  dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_MAX_ROW_COUNT] = dsRequest.maxRowCount;
                              }
                          }
                      },
                  
                      //--------------------------------------------------------------------------------------------------------------------------------
                      transformRequestForDataSorting : function(dsRequest)
                      {
                          if (dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_SORTING])
                              delete dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_SORTING];
                  
                          if (this.isFetchRequest(dsRequest) && dsRequest.sortBy)
                          {
                              if (dsRequest.sortBy.length == 0)
                                  return;
                  
                              // Transposing dsRequest.sortBy[] to a http header param name 'X-Sorting'
                              // Values are separated by ';' and prefixed with '+' for ascending and '-' for descending
                              // by default, SmartClient already prefixes descending with '-'
                  
                              var value = '';
                  
                              for (var i = 0; i < dsRequest.sortBy.length; i++)
                              {
                                  if (value.length > 0)
                                      value += ';';
                  
                                  if (dsRequest.sortBy[i].substring(0,1) != '-')
                                      value += '+' + this.getValueXPathForFieldName(dsRequest.sortBy[i]); // prefixing field name with '+' when ascending
                                  else
                                      value += '-' + this.getValueXPathForFieldName(dsRequest.sortBy[i].substring(1, dsRequest.sortBy[i].length)); // remove '-' to search value XPath
                              }
                  
                              dsRequest.httpHeaders[BaseDataSource.HTTP_HEADER_SORTING] = value;
                          }
                      },
                  
                  ....
                  });
                  For a reason I can unfortunately NOT explain (SVN repo migration lost history/commit comments at some point), most of our transformRequestFor*(dsRequest) methods start by deleting HTTP headers when they are there. This is currently required because HTTP headers need to be sanitized before every request. As a start, it's suspicious to have to clean up from one request to another.

                  Now that I dug into the code a bit more, I do not understand why this is needed ? Why are some HTTP headers already there ?

                  I was not able to establish how dsRequest gets initialized for the requests ? Is it possible that the framework performs a _shallow_ copy of an operationBindings' "requestProperties" and we end up with the same requestProperties.httpHeaders object for all requests instead of getting a clean new one for every request ?

                  Since some parts of our code assumes that dsRequest.httpHeaders is an available object, I changed our code to remove "httpHeaders" from "operationBindings".
                  Instead, at the beginning of transformRequest(), I added:
                  Code:
                          dsRequest.httpHeaders = dsRequest.httpHeaders || { 'Accept' : 'application/json' };
                  I'm still in the early stage of testing, but this change seems to fix our problematic behaviour.

                  Can you please confirm I'm using a best practice for operationBingins' default HTTP header handling ?

                  Thanks in advance !




                  Comment


                    #10
                    Your guess is correct - operationBinding.requestProperties is indeed being applied with a shallow copy. We will apply a change to correct this, and your original approach will then work as you intended.

                    Thanks for bringing this to our attention. For future reference, please share all relevant details when reporting issues; if we had known about your header-manipulation code upfront, we would very likely have got to a resolution more quickly and with less effort.

                    Thanks,
                    Isomorphic Software Support

                    Comment

                    Working...
                    X