locked
Using NSUrlProtocol to add a custom header RRS feed

  • Question

  • User9505 posted

    I have an app where I need to add a custom request header for all outgoing requests from UIWebView. I believe the way to accomplish this is to use a custom NSUrlProtocol.

    My example below is "working" but has issues, and I'm not quite sure what they are to be honest. I'm getting some intermittent loading issues where sometimes a WebView will load but other times it will not. I'm not sure if this is a garbage collection issue or not, but I can't seem to pin anything down. It seems to happen on devices and not the debugger, so I can't really track down what is happening as well.

    I'm not sure if all of what I'm doing in my example is necessary. I only need to add the header...if there is a more simple way to accomplish this, please let me know.

    Thanks!

    public class MyCustomUrlProtocol : NSUrlProtocol
    {
        private NSUrlConnection connection;
        private MyCustomConnectionDelegate connDelegate;
    
        [Export ("canInitWithRequest:")]
        public static bool canInitWithRequest (NSUrlRequest request)
        {
            if (null != request.Headers && request.Headers.ContainsKey (NSObject.FromObject ("MyCustomHeader"))) {
                return false; // request has already been handled
            }
            return true;
        }
    
        [Export ("canonicalRequestForRequest:")]
        public static new NSUrlRequest GetCanonicalRequest (NSUrlRequest request)
        {
            return request;
        }
    
        [Export ("initWithRequest:cachedResponse:client:")]
        public MyCustomUrlProtocol (NSUrlRequest request, NSCachedUrlResponse cachedResponse, NSUrlProtocolClient client) 
            : base (request, cachedResponse, client)
        {
        }
    
        public override void StartLoading ()
        {
            if (null == connDelegate) {
                connDelegate = new MyCustomConnectionDelegate (this);
            }
            // inject the ]HTTP header
            NSMutableDictionary headers = new NSMutableDictionary (Request.Headers);
            headers.Add(NSObject.FromObject("MyCustomHeader"), NSObject.FromObject ("MyCustomValue"));
            NSMutableUrlRequest newRequest = (NSMutableUrlRequest)Request.MutableCopy ();
            newRequest.Headers = headers;
    
            this.connection = new NSUrlConnection (newRequest, connDelegate, true);
        }
    
        public override void StopLoading ()
        {
            if (this.connection != null) {
                this.connection.Cancel ();
                this.connection = null;
            }
        }
    
        protected override void Dispose (bool disposing)
        {
            StopLoading ();
        }
    
        private class MyCustomConnectionDelegate : NSUrlConnectionDelegate
        {
            private MyCustomUrlProtocol handler;
    
            public MyCustomConnectionDelegate (MyCustomUrlProtocol handler) {
                this.handler = handler;
            }
            public override void ReceivedData (NSUrlConnection connection, NSData data)
            {
                handler.Client.DataLoaded (handler, data);
            }
            public override void FailedWithError (NSUrlConnection connection, NSError error)
            {
                handler.Client.FailedWithError (handler, error);
                handler.StopLoading ();
            }
            public override void ReceivedResponse (NSUrlConnection connection, NSUrlResponse response)
            {
                handler.Client.ReceivedResponse (handler, response, NSUrlCacheStoragePolicy.NotAllowed);
            }
            public override void FinishedLoading (NSUrlConnection connection)
            {
                handler.Client.FinishedLoading (handler);
                handler.StopLoading ();
            }
            protected override void Dispose (bool disposing)
            {
                handler.StopLoading ();
            }
        }
    }
    
    Saturday, December 7, 2013 2:25 PM

All replies

  • User39 posted

    There are a few tricks to determine if the GC is tripping you up:

    • Store objects in a static list to prevent the GC from freeing them:

      static List<object> list = new List<object> ();
      
      // in MyCustomUrlProtocol's ctor:
      list.Add (this);
      
    • Put debugging statements in finalizers to see if they really are freed:

      ~MyCustomUrlProtocol ()
      {
          Console.WriteLine ("~MyCustomUrlProtocol");
      }
      
    • Set GCDONTGC=1 (in the project's Run/General options create an environment variable GCDONTGC=1 - note that the value doesn't always stick, so reopen the dialog after hitting OK to ensure that it's stored properly). This will disable the GC completely.
      This is a big hammer and if your app uses a lot of memory it may very well cause your app to run out of memory before you're able to reach the point where the problem you want to diagnose occurs.

    Monday, December 9, 2013 10:08 AM
  • User9505 posted

    I guess I'm less sure that it's a garbage collection thing than I am unsure if I'm even implementing the NSUrlProtocol class correctly. I've updated my code to the following and it seems to better, but I'm not 100% certain it is correct. It seems like a loading issue still springs up every now and then.

    Specifically: is there an issue with connections not being closed? Or with too many connections being spawned? I'm not sure if I need to do more work to handle/pool connections. Or if all of this is overkill for my problem (simply attaching a request header). This is where I need the most help.

    public class MyCustomUrlProtocol : NSUrlProtocol
    {
        [Export ("canInitWithRequest:")]
        public static bool canInitWithRequest (NSUrlRequest request)
        {
            if (null != request.Headers && request.Headers.ContainsKey (NSObject.FromObject ("MyCustomHeader"))) {
                return false; // request has already been handled
            }
            return true;
        }
    
        [Export ("canonicalRequestForRequest:")]
        public static new NSUrlRequest GetCanonicalRequest (NSUrlRequest request)
        {
            return request;
        }
    
        [Export ("initWithRequest:cachedResponse:client:")]
        public MyCustomUrlProtocol (NSUrlRequest request, NSCachedUrlResponse cachedResponse, NSUrlProtocolClient client) 
            : base (request, cachedResponse, client)
        {
        }
    
        public override NSUrlRequest Request {
            get {
                // inject the HTTP header
                NSMutableDictionary headers = null;
                if (null == base.Request.Headers) {
                    headers = new NSMutableDictionary ();
                } else {
                    headers = new NSMutableDictionary (base.Request.Headers);
                }
                headers.Add(NSObject.FromObject("MyCustomHeader"), NSObject.FromObject ("MyCustomValue"));
                NSMutableUrlRequest newRequest = (NSMutableUrlRequest)base.Request.MutableCopy ();
                newRequest.Headers = headers;
                return newRequest;
            }
        }
    
        public override void StartLoading ()
        {
            new NSUrlConnection (Request, new MyCustomConnectionDelegate (this), true);
        }
    
        public override void StopLoading ()
        {
        }
    
        private class MyCustomConnectionDelegate : NSUrlConnectionDelegate
        {
            private MyCustomUrlProtocol handler;
    
            public MyCustomConnectionDelegate (MyCustomUrlProtocol handler) {
                this.handler = handler;
            }
            public override void ReceivedData (NSUrlConnection connection, NSData data)
            {
                handler.Client.DataLoaded (handler, data);
            }
            public override void FailedWithError (NSUrlConnection connection, NSError error)
            {
                handler.Client.FailedWithError (handler, error);
                connection.Cancel ();
            }
            public override void ReceivedResponse (NSUrlConnection connection, NSUrlResponse response)
            {
                handler.Client.ReceivedResponse (handler, response, NSUrlCacheStoragePolicy.NotAllowed);
            }
            public override void FinishedLoading (NSUrlConnection connection)
            {
                handler.Client.FinishedLoading (handler);
                connection.Cancel ();
            }
        }
    }
    

    Thank you!

    Friday, December 13, 2013 8:01 PM
  • User9505 posted

    I'm not sure this is completely resolved (I am still experiencing very random issues that I can't really track down), but for others experiencing similar issues, here is some more information:

    1. I believe (and was also told) that the message "failed to suspend thread 0xb041d000, hopefully it is dead" is harmless
    2. The call to not call connection.Cancel may not be needed. This could be handled by the Clien but it is unverified.
    Thursday, January 9, 2014 1:17 PM
  • User157108 posted

    Hi @RossBender,

    Sorry to bump a nearly one year old post but this is the only post that I find useful for my current situation. I need to send some auth header with WebView. Your code looks brilliant and is it working well now?

    Many thanks, H

    Sunday, October 18, 2015 8:04 PM
  • User9505 posted

    @hxhieu

    Unfortunately I was never able to get consistent results with the custom header. I saw intermittent loading issues that caused too many issues in my webviews. It seemed as if a request every now and then would not have the header attached.

    What I ended up doing instead of using a custom header was using a custom cookie, setting it like this:

    NSHttpCookieStorage.SharedStorage.SetCookie (
        new NSHttpCookie ("HeaderName", "HeaderValue", "/", "hostname.example.com")
    );
    

    This does seem to be used by all webview requests, including those to embedded resources (javascript files, etc).

    Hope that helps

    Monday, October 19, 2015 2:20 PM
  • User213860 posted

    Hello, I just wanted to update this post as someone might be confused as I was when looking at it. Seems like MyCustomConnectionDelegate should be deriving from NSUrlConnectionDataDelegate not NSUrlConnectionDelegate. Guess there was an api change to NSUrlConnectionDelegate.

    Thursday, August 16, 2018 4:32 PM