35
func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {
     var request = NSMutableURLRequest(URL: navigationAction.request.URL)
     request.setValue("value", forHTTPHeaderField: "key")
     decisionHandler(.Allow)
}

In the above code I want to add a header to the request. I have tried to do navigationAction.request.setValue("IOS", forKey: "DEVICE_APP") but it doesn't work.

please help me in any way.

Forge
  • 6,538
  • 6
  • 44
  • 64
sandip
  • 419
  • 1
  • 6
  • 13
  • Want to connect soap header ? – Gökhan Çokkeçeci Mar 11 '15 at 10:44
  • thank for reply ... but actually i created one wkwebview.where i have to add header for request .it has been done only first time .after that it will not added .as per the apple doc there is one api decidePolicyForNavigationAction where each time request is loaded .i want to add header for that request – sandip Mar 11 '15 at 12:41

9 Answers9

35

AFAIK sadly you cannot do this with WKWebView.

It most certainly does not work in webView:decidePolicyForNavigationAction:decisionHandler: because the navigationAction.request is read-only and a non-mutable NSURLRequest instance that you cannot change.

If I understand correctly, WKWebView runs sandboxed in a separate content and network process and, at least on iOS, there is no way to intercept or change it's network requests.

You can do this if you step back to UIWebView.

Stefan Arentz
  • 34,311
  • 8
  • 67
  • 88
20

There are many different ways to do that, I found that the easiest solution was to subclass WKWebView and override the loadRequest method. Something like this:

class CustomWebView: WKWebView {
    override func load(_ request: URLRequest) -> WKNavigation? {
        guard let mutableRequest: NSMutableURLRequest = request as? NSMutableURLRequest else {
            return super.load(request)
        }
        mutableRequest.setValue("custom value", forHTTPHeaderField: "custom field")
        return super.load(mutableRequest as URLRequest)
    }
}

Then simply use the CustomWebView class as if it was a WKWebView.

EDIT NOTE: This will only work on the first request as pointed out by @Stefan Arentz.

NOTE: Some fields cannot be overridden and will not be changed. I haven't done a thorough testing but I know that the User-Agent field cannot be overridden unless you do a specific hack (check here for an answer to that)

Piotr Z
  • 864
  • 11
  • 11
Gabriel Cartier
  • 1,734
  • 20
  • 22
  • 13
    This does not actually solve the problem that was asked here. Because this only works for the initial "top level" request. The custom header is not sticky and won't be used for sub resource loads or for example XHR. – Stefan Arentz Jun 06 '16 at 02:14
  • 1
    That's true, I'll add a note on my post. I haven't dug deep in the webview because this suited my needs, but I feel like it is possible to do it through the delegate. Have you actually tested it through the `webView:decidePolicyForNavigationAction:decisionHandler` method? – Gabriel Cartier Jun 08 '16 at 21:10
  • Hey Gabriel, "There are many different ways to do that" - Like what? Any concrete examples? You may want to check your suggestions. – Stefan Arentz Jun 09 '16 at 01:55
  • I'll do a bit more testing on the decide policy method and update the answer – Gabriel Cartier Jun 09 '16 at 17:07
  • 1
    Nowadays it would be `override func load(_ request: URLRequest) -> WKNavigation?` (Swift 4.2) – Daniel Nov 22 '18 at 15:43
11

I have modified Au Ris answer to use NavigationAction instead of NavigationResponse, as jonny suggested. Also, this fixes situations where the same url is called subsequently and you don't have to keep track of the current url anymore. This only works for GET requests but can surely be adapted for other request types if neccessary.

import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate  {
    var webView: WKWebView?

    override func viewDidLoad() {
        super.viewDidLoad()
        webView = WKWebView(frame: CGRect.zero)
        webView!.navigationDelegate = self
        view.addSubview(webView!)
        // [...] set constraints and stuff

        // Load first request with initial url
        loadWebPage(url: "https://my.url")
    }

    func loadWebPage(url: URL)  {
        var customRequest = URLRequest(url: url)
        customRequest.setValue("true", forHTTPHeaderField: "x-custom-header")
        webView!.load(customRequest)
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping
    (WKNavigationActionPolicy) -> Void) {
        if navigationAction.request.httpMethod != "GET" || navigationAction.request.value(forHTTPHeaderField: "x-custom-header") != nil {
            // not a GET or already a custom request - continue
            decisionHandler(.allow)
            return
        }
        decisionHandler(.cancel)
        loadWebPage(url: navigationAction.request.url!)
    }

}

Roben
  • 840
  • 9
  • 19
7

With some limitations, but you can do it. Intercept the response in the delegate function webView:decidePolicyFornavigationResponse:decisionHandler:, if the url changes cancel it by passing decisionHandler(.cancel) and reload the webview with newURLRequest which sets the custom headers and the intercepted url. In this way each time a url changes (e.g. users tap on links) you cancel that request and create a new one with custom headers.

import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate  {
    var webView: WKWebView?
    var loadUrl = URL(string: "https://www.google.com/")!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView = WKWebView(frame: CGRect.zero)
        webView!.navigationDelegate = self
        view.addSubview(webView!)
        webView!.translatesAutoresizingMaskIntoConstraints = false
        webView!.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
        webView!.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
        webView!.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        webView!.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true

        // Load first request with initial url
        loadWebPage(url: loadUrl)
    }

    func loadWebPage(url: URL)  {
        var customRequest = URLRequest(url: url)
        customRequest.setValue("some value", forHTTPHeaderField: "custom header key")
        webView!.load(customRequest)
    }

    // MARK: - WKNavigationDelegate

    func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        guard let url = (navigationResponse.response as! HTTPURLResponse).url else {
            decisionHandler(.cancel)
            return
        }

        // If url changes, cancel current request which has no custom headers appended and load a new request with that url with custom headers
        if url != loadUrl {
            loadUrl = url
            decisionHandler(.cancel)
            loadWebPage(url: url)
        } else {
            decisionHandler(.allow)
        }
    }
}
Au Ris
  • 4,541
  • 2
  • 26
  • 53
  • 1
    Just about to try this out. Just a question: why hook into navigationResponse? navigationAction just sounds like the correct timing to do it. – Jonny May 28 '18 at 05:29
  • @Jonny you are probably correct `navigationAction` may be a better place to do it. As long as you can extract the url and detect the change. I guess you can do `let url = navigationAction.request?.url ...` If it works for you I will correct my answer accordingly. – Au Ris May 28 '18 at 08:33
  • That's ok I posted what I ended up using as another answer. It's basically the same, just copying the existing request and setting the parameter. Turned out that urlrequest is a struct. – Jonny May 28 '18 at 08:39
2

To add custom headers to AJAX requests, I use a combination of two three hacks. The first provides a synchronous communication channel between my native Swift code and javascript. The second overrides the XMLHttpRequest send() method. The third injects the override into the web page that is loaded into my WKWebView.

So, the combination works like this:

instead of request.setValue("value", forHTTPHeaderField: "key"):

in the ViewController:

func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt headerName: String, defaultText _: String?, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
  if headerName == "key" {
    completionHandler("value")
  } else {
    completionHandler(nil)
  }
}}

in viewDidLoad:

let script = 
  "XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;"
  "XMLHttpRequest.prototype.send = function (body) {"
    "let value = window.prompt('key');"
    "this.setRequestHeader('key', value);"
    "this.realSend(body)"
  "};"
webView.configuration.userContentController.addUserScript(WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))

and this is the test HTML file:

<html>
<head>
  <script>
    function loadAjax() {
      const xmlhttp = new XMLHttpRequest()
      xmlhttp.onload = function() {
         document.getElementById("load").innerHTML = this.responseText
      }
      xmlhttp.open("GET", "/ajax")
      xmlhttp.send()
    }
  </script>
</head>
<body>
  <button onClick="loadAjax()">Change Content</button> <br />
  <pre id="load">load…</pre>
</body>
</html>

Call to /ajax brings a generic echo, including all request headers. This way I know that the task is fullfilled.

Alex Cohn
  • 56,089
  • 9
  • 113
  • 307
1

Here's how you do it: The strategy is to have your WKNavigationDelegate cancel the request, modify a mutable copy of it and re-initiate it. An if-else is used to allow the request to proceed if it already has the desired header; otherwise you will end up in an endless load / decidePolicy loop.

Not sure what's up, but weird things happen if you set the header on every request, so for best results only set the header on requests to the domain(s) you care about.

The example here sets a header field for requests to header.domain.com, and allows all other requests without the header:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL * actionURL = navigationAction.request.URL;
    if ([actionURL.host isEqualToString:@"header.domain.com"]) {
        NSString * headerField = @"x-header-field";
        NSString * headerValue = @"value";
        if ([[navigationAction.request valueForHTTPHeaderField:headerField] isEqualToString:headerValue]) {
            decisionHandler(WKNavigationActionPolicyAllow);
        } else {
            NSMutableURLRequest * newRequest = [navigationAction.request mutableCopy];
            [newRequest setValue:headerValue forHTTPHeaderField:headerField];
            decisionHandler(WKNavigationActionPolicyCancel);
            [webView loadRequest:newRequest];
        }
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}
jbelkins
  • 470
  • 3
  • 15
  • P.S. Excuse my ObjC. Should be easy enough to do the same in Swift. ;) – jbelkins Jun 09 '16 at 22:03
  • 4
    This will just load the top level page. It will not add headers to any resources on the page. Or to XHR requests the page will make. This is not any different than your previous answer. – Stefan Arentz Jun 10 '16 at 12:30
  • it is correct that this method only changes the headers of the html page request. however subsequent html page request will also have its headers changed. this is not the case with @gabriel-cartier's method. `loadRequest` is not called when the user taps on links. – dreamlab Sep 24 '17 at 14:42
  • you should also check if the request is on the main frame `navigationAction.targetFrame?.isMainFrame`. otherwise you will load a new page for iframe requests. – dreamlab Sep 24 '17 at 14:44
  • Although this worked like a charm on iOS 13 and iOS 14. It causes bad behaviour on iOS < 13 where any CSRF header (like ASP.NET AntiForgery Token) not to be sent in the request headers thus failing server-side checks. I'm using Xamarin.iOS so not sure if it's a bug in your code or Xamarin's WKWebView binding or a bug from Apple. I'm struggling to solve it with no luck yet @jbelkins – Mohammad Zekrallah Oct 22 '20 at 05:39
  • even only canceling the reuest and sending it again (without manipulating any headers) causes the problem .. so it's either Xamarin's or Apple issues I think. Any one know of another solution ? – Mohammad Zekrallah Oct 22 '20 at 05:40
  • it turned out that this is a known bug in WKWebView and in WebKit causing navigationLink.Request.Body to be always nil !! very frustrating ! – Mohammad Zekrallah Oct 22 '20 at 09:19
0
private var urlrequestCurrent: URLRequest?

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    //print("WEB decidePolicyFor navigationAction: \(navigationAction)")
    if let currentrequest = self.urlrequestCurrent {
        //print("currentrequest: \(currentrequest), navigationAction.request: \(navigationAction.request)")
        if currentrequest == navigationAction.request {
            self.urlrequestCurrent = nil
            decisionHandler(.allow)
            return
        }
    }

    decisionHandler(.cancel)

    var customRequest = navigationAction.request
    customRequest.setValue("myvaluefffs", forHTTPHeaderField: "mykey")
    self.urlrequestCurrent = customRequest
    webView.load(customRequest)
}
Jonny
  • 15,955
  • 18
  • 111
  • 232
0

my solution is copy request and add headers then load it again

    if navigationAction.request.value(forHTTPHeaderField: "key") == nil {
        decisionHandler(.cancel)
        
        var req:URLRequest = navigationAction.request;
        req.addValue("value", forHTTPHeaderField: "key");
        webView.load(req);
    } else {
        decisionHandler(.allow)
    }
  • Will it affects anything like the performance? Since you always cancel the current request and reload the current URL again. – Drew Jan 22 '21 at 13:11
0

The above mentioned solutions seems to work on iOS 14 but on iOS < 14, the POST request Body is always null causing server-side rejects of the request. It turned out that this is a known bug in WKWebView and in WebKit causing navigationLink.Request.Body to be always nil !! very frustrating and stupid bug from Apple forcing UIWebView migration to non-stable WKWebView !

Anyway, the solution is that you should (before canceling the request), grab POST body by running a javascript function and then assign the result back to navigationAction.Request (if navigationAction.Request.Body is null) and then cancel the action and request it again with the updated navigationAction.Request :

Solution is in Xamarin but native iOS is very close.

[Foundation.Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
    public async void DecidePolicy(WebKit.WKWebView webView, WebKit.WKNavigationAction navigationAction, Action<WebKit.WKNavigationActionPolicy> decisionHandler)
    {
        try
        {
            var url = navigationAction.Request.Url;

            // only apply to requests being made to your domain
            if (url.Host.ToLower().Contains("XXXXX"))
            {
                if (navigationAction.Request.Headers.ContainsKey((NSString)"Accept-Language"))
                {
                    var languageHeaderValue = (NSString)navigationAction.Request.Headers[(NSString)"Accept-Language"];

                    if (languageHeaderValue == Globalization.ActiveLocaleId)
                    {
                       decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
                        return;
                    }
                    else
                    {
                        decisionHandler(WKNavigationActionPolicy.Cancel);
                        var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);

                        // Temp fix for navigationAction.Request.Body always null on iOS < 14
                        // causing form not to submit correctly
                        updatedRequest = await FixNullPostBody(updatedRequest);

                        WebView.LoadRequest(updatedRequest);
                    }
                }
                else
                {
                    decisionHandler(WKNavigationActionPolicy.Cancel);

                    var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);

                    // Temp fix for navigationAction.Request.Body always null on iOS < 14
                    // causing form not to submit correctly
                    updatedRequest = await FixNullPostBody(updatedRequest);

                    WebView.LoadRequest(updatedRequest);
                }
            }
            else
            {
                decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
            }
        }
        catch (Exception ex)
        {
            Logger.LogException(ex);
            decisionHandler?.Invoke(WKNavigationActionPolicy.Allow);
        }
    }
}


    private async Task<NSMutableUrlRequest> FixNullPostBody(NSMutableUrlRequest urlRequest)
    {
        try
        {
            // if on iOS 14 and higher, don't do this
            //if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
                //return urlRequest;

            // only resume on POST http methods
            if (urlRequest.HttpMethod.ToLowerSafe() != "post")
                return urlRequest;

            // if post body is already there, exit
            if(urlRequest.Body != null)
                return urlRequest;

            if (WebView == null)
                return urlRequest;

            // get body post by running javascript
            var body = await WebView.EvaluateJavaScriptAsync("$('form').serialize()");//.ConfigureAwait(true);

            if (body != null)
            {
                //urlRequest.Body = urlRequest.Body; // always null on iOS < 14
                var bodyString = body.ToString();

                if (!bodyString.IsNullOrEmpty())
                    urlRequest.Body = NSData.FromString(bodyString);
            }

        }
        //This method will throw a NSErrorException if the JavaScript is not evaluated successfully.
        catch (NSErrorException ex)
        {
            DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
        }
        catch (Exception ex)
        {
            DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
        }

        return urlRequest;
    }


private NSMutableUrlRequest SetHeaders(NSMutableUrlRequest urlRequest)
    {
        try
        {
            if (this.UsePOST)
            {
                urlRequest.HttpMethod = "POST";
                urlRequest.Body = postParameters.Encode(NSStringEncoding.UTF8, false);
            }

            var keys = new object[] { "Accept-Language" };
            var objects = new object[] { Globalization.ActiveLocaleId };

            var dictionnary = NSDictionary.FromObjectsAndKeys(objects, keys);

            if (urlRequest.Headers == null)
            {
                urlRequest.Headers = dictionnary;
            }
            else
            {
                NSMutableDictionary httpHeadersCopy = new NSMutableDictionary(urlRequest.Headers);

                httpHeadersCopy.Remove((NSString)"Accept-Language");
                httpHeadersCopy.Add((NSString)"Accept-Language", (NSString)Globalization.ActiveLocaleId);

                urlRequest.Headers = null;
                urlRequest.Headers = (NSDictionary)httpHeadersCopy;
            }
        }
        catch (Exception ex)
        {
            Logger.LogException(ex);
        }
        return urlRequest;
    }