Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,12 @@ open class AccompanistWebViewClient : WebViewClient() {
}
true
}

is WebRequestInterceptResult.Respond -> {
// Respond is handled in shouldInterceptRequest, not here
KLogger.w { "Respond interceptResult not supported in shouldOverrideUrlLoading" }
true
Comment on lines +434 to +437
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Respond is handled in shouldInterceptRequest, not here", but the shouldInterceptRequest method (lines 323-332) doesn't actually implement handling for WebRequestInterceptResult.Respond. It only delegates to assetLoader and doesn't check the request interceptor or handle custom responses.

The shouldInterceptRequest method needs to be updated to:

  1. Call navigator.requestInterceptor?.onInterceptUrlRequest()
  2. When the result is WebRequestInterceptResult.Respond, create and return a WebResourceResponse with the response data, mimeType, statusCode, and headers
  3. This is critical for Android support of custom URL schemes as mentioned in issue Add shouldInterceptRequest method #180

Without this implementation, the Respond functionality will not work on Android.

Copilot uses AI. Check for mistakes.
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,23 @@ sealed interface WebRequestInterceptResult {
class Modify(
val request: WebRequest,
) : WebRequestInterceptResult

/**
* Respond with custom data instead of making a network request.
* This allows implementing custom URL schemes or serving local content.
*
* Note: This result type requires a custom URL scheme handler to be registered
* on iOS (via WKURLSchemeHandler) or Android (via shouldInterceptRequest).
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
*
* @param data The response body as a byte array
* @param mimeType The MIME type of the response (e.g., "text/html", "application/json")
* @param statusCode The HTTP status code (default: 200)
* @param headers Optional response headers
*/
class Respond(
val data: ByteArray,
val mimeType: String,
val statusCode: Int = 200,
val headers: Map<String, String> = emptyMap(),
) : WebRequestInterceptResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ internal fun KCEFBrowser.addRequestHandler(
}
true
}

is WebRequestInterceptResult.Respond -> {
// Respond is not supported on Desktop
KLogger.w { "Respond interceptResult not supported on Desktop" }
true
}
}
}
return super.onBeforeBrowse(browser, frame, request, userGesture, isRedirect)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.multiplatform.webview.request

import com.multiplatform.webview.util.KLogger
import com.multiplatform.webview.web.WebViewNavigator
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCSignatureOverride
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Foundation.HTTPMethod
Comment thread
sebastiangl marked this conversation as resolved.
import platform.Foundation.NSData
import platform.Foundation.NSHTTPURLResponse
import platform.Foundation.NSURL
import platform.Foundation.allHTTPHeaderFields
import platform.Foundation.create
import platform.WebKit.WKURLSchemeHandlerProtocol
import platform.WebKit.WKURLSchemeTaskProtocol
import platform.WebKit.WKWebView
import platform.darwin.NSObject

/**
* WKURLSchemeHandler implementation for custom URL schemes.
* This allows intercepting requests with custom schemes (e.g., "app://", "local://")
* and providing custom responses.
*/
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
class WKSchemeHandler(
private val navigator: WebViewNavigator,
) : NSObject(), WKURLSchemeHandlerProtocol {

private val activeTasks = mutableMapOf<Int, Boolean>()
Comment thread
sebastiangl marked this conversation as resolved.

@ObjCSignatureOverride
override fun webView(webView: WKWebView, startURLSchemeTask: WKURLSchemeTaskProtocol) {
val taskId = startURLSchemeTask.hashCode()
activeTasks[taskId] = true

val request = startURLSchemeTask.request
val url = request.URL?.absoluteString ?: ""

KLogger.info { "WKSchemeHandler: Intercepting request for $url" }

// Build WebRequest
val headerMap = mutableMapOf<String, String>()
request.allHTTPHeaderFields?.forEach {
headerMap[it.key.toString()] = it.value.toString()
}

val webRequest = WebRequest(
url = url,
headers = headerMap,
isForMainFrame = true,
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
isRedirect = false,
method = request.HTTPMethod ?: "GET",
)

// Check if we have an interceptor
val interceptor = navigator.requestInterceptor
if (interceptor == null) {
KLogger.w { "WKSchemeHandler: No request interceptor set, failing request" }
failTask(startURLSchemeTask, "No request interceptor configured")
activeTasks.remove(taskId)
return
}

// Call the interceptor
val result = interceptor.onInterceptUrlRequest(webRequest, navigator)

// Check if task was cancelled
if (activeTasks[taskId] != true) {
KLogger.info { "WKSchemeHandler: Task was cancelled" }
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
return
}

when (result) {
is WebRequestInterceptResult.Respond -> {
respondWithData(startURLSchemeTask, result, url)
}
is WebRequestInterceptResult.Reject -> {
failTask(startURLSchemeTask, "Request rejected by interceptor")
}
else -> {
// For Allow and Modify, we can't actually make the request
// because this is a custom scheme. Return an error.
failTask(startURLSchemeTask, "Custom scheme requires Respond result")
}
}

activeTasks.remove(taskId)
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
}

@ObjCSignatureOverride
override fun webView(webView: WKWebView, stopURLSchemeTask: WKURLSchemeTaskProtocol) {
val taskId = stopURLSchemeTask.hashCode()
activeTasks[taskId] = false
KLogger.info { "WKSchemeHandler: Task stopped" }
}

private fun respondWithData(
task: WKURLSchemeTaskProtocol,
result: WebRequestInterceptResult.Respond,
url: String,
) {
try {
// Build response headers
val headerFields = mutableMapOf<Any?, Any?>()
headerFields["Content-Type"] = result.mimeType
headerFields["Content-Length"] = result.data.size.toString()
result.headers.forEach { (key, value) ->
headerFields[key] = value
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
}

// Create HTTP response
val response = NSHTTPURLResponse(
uRL = NSURL.URLWithString(url)!!,
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
statusCode = result.statusCode.toLong(),
HTTPVersion = "HTTP/1.1",
headerFields = headerFields,
)

// Send response
task.didReceiveResponse(response!!)
Comment thread
sebastiangl marked this conversation as resolved.
Outdated

// Send data
if (result.data.isNotEmpty()) {
result.data.usePinned { pinned ->
val nsData = NSData.create(
bytes = pinned.addressOf(0),
length = result.data.size.toULong(),
)
task.didReceiveData(nsData)
}
}

// Finish
task.didFinish()

KLogger.info { "WKSchemeHandler: Successfully responded with ${result.data.size} bytes" }
} catch (e: Exception) {
KLogger.e { "WKSchemeHandler: Error responding: ${e.message}" }
failTask(task, e.message ?: "Unknown error")
}
}

private fun failTask(task: WKURLSchemeTaskProtocol, message: String) {
try {
val error = platform.Foundation.NSError.errorWithDomain(
domain = "WKSchemeHandler",
code = -1,
userInfo = mapOf("NSLocalizedDescriptionKey" to message),
)
task.didFailWithError(error)
} catch (e: Exception) {
KLogger.e { "WKSchemeHandler: Error failing task: ${e.message}" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ class WKNavigationDelegate(
}
decisionHandler(WKNavigationActionPolicy.WKNavigationActionPolicyCancel)
}

is WebRequestInterceptResult.Respond -> {
// Respond is handled by WKSchemeHandler for custom schemes.
// For navigation requests, treat as reject since we can't provide custom response here.
KLogger.w { "Respond interceptResult not supported in navigation delegate, use custom scheme" }
decisionHandler(WKNavigationActionPolicy.WKNavigationActionPolicyCancel)
}
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.ui.viewinterop.UIKitInteropProperties
import androidx.compose.ui.viewinterop.UIKitView
import com.multiplatform.webview.jsbridge.ConsoleBridge
import com.multiplatform.webview.jsbridge.WebViewJsBridge
import com.multiplatform.webview.request.WKSchemeHandler
import com.multiplatform.webview.util.toUIColor
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
Expand Down Expand Up @@ -48,6 +49,7 @@ actual fun ActualWebView(
webViewJsBridge = webViewJsBridge,
onCreated = onCreated,
onDispose = onDispose,
platformWebViewParams = platformWebViewParams,
factory = factory,
)
}
Expand All @@ -57,7 +59,16 @@ actual data class WebViewFactoryParam(
val config: WKWebViewConfiguration,
)

actual class PlatformWebViewParams
/**
* iOS-specific WebView parameters.
*
* @param customSchemes List of custom URL schemes to register (e.g., "app", "local").
* Requests to these schemes will be handled by the RequestInterceptor,
* which should return WebRequestInterceptResult.Respond with the response data.
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
*/
actual class PlatformWebViewParams(
val customSchemes: List<String> = emptyList(),
)

/** Default WebView factory for iOS. */
@OptIn(ExperimentalForeignApi::class)
Expand All @@ -80,6 +91,7 @@ fun IOSWebView(
webViewJsBridge: WebViewJsBridge?,
onCreated: (NativeWebView) -> Unit,
onDispose: (NativeWebView) -> Unit,
platformWebViewParams: PlatformWebViewParams?,
factory: (WebViewFactoryParam) -> NativeWebView,
) {
val observer =
Expand All @@ -90,6 +102,7 @@ fun IOSWebView(
)
}
val navigationDelegate = remember { WKNavigationDelegate(state, navigator) }
val schemeHandler = remember { WKSchemeHandler(navigator) }
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
val scope = rememberCoroutineScope()

UIKitView(
Expand All @@ -116,6 +129,11 @@ fun IOSWebView(
value = state.webSettings.allowUniversalAccessFromFileURLs,
forKey = "allowUniversalAccessFromFileURLs",
)

// Register custom URL scheme handlers
platformWebViewParams?.customSchemes?.forEach { scheme ->
setURLSchemeHandler(schemeHandler, forURLScheme = scheme)
}
Comment thread
sebastiangl marked this conversation as resolved.
Outdated
}
factory(WebViewFactoryParam(config))
.apply {
Expand Down
Loading