diff --git a/README.md b/README.md
index 576bc218..26b5bf66 100644
--- a/README.md
+++ b/README.md
@@ -420,6 +420,36 @@ val navigator =
)
```
+### Intercepting Error Responses
+
+You can intercept failed navigations and decide whether to suppress the default platform error UI or continue with normal handling. This is useful for showing a custom error screen, logging, or redirecting.
+
+
+
+#### API
+- `ErrorResponse`: encapsulates platform-agnostic error details (`errorCode`, `description`, and the failing `url` when available).
+- `ErrorResponseInterceptor`: callback invoked when a page/resource load fails.
+- `ShouldStopLoading`: return `true` to stop default handling; return `false` to allow it.
+
+#### Quick start
+```kotlin
+val webViewNavigator = rememberWebViewNavigator(
+ errorResponseInterceptor = object : ErrorResponseInterceptor {
+ override fun onInterceptErrorResponse(
+ response: ErrorResponse,
+ navigator: WebViewNavigator
+ ): ShouldStopLoading {
+ // Show a custom UI or navigate elsewhere
+ return true // suppress default error handling
+ }
+ }
+)
+
+WebView(
+ state = webViewState,
+ navigator = webViewNavigator,
+)
+```
## WebSettings
Starting from version 1.3.0, this library allows users to customize web settings.
diff --git a/sample/iosApp/iosApp.xcodeproj/project.pbxproj b/sample/iosApp/iosApp.xcodeproj/project.pbxproj
index 6016cf1e..ddcb0db1 100644
--- a/sample/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/sample/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -320,7 +320,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = 9MQUMBML8C;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@@ -352,7 +352,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = 9MQUMBML8C;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
diff --git a/sample/shared/src/commonMain/composeResources/files/samples/errorResponse.html b/sample/shared/src/commonMain/composeResources/files/samples/errorResponse.html
new file mode 100644
index 00000000..8d2ece81
--- /dev/null
+++ b/sample/shared/src/commonMain/composeResources/files/samples/errorResponse.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Compose WebView Multiplatform
+
+
+Compose WebView Multiplatform
+Error Response
+
+
diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/ErrorResponseSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/ErrorResponseSample.kt
new file mode 100644
index 00000000..7e86cf5c
--- /dev/null
+++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/ErrorResponseSample.kt
@@ -0,0 +1,178 @@
+package com.kevinnzou.sample
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import co.touchlab.kermit.Logger
+import com.multiplatform.webview.jsbridge.rememberWebViewJsBridge
+import com.multiplatform.webview.response.ErrorResponse
+import com.multiplatform.webview.response.ErrorResponseInterceptor
+import com.multiplatform.webview.response.ShouldStopLoading
+import com.multiplatform.webview.util.KLogSeverity
+import com.multiplatform.webview.web.WebView
+import com.multiplatform.webview.web.WebViewFileReadType
+import com.multiplatform.webview.web.WebViewNavigator
+import com.multiplatform.webview.web.WebViewState
+import com.multiplatform.webview.web.rememberWebViewNavigator
+import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile
+import compose_webview_multiplatform.sample.shared.generated.resources.Res
+
+/**
+ * Created By Matthias Hennemeyer On 2025/8/8
+ *
+ * Sample intercepting an error response
+ *
+ * Note: Developers targeting the Desktop platform should refer to
+ * [README.desktop.md](https://github.com/KevinnZou/compose-webview-multiplatform/blob/main/README.desktop.md)
+ * for setup instructions first.
+ */
+@Composable
+internal fun ErrorResponseSample(navHostController: NavHostController? = null) {
+ val webViewState =
+ rememberWebViewStateWithHTMLFile(
+ fileName = Res.getUri("files/samples/errorResponse.html"),
+ readType = WebViewFileReadType.COMPOSE_RESOURCE_FILES,
+ )
+ val errorUrl = "http://matthiashennemeyer.com/404-on-android-and-tls-error-on-ios"
+ var errorResponse by remember { mutableStateOf(null) }
+ val webViewNavigator = rememberWebViewNavigator(
+ errorResponseInterceptor = object : ErrorResponseInterceptor {
+ override fun onInterceptErrorResponse(
+ response: ErrorResponse,
+ navigator: WebViewNavigator
+ ): ShouldStopLoading {
+ Logger.e("errorResponseInterceptor: code: ${response.errorCode} description: ${response.description}")
+ errorResponse = response
+ return true
+ }
+ }
+ )
+ val jsBridge = rememberWebViewJsBridge(webViewNavigator)
+ LaunchedEffect(Unit) {
+ init(webViewState)
+ }
+ MaterialTheme {
+ Scaffold { innerPadding ->
+ Column(
+ modifier =
+ Modifier
+ .padding(innerPadding)
+ .fillMaxSize(),
+ ) {
+ Column {
+ TopAppBar(
+ modifier =
+ Modifier
+ .background(
+ color = MaterialTheme.colors.primary,
+ ).padding(
+ top =
+ WindowInsets.statusBars
+ .asPaddingValues()
+ .calculateTopPadding(),
+ ),
+ title = { Text(text = "Error Response Sample") },
+ navigationIcon = {
+ IconButton(onClick = {
+ navHostController?.popBackStack()
+ }) {
+ Icon(
+ imageVector = Icons.Default.ArrowBack,
+ contentDescription = "Back",
+ )
+ }
+ },
+ actions = {
+ Button(
+ onClick = {
+ webViewNavigator.loadUrl(errorUrl)
+ },
+ modifier = Modifier.padding(horizontal = 4.dp),
+ colors =
+ ButtonDefaults.buttonColors(
+ backgroundColor = Color.White,
+ contentColor = MaterialTheme.colors.primary,
+ ),
+ ) {
+ Text(
+ "Trigger Error",
+ style = MaterialTheme.typography.caption,
+ )
+ }
+ },
+
+ )
+
+ AnimatedVisibility(visible = (errorResponse != null)) {
+ Box(modifier = Modifier.fillMaxWidth()) {
+ Button(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(horizontal = 4.dp)
+ ,
+ onClick = { errorResponse = null }
+ ) { Text(text = "X") }
+ }
+
+
+ if (errorResponse != null) {
+ Text(text = "Error: ${errorResponse!!.description}\nCode: ${errorResponse!!.errorCode}")
+ }
+ }
+
+ // WebView without overlay buttons
+ WebView(
+ state = webViewState,
+ modifier = Modifier.fillMaxSize(),
+ captureBackPresses = false,
+ navigator = webViewNavigator,
+ webViewJsBridge = jsBridge,
+ )
+ }
+ }
+ }
+ }
+}
+
+fun init(webViewState: WebViewState) {
+ webViewState.webSettings.apply {
+ zoomLevel = 1.0
+ isJavaScriptEnabled = true
+ logSeverity = KLogSeverity.Debug
+ allowFileAccessFromFileURLs = true
+ allowUniversalAccessFromFileURLs = true
+ androidWebSettings.apply {
+ isAlgorithmicDarkeningAllowed = true
+ safeBrowsingEnabled = true
+ allowFileAccess = true
+ }
+ }
+}
diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt
index 60550c1f..de344824 100644
--- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt
+++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt
@@ -60,6 +60,9 @@ internal fun WebViewApp() {
composable("file") {
FileChooseWebViewSample(controller)
}
+ composable("error") {
+ ErrorResponseSample(controller)
+ }
}
}
@@ -116,6 +119,12 @@ fun MainScreen(controller: NavController) {
}) {
Text("File Choose Sample", fontSize = 18.sp)
}
+ Spacer(modifier = Modifier.height(20.dp))
+ Button(onClick = {
+ controller.navigate("error")
+ }) {
+ Text("Error Response Sample", fontSize = 18.sp)
+ }
}
}
}
diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt
index 2ac8cc58..82b994b2 100644
--- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt
+++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt
@@ -33,6 +33,7 @@ import com.multiplatform.webview.jsbridge.ConsoleBridge
import com.multiplatform.webview.jsbridge.WebViewJsBridge
import com.multiplatform.webview.request.WebRequest
import com.multiplatform.webview.request.WebRequestInterceptResult
+import com.multiplatform.webview.response.ErrorResponse
import com.multiplatform.webview.setting.PlatformWebSettings
import com.multiplatform.webview.util.InternalStoragePathHandler
import com.multiplatform.webview.util.KLogger
@@ -357,6 +358,31 @@ open class AccompanistWebViewClient : WebViewClient() {
navigator.canGoForward = view.canGoForward()
}
+ override fun onReceivedHttpError(
+ view: WebView?,
+ request: WebResourceRequest?,
+ errorResponse: WebResourceResponse?
+ ) {
+ super.onReceivedHttpError(view, request, errorResponse)
+ KLogger.e {
+ "onReceivedHttpError: $errorResponse"
+ }
+
+
+ if (navigator.errorResponseInterceptor?.onInterceptErrorResponse(
+ ErrorResponse(
+ url = request?.url.toString(),
+ errorCode = errorResponse?.statusCode?.toLong(),
+ description = errorResponse?.reasonPhrase
+ ),
+ navigator
+ ) ?: false
+ ) {
+ view?.stopLoading()
+ navigator.stopLoading()
+ }
+ }
+
override fun onReceivedError(
view: WebView,
request: WebResourceRequest?,
@@ -381,6 +407,17 @@ open class AccompanistWebViewClient : WebViewClient() {
),
)
}
+ if (navigator.errorResponseInterceptor?.onInterceptErrorResponse(
+ ErrorResponse(
+ url = request?.url.toString(),
+ errorCode = error?.errorCode?.toLong(),
+ description = error?.description.toString()),
+ navigator
+ ) ?: false
+ ) {
+ view.stopLoading()
+ navigator.stopLoading()
+ }
}
override fun shouldOverrideUrlLoading(
diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/response/ErrorResponse.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/response/ErrorResponse.kt
new file mode 100644
index 00000000..35c51e6d
--- /dev/null
+++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/response/ErrorResponse.kt
@@ -0,0 +1,9 @@
+package com.multiplatform.webview.response
+
+import com.multiplatform.webview.web.WebContent
+
+data class ErrorResponse(
+ val description: String? = null,
+ val errorCode: Long? = null,
+ val url: String? = null
+)
\ No newline at end of file
diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/response/ErrorResponseInterceptor.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/response/ErrorResponseInterceptor.kt
new file mode 100644
index 00000000..bd8d1262
--- /dev/null
+++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/response/ErrorResponseInterceptor.kt
@@ -0,0 +1,11 @@
+package com.multiplatform.webview.response
+
+import com.multiplatform.webview.web.WebViewNavigator
+
+typealias ShouldStopLoading = Boolean
+interface ErrorResponseInterceptor {
+ fun onInterceptErrorResponse(
+ response: ErrorResponse,
+ navigator: WebViewNavigator,
+ ): ShouldStopLoading
+}
\ No newline at end of file
diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt
index ddf0b405..785e1619 100644
--- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt
+++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt
@@ -8,6 +8,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.multiplatform.webview.request.RequestInterceptor
+import com.multiplatform.webview.response.ErrorResponseInterceptor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -28,6 +29,7 @@ import kotlinx.coroutines.withContext
class WebViewNavigator(
val coroutineScope: CoroutineScope,
val requestInterceptor: RequestInterceptor? = null,
+ val errorResponseInterceptor: ErrorResponseInterceptor? = null,
) {
/**
* Sealed class for constraining possible navigation events.
@@ -310,4 +312,5 @@ class WebViewNavigator(
fun rememberWebViewNavigator(
coroutineScope: CoroutineScope = rememberCoroutineScope(),
requestInterceptor: RequestInterceptor? = null,
-): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope, requestInterceptor) }
+ errorResponseInterceptor: ErrorResponseInterceptor? = null,
+): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope, requestInterceptor, errorResponseInterceptor) }
diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt
index 78b2e6bb..3b3307ec 100644
--- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt
+++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WKNavigationDelegate.kt
@@ -2,6 +2,7 @@ package com.multiplatform.webview.web
import com.multiplatform.webview.request.WebRequest
import com.multiplatform.webview.request.WebRequestInterceptResult
+import com.multiplatform.webview.response.ErrorResponse
import com.multiplatform.webview.util.KLogger
import com.multiplatform.webview.util.getPlatformVersionDouble
import com.multiplatform.webview.util.notZero
@@ -117,6 +118,15 @@ class WKNavigationDelegate(
KLogger.e {
"didFailNavigation"
}
+ if (navigator.errorResponseInterceptor?.let { errorResponseInterceptor ->
+ errorResponseInterceptor.onInterceptErrorResponse(
+ ErrorResponse(withError.description, withError.code, webView.URL.toString()),
+ navigator
+ )
+ } ?: false) {
+ webView.stopLoading()
+ navigator.stopLoading()
+ }
}
override fun webView(