Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

![ErrorResponse Interceptor Demo](media/error-response-interceptor.gif)

#### 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.
Expand Down
4 changes: 2 additions & 2 deletions sample/iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -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)",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>
<head>
<header><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no'></header>
<link rel="stylesheet" type="text/css" href="styles.css">
<script type="text/javascript" src="script.js"></script>
<title>Compose WebView Multiplatform</title>
</head>
<body>
<h1 class="title">Compose WebView Multiplatform</h1>
<h2 class="subtitle" id="subtitle">Error Response</h2>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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<ErrorResponse?>(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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ internal fun WebViewApp() {
composable("file") {
FileChooseWebViewSample(controller)
}
composable("error") {
ErrorResponseSample(controller)
}
}
}

Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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) }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading