How to detect resource request in an iOS webview, same as Android's "shouldInterceptRequest"?
Asked Answered
G

2

6

In the iOS WKWebView, is there a way to detect when the website makes a resource request (e.g. an HttpRequest via Ajax)?

In Android, there is a very convenient method "shouldInterceptRequest", but I cannot find an equivalent in iOS.

Gynophore answered 30/12, 2023 at 18:11 Comment(8)
The tooltip for the downvote button states "This question does not show any research effort; it is unclear or not useful". Your question does not show any research effort. WKWebView has a couple of delegates with lots of methods. Have you tested to see if any of them are called for your use case? Have you done any searching? Updating your question with the results of these efforts would prove beneficial.Cavallaro
I did some research, but I couldn't find a way to detect a resource request. The question is very clear.Gynophore
Did you try all of the possible delegates and their methods? What were the results? Please put those results in your question. Also mention when research you've done. This avoids duplication of effort by prospective answerers.Cavallaro
@Cavallaro whatever I tried didn't work for me. I don't see any point in adding my failed attempts in the question. I also found this question, which doesn't seem very hopeful, even though it's quite old: #30542431Gynophore
@DanieleB There is a way of intercepting WKWebview requests but only with custom scheme e.g. mycustomscheme://test.com using setURLSchemeHandler method on WKWebview and implementing WKURLSchemeHandler. When you try to use http, https as custom scheme app will crash because Apple does not allow doing it.Dunt
unfortunately using a custom scheme doesn't help when trying to monitor HTTP Ajax requestsGynophore
Did you maybe tried inject JS script using addUserScript that will override XMLHTTPRequest (https://mcmap.net/q/261096/-intercept-xmlhttprequest-and-modify-responsetext)? And inside of override send window.webkit.messageHandlers.{your_handler}.postMessage() (you need to register your_handler for webview).Dunt
That looks like a very hacky way to do it.Gynophore
C
4

As we know we can NOT WKWebViewConfiguration.setURLSchemeHandler with http/https schemes because of the error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: ''https' is a URL scheme that WKWebView handles natively'

This happens because WKWebView.handlesURLScheme is called inside and generates this error in case of returned true. Thus we can use an approach by swizzling this method and it allows to handle http/https schemes as well as custom ones.

func swizzleClassMethod(cls: AnyClass, origSelector: Selector, newSelector: Selector) {
  guard let origMethod = class_getClassMethod(cls, origSelector),
        let newMethod = class_getClassMethod(cls, newSelector),
        let cls = object_getClass(cls)
  else {
    return
  }
  
  if class_addMethod(cls, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)) {
    class_replaceMethod(cls, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod))
  }
  else {
    method_exchangeImplementations(origMethod, newMethod)
  }
}

extension WKWebView {
  @objc
  static func _handlesURLScheme(_ urlScheme: String) -> Bool {
    false // Allow all schemes
  }
}

...

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  swizzleClassMethod(cls: WKWebView.self,
                     origSelector: #selector(WKWebView.handlesURLScheme),
                     newSelector: #selector(WKWebView._handlesURLScheme))

  return true
}

Now we can set http/https schemes and intercept all fetch and XMLHttpRequest inside WKWebView, for instance:

override func viewDidLoad() {
  super.viewDidLoad()

  let js = """
  fetch('https://api.github.com/users')
  .then(response => {
    return response.text()
  })
  .then(text => {
    document.getElementById("body").innerHTML += "<br> fetch:" + text;
  })

  var ajax = new XMLHttpRequest();
  ajax.onreadystatechange = function() {
    if (this.readyState == 3) {
      document.getElementById("body").innerHTML += "<br> ajax:" + this.responseText;
    }
  };
  ajax.open("GET", "https://api.github.com/users", true);
  ajax.send();
  """
  let script = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
  let config = WKWebViewConfiguration()
  config.userContentController.addUserScript(script)

  config.setURLSchemeHandler(self, forURLScheme: "https")

  webView = WKWebView(frame: view.bounds, configuration: config)
  view.addSubview(webView)
  webView.loadHTMLString("<html><body id='body'></body></html>", baseURL: nil)
}
extension ViewController: WKURLSchemeHandler {
  func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
    let json = #"[{"login": "mojombo","id": 1}, {"login": "defunkt", "id": 2}]"#
    
    let response = HTTPURLResponse(
      url: urlSchemeTask.request.url!,
      statusCode: 200,
      httpVersion: nil,
      headerFields: [ "access-control-allow-origin": "*"]
    )!
    
    urlSchemeTask.didReceive(response)
    urlSchemeTask.didReceive(json.data(using: .utf8)!)
    urlSchemeTask.didFinish()
  }
  
  func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
  }
}

Outputs:

fetch:[{"login": "mojombo","id": 1}, {"login": "defunkt", "id": 2}]
ajax:[{"login": "mojombo","id": 1}, {"login": "defunkt", "id": 2}]
Choline answered 4/1 at 21:49 Comment(0)
S
0

You can inspect the requests from a WKWebView using the WKNavigationDelegate.
You may override webView:decidePolicyFor:... to inspect the request, check for its type (e.g. link clicked, form submitted, etc.) and inspect the associated URL to take some action:

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()
        webView = WKWebView(frame: view.bounds)
        webView.navigationDelegate = self
        view.addSubview(webView)
        let url = URL(string: "https://www.google.com")!
        let request = URLRequest(url: url)
        webView.load(request)
    }

    // WKNavigationDelegate method to intercept resource requests
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        // `navigationType` indicates how this request got created
        // `.other` may indicate an Ajax request
        // More info: https://developer.apple.com/documentation/webkit/wknavigationtype

        // Update: remove the if clause for now and see if you get any result for your Ajax requests.
        // if navigationAction.navigationType == .other {
            if let request = navigationAction.request.url {
                print("Resource Request: \(request)")
            }
        // }

        // Allow the navigation to continue or not, depending on your business logic
        decisionHandler(.allow)
    }
}
Shame answered 2/1 at 15:40 Comment(3)
unfortunately I can't see any Ajax requests showing up hereGynophore
I have tried with this website: "demo.tutorialzine.com/2009/09/simple-ajax-website-jquery/…" The delegate method fires, although this check navigationAction.navigationType == .other doesn't do it. Can you remove this if statement and try again?Shame
Unfortunately even by removing that check, it doesn't work. It look like that delegate is not able to intercept all resource requests.Gynophore

© 2022 - 2024 — McMap. All rights reserved.