Mobile Development 18 min read

iOS WKWebView Request Interception via WKURLSchemeHandler and JavaScript Injection

By combining WKURLSchemeHandler with injected JavaScript that captures request bodies, forwards them via WKScriptMessageHandler, and synchronizes cookies and redirects, this approach overcomes iOS WKWebView’s native limitations, offering a stable, per‑webview interception method superior to NSURLProtocol for offline‑package loading.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
iOS WKWebView Request Interception via WKURLSchemeHandler and JavaScript Injection

WebView is widely used on mobile; to improve loading speed, offline package technology is used. Intercepting WKWebView requests is needed but iOS does not provide direct capability.

Research

Two main interception approaches were studied: NSURLProtocol and WKURLSchemeHandler.

NSURLProtocol can intercept all URL Loading System requests, but WKWebView runs in a separate process and its requests do not pass through the system, requiring additional hooking.

WKURLSchemeHandler introduced in iOS 11 allows custom handling of http/https schemes but also needs hooking to intercept.

Comparison:

Isolation: NSURLProtocol is global; WKURLSchemeHandler can be configured per WKWebViewConfiguration.

Stability: NSURLProtocol loses request body; WKURLSchemeHandler loses body only before iOS 11.3, later only Blob data.

Consistency: NSURLProtocol may change behavior (e.g., stopLoading not called); WKURLSchemeHandler behaves normally.

Conclusion: WKURLSchemeHandler performs better in isolation, stability and consistency, but body‑loss must be solved.

Our Solution

We combine WKURLSchemeHandler with JavaScript injection to capture request bodies before they are lost. The flow:

Inject custom Fetch/XMLHttpRequest scripts when loading HTML.

Before sending a request, collect body parameters and pass them to the native app via WKScriptMessageHandler.

Native app stores the body and returns a unique identifier.

Native app notifies the web page that storage is complete.

Native app uses WKURLSchemeHandler to retrieve the stored body and re‑assemble the request.

Script Injection – Replace Fetch

The injected script intercepts fetch calls, checks if the method has a body, determines whether the device is iOS < 11.3 or the body contains a Blob, and if needed saves the parameters to native storage before invoking the original fetch.

var nativeFetch = window.fetch
var interceptMethodList = ['POST','PUT','PATCH','DELETE'];
window.fetch = function(url, opts) {
  // Determine if request has body
  var hasBodyMethod = opts != null && opts.method != null && (interceptMethodList.indexOf(opts.method.toUpperCase()) !== -1);
  if (hasBodyMethod) {
    var shouldSaveParamsToNative = isLessThan11_3;
    if (!shouldSaveParamsToNative) {
      // If iOS >=11.3, check if body contains Blob
      shouldSaveParamsToNative = opts != null ? isBlobBody(opts) : false;
    }
    if (shouldSaveParamsToNative) {
      return saveParamsToNative(url, opts).then(function (newUrl) {
        return nativeFetch(newUrl, opts);
      });
    }
  }
  return nativeFetch(url, opts);
};

Saving parameters to native uses WKScriptMessageHandler and a UUID identifier:

function saveParamsToNative(url, opts) {
  return new Promise(function (resolve, reject) {
    var identifier = generateUUID();
    var appendIdentifyUrl = urlByAppendIdentifier(url, "identifier", identifier);
    if (opts && opts.body) {
      getBodyString(opts.body, function (body) {
        finishSaveCallbacks[identifier] = function () {
          resolve(appendIdentifyUrl);
        };
        window.webkit.messageHandlers.saveBodyMessageHandler.postMessage({'body': body, 'identifier': identifier});
      });
    } else {
      resolve(url);
    }
  });
}

Body parsing handles binary, string, and multipart types, converting binary data to Base64 and assembling multipart/form‑data when necessary.

function getBodyString(body, callback) {
  if (typeof body == 'string') {
    callback(body)
  } else if (typeof body == 'object') {
    if (body instanceof ArrayBuffer) body = new Blob([body])
    if (body instanceof Blob) {
      var reader = new FileReader()
      reader.addEventListener("loadend", function () {
        callback(reader.result.split(",")[1])
      })
      reader.readAsDataURL(body)
    } else if (body instanceof FormData) {
      generateMultipartFormData(body).then(function (result) {
        callback(result)
      });
    } else if (body instanceof URLSearchParams) {
      var resultArr = []
      for (pair of body.entries()) {
        resultArr.push(pair[0] + '=' + pair[1])
      }
      callback(resultArr.join('&'))
    } else {
      callback(body);
    }
  } else {
    callback(body);
  }
}

Redirect Handling

WKURLSchemeHandler does not automatically propagate redirects. The implementation intercepts HTTP redirection callbacks, cancels the original load, and manually loads the new URL.

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
  NSString *originUrl = task.originalRequest.URL.absoluteString;
  if ([originUrl isEqualToString:currentWebViewUrl]) {
    [urlSchemeTask didReceiveResponse:response];
    [urlSchemeTask didFinish];
    completionHandler(nil);
  } else {
    completionHandler(request);
  }
}

Cookie Synchronization

Because WKWebView runs in a separate process, cookies must be synchronized between NSHTTPCookieStorage and WKHTTPCookieStore. Responses containing Set‑Cookie are parsed and stored in the web view’s cookie store, while modifications to document.cookie are forwarded to the native side via a custom setter.

(function() {
  var cookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') || Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
  if (cookieDescriptor && cookieDescriptor.configurable) {
    Object.defineProperty(document, 'cookie', {
      configurable: true,
      enumerable: true,
      set: function (val) {
        window.webkit.messageHandlers.save.postMessage(val);
        cookieDescriptor.set.call(document, val);
      },
      get: function () {
        return cookieDescriptor.get.call(document);
      }
    });
  }
})();

Stability Improvements

Dynamic black‑list, logging, and full request proxying are added to reduce edge‑case failures and to leverage native networking features such as HTTP DNS and anti‑fraud.

Conclusion

The combination of WKURLSchemeHandler and JavaScript injection enables reliable request interception for offline‑package loading and zero‑traffic scenarios on iOS, while addressing body loss, redirects, and cookie synchronization.

Mobile DevelopmentiOSWKWebViewJavaScript Injectionoffline packagerequest interceptionWKURLSchemeHandler
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.