Showing posts with label Ajax. Show all posts
Showing posts with label Ajax. Show all posts

Thursday, April 14, 2016

Issue with JQuery ajax request which uses CORS when JSONP is explicitly specified in request options

Sometime ago we faced with interesting issue: we use JSONP request via jQuery.ajax() to send data to another domain:

   1: jQuery.ajax({
   2:     url: url,
   3:     data: { ... },
   4:     dataType: "jsonp",
   5:     success: function(msg) {
   6:         ...
   7:     }
   8: });

On most sites it works as expected, i.e. it sends HTTP GET request and passes JSONP callback function name in query string parameter. But on one site because of some reason it sends HTTP OPTION verb to the same url, which as you probably know, happens when using CORS. Since our endpoint didn’t support CORS we got errors:

Chrome:

XMLHttpRequest cannot load http://example1.com. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin http://example2.com is therefore not allowed access. The response had HTTP status code 404.

FF:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://example1.com. (Reason: CORS header 'Access-Control-Allow-Origin' missing).

IE:

XMLHttpRequest: Network Error 0x80070005, Access is denied.

In order to fix the issue, i.e. in order to force jQuery to use JSONP instead of CORS additional parameter “crossDomain: true” should be added to request options:

   1: jQuery.ajax({
   2:     url: url,
   3:     data: { ... },
   4:     dataType: "jsonp",
   5:     crossDomain: true,
   6:     success: function(msg) {
   7:         ...
   8:     }
   9: });

After that jQuery will start to use JSONP. However the question why it implicitly uses CORS on some sites even if we pass dataType: “jsonp” in request options is still open. If you will find the reason please share it in comments.

Wednesday, December 11, 2013

Problem with only first postback working in Chrome from UpdatePanel in Sharepoint site

Some time ago I investigated interesting problem: in existing site running on Sharepoint 2010 on the page with UpdatePanel only first postback worked in Chrome (when possible I try to avoid using of UpdatePanel, preferring jquery.ajax() instead. But in this case site was implemented quite long time ago and we couldn’t change it). In other browsers postbacks worked and UpdatePanel was modified successfully. What was even more unclear is that even in Chrome postbacks started to work after removing of iframes with google maps code (going forward I must say that google maps is not directly related with the problem). So I started analysis with developer tools in Chrome, comparing it with results in firebug in FF.

There were no any errors neither in Chrome console nor in javascript debugger. The first thing I did was setting ScriptMode property of ScriptManager control to Debug. It made all scripts (including those which are added via ScriptResource.axd handler) user friendly (not minified as they were before), which made debugging possible.

Debugging showed that problem comes from Sys$WebForms$PageRequestManager$_onFormSubmit() method from MicrosoftAjaxWebForms.js file:

   1:   function Sys$WebForms$PageRequestManager$_onFormSubmit(evt) {
   2:       var continueSubmit = true;
   3:       var isCrossPost = this._isCrossPost;
   4:       this._isCrossPost = false;
   5:       var i, l;
   6:       if (this._onsubmit) {
   7:           continueSubmit = this._onsubmit();
   8:       }
   9:       if (continueSubmit) {
  10:           for (i = 0, l = this._onSubmitStatements.length; i < l; i++) {
  11:               if (!this._onSubmitStatements[i]()) {
  12:                   continueSubmit = false;
  13:                   break;
  14:               }
  15:           }
  16:       }
  17:       if (!continueSubmit) {
  18:           if (evt) {
  19:               evt.preventDefault();
  20:           }
  21:           return;
  22:       }
  23:       var form = this._form;
  24:       if (isCrossPost) {
  25:           return;
  26:       }
  27:       if (this._activeDefaultButton && !this._activeDefaultButtonClicked) {
  28:           this._onFormElementActive(this._activeDefaultButton, 0, 0);
  29:       }
  30:       if (!this._postBackSettings.async) {
  31:           return;
  32:       }
  33:       var formBody = new Sys.StringBuilder();
  34:       formBody.append(encodeURIComponent(this._scriptManagerID) + '=' +
  35:          encodeURIComponent(this._postBackSettings.panelID) + '&');
  36:       var count = form.elements.length;
  37:       for (i = 0; i < count; i++) {
  38:           var element = form.elements[i];
  39:           var name = element.name;
  40:           if (typeof (name) === "undefined" || (name === null) || (name.length === 0) ||
  41:              (name === this._scriptManagerID)) {
  42:               continue;
  43:           }
  44:           var tagName = element.tagName.toUpperCase();
  45:           if (tagName === 'INPUT') {
  46:               var type = element.type;
  47:               if ((type === 'text') || (type === 'password') || (type === 'hidden') || (((type === 'checkbox') ||
  48:                   (type === 'radio')) && element.checked)) {
  49:                   formBody.append(encodeURIComponent(name));
  50:                   formBody.append('=');
  51:                   formBody.append(encodeURIComponent(element.value));
  52:                   formBody.append('&');
  53:               }
  54:           } else if (tagName === 'SELECT') {
  55:               var optionCount = element.options.length;
  56:               for (var j = 0; j < optionCount; j++) {
  57:                   var option = element.options[j];
  58:                   if (option.selected) {
  59:                       formBody.append(encodeURIComponent(name));
  60:                       formBody.append('=');
  61:                       formBody.append(encodeURIComponent(option.value));
  62:                       formBody.append('&');
  63:                   }
  64:               }
  65:           } else if (tagName === 'TEXTAREA') {
  66:               formBody.append(encodeURIComponent(name));
  67:               formBody.append('=');
  68:               formBody.append(encodeURIComponent(element.value));
  69:               formBody.append('&');
  70:           }
  71:       }
  72:       formBody.append("__ASYNCPOST=true&");
  73:       if (this._additionalInput) {
  74:           formBody.append(this._additionalInput);
  75:           this._additionalInput = null;
  76:       }
  77:       var request = new Sys.Net.WebRequest();
  78:       var action = form.action;
  79:       if (Sys.Browser.agent === Sys.Browser.InternetExplorer) {
  80:           var fragmentIndex = action.indexOf('#');
  81:           if (fragmentIndex !== -1) {
  82:               action = action.substr(0, fragmentIndex);
  83:           }
  84:           var queryIndex = action.indexOf('?');
  85:           if (queryIndex !== -1) {
  86:               var path = action.substr(0, queryIndex);
  87:               if (path.indexOf("%") === -1) {
  88:                   action = encodeURI(path) + action.substr(queryIndex);
  89:               }
  90:           } else if (action.indexOf("%") === -1) {
  91:               action = encodeURI(action);
  92:           }
  93:       }
  94:       request.set_url(action);
  95:       request.get_headers()['X-MicrosoftAjax'] = 'Delta=true';
  96:       request.get_headers()['Cache-Control'] = 'no-cache';
  97:       request.set_timeout(this._asyncPostBackTimeout);
  98:       request.add_completed(Function.createDelegate(this, this._onFormSubmitCompleted));
  99:       request.set_body(formBody.toString());
 100:       var eventArgs, handler = this._get_eventHandlerList().getHandler("initializeRequest");
 101:       if (handler) {
 102:           eventArgs = new Sys.WebForms.InitializeRequestEventArgs(request, this._postBackSettings.sourceElement);
 103:           handler(this, eventArgs);
 104:           continueSubmit = !eventArgs.get_cancel();
 105:       }
 106:       if (!continueSubmit) {
 107:           if (evt) {
 108:               evt.preventDefault();
 109:           }
 110:           return;
 111:       }
 112:       this._scrollPosition = this._getScrollPosition();
 113:       this.abortPostBack();
 114:       handler = this._get_eventHandlerList().getHandler("beginRequest");
 115:       if (handler) {
 116:           eventArgs = new Sys.WebForms.BeginRequestEventArgs(request, this._postBackSettings.sourceElement);
 117:           handler(this, eventArgs);
 118:       }
 119:       if (this._originalDoCallback) {
 120:           this._cancelPendingCallbacks();
 121:       }
 122:       this._request = request;
 123:       this._processingRequest = false;
 124:       request.invoke();
 125:       if (evt) {
 126:           evt.preventDefault();
 127:       }
 128:   }

This method makes actual POST request when some even triggers postback from inside UpdatePanel. 2 lines from this method are most interesting for us:

line 7: continueSubmit = this._onsubmit();
and
line 124: request.invoke();

On line 7 it checks should it make post request or not. If result of this._onsubmit() call will be false, on lines 17-22 method will exit without sending the request. In FF and IE it always returned true, while in Chrome – false. Let’s check first why it returned false in Chrome and then will analyze what happens when request is send on line 124.

Line 7 calls _spFormOnSubmitWrapper() function defined in init.js – standard Sharepoint javascript file located in /layouts/{lcid} folder. This file is one of the basic, without which Sharepoint sites won’t work properly. Here is the code of _spFormOnSubmitWrapper() function:

   1:  function _spFormOnSubmitWrapper() {
   2:      if (_spSuppressFormOnSubmitWrapper) {
   3:          return true;
   4:      }
   5:      if (_spFormOnSubmitCalled) {
   6:          return false;
   7:      }
   8:      if (typeof (_spFormOnSubmit) == "function") {
   9:          var retval = _spFormOnSubmit();
  10:          var testval = false;
  11:          if (typeof (retval) == typeof (testval) && retval == testval) {
  12:              return false;
  13:          }
  14:      }
  15:      _spFormOnSubmitCalled = true;
  16:      return true;
  17:  }

In Chrome global javascript variable _spFormOnSubmitCalled, defined in init.js file, was true, that’s why method _spFormOnSubmitWrapper() returned false (from lines 5-7). The question was why it was true?

In order to answer it we need to return to line 124 from the Sys$WebForms$PageRequestManager$_onFormSubmit() function, showed above. request.invoke() calls Sys$Net$_WebRequestManager$executeRequest() function from MicrosoftAjax.js:

   1:   function Sys$Net$_WebRequestManager$executeRequest(webRequest) {
   2:       var e = Function._validateParams(arguments, [{
   3:           name: "webRequest",
   4:           type: Sys.Net.WebRequest
   5:       }]);
   6:       if (e) throw e;
   7:       var executor = webRequest.get_executor();
   8:       if (!executor) {
   9:           var failed = false;
  10:           try {
  11:               var executorType = eval(this._defaultExecutorType);
  12:               executor = new executorType();
  13:           } catch (e) {
  14:               failed = true;
  15:           }
  16:           if (failed || !Sys.Net.WebRequestExecutor.isInstanceOfType(executor) ||
  17:               !executor) {
  18:               throw Error.argument("defaultExecutorType",
  19:                   String.format(Sys.Res.invalidExecutorType,
  20:                       this._defaultExecutorType));
  21:           }
  22:           webRequest.set_executor(executor);
  23:       }
  24:       if (executor.get_aborted()) {
  25:           return;
  26:       }
  27:       var evArgs = new Sys.Net.NetworkRequestEventArgs(webRequest);
  28:       var handler = this._get_eventHandlerList().getHandler("invokingRequest");
  29:       if (handler) {
  30:           handler(this, evArgs);
  31:       }
  32:       if (!evArgs.get_cancel()) {
  33:           executor.executeRequest();
  34:       }
  35:   }

In this method difference between FF and Chrome was in line 28 (var handler = this._get_eventHandlerList().getHandler("invokingRequest")). In FF it returned not empty function which set _spFormOnSubmitCalled global variable (because of which _spFormOnSubmitWrapper function returned false and because of which postbacks didn’t work in Chrome): _spResetFormOnSubmitCalledFlag() defined in init.js:

   1:  function _spResetFormOnSubmitCalledFlag( sender, e)
   2:  {
   3:  _spFormOnSubmitCalled=false;
   4:  }

So now the question was why _spResetFormOnSubmitCalledFlag() attached as a handler to "invokingRequest" event in FF and IE, but not in Chrome? Analysis of init.js showed that _spResetFormOnSubmitCalledFlag() function is attached to event in _spBodyOnLoadWrapper() function, which is also defined in init.js:

   1:  function _spBodyOnLoadWrapper() {
   2:      _spBodyOnLoadCalled = true;
   3:      if (!_spBodyOnPageShowRegistered && typeof (browseris) != "undefined" &&
   4:          !browseris.ie && typeof (window.addEventListener) == 'function') {
   5:          window.addEventListener('pageshow', _spBodyOnPageShow, false);
   6:          _spBodyOnPageShowRegistered = true;
   7:      }
   8:      if (typeof (Sys) != "undefined" && typeof (Sys.WebForms) != "undefined" &&
   9:          typeof (Sys.WebForms.PageRequestManager) != "undefined") {
  10:          var pageRequestMgr = Sys.WebForms.PageRequestManager.getInstance();
  11:          if (!_spPageLoadedRegistered && pageRequestMgr != null) {
  12:              pageRequestMgr.add_pageLoaded(_spPageLoaded);
  13:              _spPageLoadedRegistered = true;
  14:          }
  15:      }
  16:      if (!_spPageLoadedRegistered) {
  17:          _spPageLoaded();
  18:      }
  19:      _spFormOnSubmitCalled = false;
  20:      if (typeof (Sys) != "undefined" && typeof (Sys.Net) != "undefined" &&
  21:          typeof (Sys.Net.WebRequestManager) != "undefined") {
  22:  Sys.Net.WebRequestManager.add_invokingRequest(_spResetFormOnSubmitCalledFlag);
  23:      }
  24:      if (typeof (NotifyBodyLoadedAndExecuteWaitingJobs) != "undefined") {
  25:          NotifyBodyLoadedAndExecuteWaitingJobs();
  26:      }
  27:      ExecuteOrDelayUntilScriptLoaded(ProcessDefaultOnLoad, "core.js");
  28:      if (typeof (g_prefetch) == "undefined" || g_prefetch == 1) {
  29:          var prefetch = _spGetQueryParam("prefetch");
  30:          if (prefetch != 0) _spPreFetch();
  31:      }
  32:  }

And that’s was it: it turned out that problem with postbacks in Chrome was caused by known compatibility problem with Chrome in Sharepoint. In Chrome function _spBodyOnLoadWrapper() is not always loaded which causes many issues in Sharepoint. You will find a lot of examples of this problem if will search by function name and Sharepoint. This function supposed to be called by javascript embedded to the page. I.e. we have body element with onload handler:

   1:  <body onload="javascript: _spBodyOnLoadWrapper();">
   2:  </body>

If we will check the output html we will find that this handler is overridden by the following way in the end of page:

   1:  <script type="text/javascript" language="JavaScript">
   2:  // append an onload event handler
   3:  var S39A6E3E3__onload = document.body.onload;
   4:  if (typeof document.body.onload == 'function') {
   5:      document.body.onload = function() {
   6:          S39A6E3E3__onload();
   7:          document.getElementById('ctl00_PlaceHolderSearchArea_SmallSearchInputBox_S39A6E3E3_InputKeywords').name =
   8:  'InputKeywords';
   9:      }
  10:  } else {
  11:      document.body.onload = function() {
  12:          eval(S39A6E3E3__onload);
  13:          document.getElementById('ctl00_PlaceHolderSearchArea_SmallSearchInputBox_S39A6E3E3_InputKeywords').name =
  14:  'InputKeywords';
  15:      }
  16:  }
  17:   
  18:  function S39A6E3E3_OSBEK(event1) {
  19:      if ((event1.which == 10) || (event1.which == 13)) {
  20:          S39A6E3E3_Submit();
  21:          return false;
  22:      }
  23:  } {
  24:      var searchTextBox = document.getElementByI
  25:  ('ctl00_PlaceHolderSearchArea_SmallSearchInputBox_S39A6E3E3_InputKeywords');
  26:      if (searchTextBox.className.indexOf('s4-searchbox-QueryPrompt') == -1)
  27:  searchTextBox.className += searchTextBox.className ?
  28:  ' s4-searchbox-QueryPrompt' : 's4-searchbox-QueryPrompt';
  29:  } // -->
  30:  </script>

I.e. it stores original handler in S39A6E3E3__onload variable (line 3, name can be different on your site) and then attaches another handler, calls original handler inside it and makes additional actions (lines 5-8 or 11-14). But under some conditions neither of 2 overridden functions get called in Chrome. In our case it happened when page contained iframe with google maps. In one of the posts about similar problem I read assumption that it happens because of race condition in scripts and page loading in Chrome. Most probably the reason is really realted with scripts loading dependencies: in some cases when Chrome reaches the embedded javascript code shown above, _spBodyOnLoadWrapper() function is not attached to body.onload event, so neither of 2 ways of overriding cause calling of _spBodyOnLoadWrapper() function.

But how to fix this problem? The most frequent solution I saw used javascript timer and assumption that init.js will be loaded after predefined amount of time (e.g. 200 ms). I created more reliable solution using standard ExecuteOrDelayUntilScriptLoaded() function:

   1:  $(function () {
   2:      if (navigator && navigator.userAgent &&
   3:          /chrome/.test(navigator.userAgent.toLowerCase())) {
   4:          if (!_spBodyOnLoadCalled) {
   5:              ExecuteOrDelayUntilScriptLoaded(function () {
   6:                  if (!_spBodyOnLoadCalled) {
   7:                      _spBodyOnLoadWrapper();
   8:                  }
   9:              }, "init.js");
  10:          }
  11:      }
  12:  });

At first it checks that current browser is Chrome and then checks was _spBodyOnLoadWrapper() already called using another global variable _spBodyOnLoadCalled which is set to true in the beginning of _spBodyOnLoadWrapper(). Then it waits until init.js will be loaded using ExecuteOrDelayUntilScriptLoaded() function, checks _spBodyOnLoadCalled again and finally calls _spBodyOnLoadWrapper(). After that problem was fixed, i.e. postbacks from UpdatePanel started to work properly in Chrome.

This was interesting debugging session. Hope that it will be useful in your work.

Saturday, August 3, 2013

Return JSON from asmx when call it using jquery ajax: step by step guide

Some time you still need to configure old asmx web service for working with jquery ajax. Interesting that there is still a lot of questions about different aspects of the subject (even though that people should use WCF with REST endpoints nowadays in .Net stack), so I thought that it will be good idea to summarize all configuration steps in single post.

First of all we need to create asmx service itself which returns some POCO object. Let’s create some abstract QueueService:

   1:  [WebService(Namespace = "http://example.com")]
   2:  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
   3:  [ToolboxItem(false)]
   4:  [ScriptService]
   5:  public class QueueService : WebService
   6:  {
   7:      [WebMethod]
   8:      [ScriptMethod(UseHttpGet = false, ResponseFormat = ResponseFormat.Json)]
   9:      public QueueItem GetQueueItem(string itemId)
  10:      {
  11:          var queueRepository = new QueueRepository();
  12:          var queueItem = queueRepository.GetById(new Guid(itemId));
  13:          return queueItem;
  14:      }
  15:  }

QueueItem class may look like this:

   1:  [Serializable]
   2:  public class QueueItem
   3:  {
   4:      public QueueItemStatus Status { get; set; }
   5:      public int Progress { get; set; }
   6:  }

There are several important notes in the code above. First of all web service class is decorated with ScriptService attribute. Then GetQueueItem() web method is decorated with ScriptMethod attribute with UseHttpGet = false, ResponseFormat = ResponseFormat.Json properties. They say that method will be called via HTTP POST verb (you may make it work GET e.g. with JSONP). And ResponseFormat.Json says that result will be returned in JSON format. Note that in the web method we just return object, we don’t serialize it to JSON by ourselves using e.g. standard JavaScriptSerializer or JSON.Net.

The next step will be changing of the web.config, which should be located in the same folder where asmx file is, or location of asmx file should inherit it from parent folders. Here is the minimal required configuration:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:    <system.web>
   4:      <webServices>
   5:        <protocols>
   6:          <add name="HttpPost"/>
   7:        </protocols>
   8:      </webServices>
   9:      <httpHandlers>
  10:        <remove verb="*" path="*.asmx"/>
  11:        <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  12:      </httpHandlers>
  13:    </system.web>
  14:  </configuration>

Here we allow using of HTTP POST verb for calling of our web service (lines 4-8) and replace default handler of asmx files to ScriptHandlerFactory.

This was the server part. Now we need to consume the web service from jquery ajax. Here is the example:

   1:  $.ajax({
   2:      url: "http://example.com/QueueService.asmx/GetQueueItem",
   3:      type: "POST",
   4:      contentType: "application/json; charset=utf-8",
   5:      data: "{'itemId':'...'}",
   6:      dataType: "json",
   7:      success: function(r) {
   8:          if (!r) {
   9:              // handle error
  10:          }
  11:          var result = r.d;
  12:          if (!result) {
  13:              // handle error
  14:          }
  15:          ...
  16:      },
  17:      error: function(xhr, status, error) {
  18:          // handle error
  19:      }
  20:  });

There are several important notes here as well and without understanding them, you will continuously get “There was an error processing the request” with HTTP 500 Internal server error. First of all we call the web service using POST verb (line 3). Second – we need to specify contentType = "application/json; charset=utf-8" (line 4). Without it ASP.Net won’t treat it as JSON request and call will fail (see this article for details: JSON Hijacking and How ASP.NET AJAX 1.0 Avoids these Attacks). Next important thing is that we need to pass parameters to the web method also in JSON format (line 5). Without it you request won’t even reach the service. End the last thing which you need to note is setting dataType = "json" (line 6).

After this you should be able to consume asmx web service which returns JSON from jquery ajax. If you will check requests in fiddler you should see something like this:

request:

POST /QueueService.asmx/GetQueueItem HTTP/1.1
x-requested-with: XMLHttpRequest
Accept-Language: en-US,fi;q=0.5
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/json; charset=utf-8
Content-Length: 52
Connection: Keep-Alive
Pragma: no-cache

{'itemId':'…'}

response:

HTTP/1.1 200 OK
Content-Length: 152
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET

{"d":{"__type":"QueueItem","Status":0,"Progress":0}}

And there are no any additional efforts needed for serializing/deserializing object to JSON neither on server nor on client. Hope this guide will help in you work.

Sunday, November 25, 2012

Problem with context site in Sharepoint when call web service from javascript

If you need to call web service (here I will talk about old asmx web services) from javascript in ASP.Net or Sharepoint you may use auto-generated js proxy which is added to html output when you add ServiceReference to the ScriptManager instance for the current page:

   1: var scriptManager = ScriptManager.GetCurrent(this.Page);
   2: if (scriptManager == null)
   3: {
   4:     scriptManager = new ScriptManager();
   5:     this.Controls.Add(scriptManager);
   6: }
   7:  
   8: var referenceProxy = new ServiceReference();
   9: referenceProxy.Path = "/_layouts/test/test.asmx";
  10: referenceProxy.InlineScript = true;
  11:  
  12: scriptManager.Services.Add(referenceProxy);

Your web service’s class should be marked with [ScriptService] attribute, and all web methods which you want to use from javascript should be marked with [ScriptMethod] attribute:

   1: namespace MyNamespace
   2: {
   3:     [ScriptService]
   4:     [WebService(Namespace = "http://example.com/", Name = "Test")]
   5:     [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
   6:     [ToolboxItem(false)]
   7:     public class Test : WebService
   8:     {
   9:         [ScriptMethod]
  10:         [WebMethod]
  11:         public string Foo(string str)
  12:         {
  13:             // ...
  14:         }
  15:     }
  16: }

After that you can call web service from javascript like this:

   1: MyNamespace.Test.Foo("Hello, world!");

However in Sharepoint you may encounter with the problem when use this method. Check the line 9 in the code above which adds service reference to script manager.

   1: referenceProxy.Path = "/_layouts/test/test.asmx";

Path contains the url to the service. As you can see it is located in layouts folder, i.e. it can be called in context of any Sharepoint site. Also we specified that generated proxy should be added as inline script to the output html (referenceProxy.InlineScript = true). In this case we need to use relative url in the ServiceReference.Path property as it is said in the documentation:

If the InlineScript property is set to true, then you must use a relative path that points to the same Web application as the page that contains the ServiceReference instance.

In our example path is relative, so it should be Ok. However if this code runs not on the root site, but on some sub site (e.g. http://example.com/subsite) we will have problem: relative path which we specified will be combined with root url, i.e. web service will be executed in context of root site, but not in context of the sub site. I.e. full url will be the following: http://example.com/_layouts/test/test.asmx. It may cause different problems, e.g. if locale of subsite differs from locale of the root site your users may see incorrectly localized content.

In order to fix it first of all we need to comment the line which sets InlineScript to true. It will allow us to use absolute url in the Path property:

   1: string referenceProxyUrl = SPUrlUtility.CombineUrl(SPContext.Current.Web.Url,
   2:     "/_layouts/test/test.asmx");
   3:  
   4: var referenceProxy = new ServiceReference();
   5: referenceProxy.Path = referenceProxyUrl;
   6: //referenceProxy.InlineScript = true;
   7:  
   8: scriptManager.Services.Add(referenceProxy);

These changes will have the following effect. Proxy will be added as external js file via <script> tag. Src attribute will contain absolute url:

   1: <script src="http://example.com/subsite/_layouts/test/test.asmx/jsdebug" type="text/javascript"></script>

(it added “/jsdebug” to the absolute url to the asmx). As you can see js proxy is loaded from context of the correct sub site now. So problem is solved? Unfortunately no. When you will check SPContext.Current.Web.Url property in the debugger of the web method, you will see that code still runs in context of the root site (it will contain http://example.com url, instead of http://example.com/subsite). And this is regardless of the site in which content js proxy was loaded.

In order to fix it we need to perform one extra step. Open the url which is specified in the src script. You will see the code of the proxy. It contain line which we are interesting in:

   1: MyNamespace.Test.set_path("/_layouts/test/test.asmx");

set_path is internal method generated on the client side only, i.e. there is no such method in our web service. As you can see it still contains relative url. This is exactly what we need. Before to use web service proxy in the javascript, we need to override relative url by absolute. We can get absolute url on server side as shown above using standard SPUrlUtility.CombineUrl() method and then pass it to javascript. After that call set_path with passed value:

   1: MyNamespace.Test.set_path(absoluteUrlFromServerSide);
   2: MyNamespace.Test.Foo("Hello, world!");

And it fixes the problem finally. Now the code of asmx web service will be executed in the context of the sub site. Hope that it will help you. E.g. you will encounter with this problem if will use framework for loading web parts asynchronously which I wrote above in the following post: Create asynchronous web parts for Sharepoint.

PS. However the described solution doesn’t fix problem with different locales which I used as example :) Some time ago I wrote how Sharepoint sets locale of the current thread using language of requested SPWeb: see this post. But because of some reason it didn’t happen in this case. So I fixed problem with locale manually:

   1: namespace MyNamespace
   2: {
   3:     [ScriptService]
   4:     [WebService(Namespace = "http://example.com/", Name = "Test")]
   5:     [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
   6:     [ToolboxItem(false)]
   7:     public class Test : WebService
   8:     {
   9:         [ScriptMethod]
  10:         [WebMethod]
  11:         public string Foo(string str)
  12:         {
  13:             this.ensureCorrectLocale();
  14:             // ...
  15:         }
  16:  
  17:         private void ensureCorrectLocale()
  18:         {
  19:             if (Thread.CurrentThread.CurrentUICulture.LCID !=
  20: SPContext.Current.Web.UICulture.LCID)
  21:             {
  22:                 Thread.CurrentThread.CurrentUICulture =
  23: SPContext.Current.Web.UICulture;
  24:             }
  25:             if (Thread.CurrentThread.CurrentCulture.LCID !=
  26: SPContext.Current.Web.Locale.LCID)
  27:             {
  28:                 Thread.CurrentThread.CurrentCulture =
  29: SPContext.Current.Web.Locale;
  30:             }
  31:         }
  32:     }
  33: }

After that current thread’s locale became correct.