Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass more detailed visit errors to apps to avoid ambiguity #317

Merged
merged 7 commits into from
Feb 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import dev.hotwire.turbo.fragments.TurboWebFragment
import dev.hotwire.turbo.nav.TurboNavGraphDestination
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitOptions

@TurboNavGraphDestination(uri = "turbo://fragment/web")
Expand Down Expand Up @@ -58,10 +59,10 @@ open class WebFragment : TurboWebFragment(), NavDestination {
menuProgress?.isVisible = false
}

override fun onVisitErrorReceived(location: String, errorCode: Int) {
when (errorCode) {
override fun onVisitErrorReceived(location: String, error: TurboVisitError) {
when (error.code) {
401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE))
else -> super.onVisitErrorReceived(location, errorCode)
else -> super.onVisitErrorReceived(location, error)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import dev.hotwire.turbo.demo.R
import dev.hotwire.turbo.nav.TurboNavGraphDestination
import dev.hotwire.turbo.visit.TurboVisitError

@TurboNavGraphDestination(uri = "turbo://fragment/web/home")
class WebHomeFragment : WebFragment() {
Expand All @@ -15,7 +16,7 @@ class WebHomeFragment : WebFragment() {
}

@SuppressLint("InflateParams")
override fun createErrorView(statusCode: Int): View {
override fun createErrorView(error: TurboVisitError): View {
return layoutInflater.inflate(R.layout.error_web_home, null)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisit
import dev.hotwire.turbo.visit.TurboVisitAction
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitOptions
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -158,8 +159,8 @@ internal class TurboWebFragmentDelegate(
/**
* Displays the error view that's implemented via [TurboWebFragmentCallback.createErrorView].
*/
fun showErrorView(code: Int) {
turboView?.addErrorView(callback.createErrorView(code))
fun showErrorView(error: TurboVisitError) {
turboView?.addErrorView(callback.createErrorView(error))
}

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -205,19 +206,19 @@ internal class TurboWebFragmentDelegate(
navDestination.fragmentViewModel.setTitle(title())
}

override fun onReceivedError(errorCode: Int) {
callback.onVisitErrorReceived(location, errorCode)
override fun onReceivedError(error: TurboVisitError) {
callback.onVisitErrorReceived(location, error)
}

override fun onRenderProcessGone() {
navigator.navigate(location, TurboVisitOptions(action = TurboVisitAction.REPLACE))
}

override fun requestFailedWithStatusCode(visitHasCachedSnapshot: Boolean, statusCode: Int) {
override fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: TurboVisitError) {
if (visitHasCachedSnapshot) {
callback.onVisitErrorReceivedWithCachedSnapshotAvailable(location, statusCode)
callback.onVisitErrorReceivedWithCachedSnapshotAvailable(location, error)
} else {
callback.onVisitErrorReceived(location, statusCode)
callback.onVisitErrorReceived(location, error)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dev.hotwire.turbo.delegates.TurboWebFragmentDelegate
import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES
import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebChromeClient
import dev.hotwire.turbo.visit.TurboVisitError

/**
* The base class from which all bottom sheet web fragments in a
Expand Down Expand Up @@ -82,15 +83,15 @@ abstract class TurboWebBottomSheetDialogFragment : TurboBottomSheetDialogFragmen
}

@SuppressLint("InflateParams")
override fun createErrorView(statusCode: Int): View {
override fun createErrorView(error: TurboVisitError): View {
return layoutInflater.inflate(R.layout.turbo_error, null)
}

override fun createWebChromeClient(): TurboWebChromeClient {
return TurboWebChromeClient(session)
}

override fun onVisitErrorReceived(location: String, errorCode: Int) {
webDelegate.showErrorView(errorCode)
override fun onVisitErrorReceived(location: String, error: TurboVisitError) {
webDelegate.showErrorView(error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dev.hotwire.turbo.session.TurboSessionModalResult
import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES
import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebChromeClient
import dev.hotwire.turbo.visit.TurboVisitError

/**
* The base class from which all web "standard" fragments (non-dialogs) in a
Expand Down Expand Up @@ -94,15 +95,15 @@ abstract class TurboWebFragment : TurboFragment(), TurboWebFragmentCallback {
}

@SuppressLint("InflateParams")
override fun createErrorView(statusCode: Int): View {
override fun createErrorView(error: TurboVisitError): View {
return layoutInflater.inflate(R.layout.turbo_error, null)
}

override fun createWebChromeClient(): TurboWebChromeClient {
return TurboWebChromeClient(session)
}

override fun onVisitErrorReceived(location: String, errorCode: Int) {
webDelegate.showErrorView(errorCode)
override fun onVisitErrorReceived(location: String, error: TurboVisitError) {
webDelegate.showErrorView(error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.webkit.HttpAuthHandler
import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebChromeClient
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisitError

/**
* Callback interface to be implemented by a [TurboWebFragment],
Expand All @@ -19,7 +20,7 @@ interface TurboWebFragmentCallback {
/**
* Inflate and return a new view to serve as an error view.
*/
fun createErrorView(statusCode: Int): View
fun createErrorView(error: TurboVisitError): View

/**
* Inflate and return a new view to serve as a progress view.
Expand Down Expand Up @@ -71,7 +72,7 @@ interface TurboWebFragmentCallback {
/**
* Called when a Turbo visit resulted in an error.
*/
fun onVisitErrorReceived(location: String, errorCode: Int) {}
fun onVisitErrorReceived(location: String, error: TurboVisitError) {}

/**
* Called when a Turbo form submission has started.
Expand All @@ -87,7 +88,7 @@ interface TurboWebFragmentCallback {
* Called when the Turbo visit resulted in an error, but a cached
* snapshot is being displayed, which may be stale.
*/
fun onVisitErrorReceivedWithCachedSnapshotAvailable(location: String, errorCode: Int) {}
fun onVisitErrorReceivedWithCachedSnapshotAvailable(location: String, error: TurboVisitError) {}

/**
* Called when the WebView has received an HTTP authentication request.
Expand Down
54 changes: 44 additions & 10 deletions turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import dev.hotwire.turbo.util.*
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisit
import dev.hotwire.turbo.visit.TurboVisitAction
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitErrorType
import dev.hotwire.turbo.visit.TurboVisitOptions
import kotlinx.coroutines.*
import java.util.*
Expand Down Expand Up @@ -283,6 +285,12 @@ class TurboSession internal constructor(
*/
@JavascriptInterface
fun visitRequestFailedWithStatusCode(visitIdentifier: String, visitHasCachedSnapshot: Boolean, statusCode: Int) {
val visitError = TurboVisitError(
type = TurboVisitErrorType.HTTP_ERROR,
code = statusCode,
description = "Request failed"
)

logEvent(
"visitRequestFailedWithStatusCode",
"visitIdentifier" to visitIdentifier,
Expand All @@ -292,7 +300,7 @@ class TurboSession internal constructor(

currentVisit?.let { visit ->
if (visitIdentifier == visit.identifier) {
callback { it.requestFailedWithStatusCode(visitHasCachedSnapshot, statusCode) }
callback { it.requestFailedWithError(visitHasCachedSnapshot, visitError) }
}
}
}
Expand Down Expand Up @@ -477,9 +485,15 @@ class TurboSession internal constructor(
*/
@JavascriptInterface
fun turboFailedToLoad() {
logEvent("turboFailedToLoad")
val visitError = TurboVisitError(
type = TurboVisitErrorType.LOAD_ERROR,
code = -1,
description = "Turbo failed to load"
)

logEvent("turboFailedToLoad", "error" to visitError)
reset()
callback { it.onReceivedError(-1) }
callback { it.onReceivedError(visitError) }
}

/**
Expand Down Expand Up @@ -748,32 +762,52 @@ class TurboSession internal constructor(
super.onReceivedError(view, request, error)

if (request.isForMainFrame && isFeatureSupported(WEB_RESOURCE_ERROR_GET_CODE)) {
logEvent("onReceivedError", "errorCode" to error.errorCode)
val visitError = TurboVisitError(
type = TurboVisitErrorType.WEB_RESOURCE_ERROR,
code = error.errorCode,
description = if (isFeatureSupported(WEB_RESOURCE_ERROR_GET_DESCRIPTION)) {
error.description.toString()
} else {
null
}
)

logEvent("onReceivedError", "error" to visitError)
reset()
callback { it.onReceivedError(error.errorCode) }
callback { it.onReceivedError(visitError) }
}
}

override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
super.onReceivedHttpError(view, request, errorResponse)

if (request.isForMainFrame) {
logEvent("onReceivedHttpError", "statusCode" to errorResponse.statusCode)
val visitError = TurboVisitError(
type = TurboVisitErrorType.HTTP_ERROR,
code = errorResponse.statusCode,
description = errorResponse.reasonPhrase
)

logEvent("onReceivedHttpError", "error" to visitError)
reset()
callback { it.onReceivedError(errorResponse.statusCode) }
callback { it.onReceivedError(visitError) }
}
}

override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
super.onReceivedSslError(view, handler, error)
handler.cancel()

logEvent("onReceivedSslError", "url" to error.url)
val visitError = TurboVisitError(
type = TurboVisitErrorType.WEB_SSL_ERROR,
code = error.primaryError
)

logEvent("onReceivedSslError", "error" to visitError)
reset()
callback { it.onReceivedError(-1) }
callback { it.onReceivedError(visitError) }
}

@TargetApi(Build.VERSION_CODES.O)
jayohms marked this conversation as resolved.
Show resolved Hide resolved
override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean {
logEvent("onRenderProcessGone", "didCrash" to detail.didCrash())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ package dev.hotwire.turbo.session

import android.webkit.HttpAuthHandler
import dev.hotwire.turbo.nav.TurboNavDestination
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitErrorType
import dev.hotwire.turbo.visit.TurboVisitOptions

internal interface TurboSessionCallback {
fun onPageStarted(location: String)
fun onPageFinished(location: String)
fun onReceivedError(errorCode: Int)
fun onReceivedError(error: TurboVisitError)
fun onRenderProcessGone()
fun onZoomed(newScale: Float)
fun onZoomReset(newScale: Float)
fun pageInvalidated()
fun requestFailedWithStatusCode(visitHasCachedSnapshot: Boolean, statusCode: Int)
fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: TurboVisitError)
fun onReceivedHttpAuthRequest(handler: HttpAuthHandler, host: String, realm: String)
fun visitRendered()
fun visitCompleted(completedOffline: Boolean)
Expand Down
18 changes: 18 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.hotwire.turbo.visit

data class TurboVisitError(
jayohms marked this conversation as resolved.
Show resolved Hide resolved
/**
* The [TurboVisitErrorType] type of error received.
*/
val type: TurboVisitErrorType,

/**
* The error code associated with the [TurboVisitErrorType] type.
*/
val code: Int,

/**
* The (optional) description of the error.
*/
val description: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.hotwire.turbo.visit

enum class TurboVisitErrorType {
jayohms marked this conversation as resolved.
Show resolved Hide resolved
/**
* Represents an error when Turbo and the javascript adapter fails to
* load on the page.
*/
LOAD_ERROR,

/**
* Represents an error received from your server with an HTTP status code.
* The code corresponds to the status code received from your server.
*/
HTTP_ERROR,

/**
* Represents an [androidx.webkit.WebResourceErrorCompat] error received
* from the WebView when attempting to load the page. The code corresponds
* to one of the ERROR_* constants in [androidx.webkit.WebViewClientCompat].
*/
WEB_RESOURCE_ERROR,

/**
* Represents an [android.net.http.SslError] error received from the WebView
* when attempting to load the page. The code corresponds to one of the
* SSL_* constants in [android.net.http.SslError].
*/
WEB_SSL_ERROR
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import dev.hotwire.turbo.nav.TurboNavDestination
import dev.hotwire.turbo.util.toJson
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisit
import dev.hotwire.turbo.visit.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitErrorType
import dev.hotwire.turbo.visit.TurboVisitOptions
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
Expand Down Expand Up @@ -88,14 +90,32 @@ class TurboSessionTest {
assertThat(session.currentVisit?.identifier).isEqualTo(visitIdentifier)
}

@Test
fun visitFailedToLoadCallsAdapter() {
val visitIdentifier = "12345"

session.currentVisit = visit.copy(identifier = visitIdentifier)
session.turboFailedToLoad()

verify(callback).onReceivedError(TurboVisitError(
type = TurboVisitErrorType.LOAD_ERROR,
code = -1,
description = "Turbo failed to load"
))
}

@Test
fun visitRequestFailedWithStatusCodeCallsAdapter() {
val visitIdentifier = "12345"

session.currentVisit = visit.copy(identifier = visitIdentifier)
session.visitRequestFailedWithStatusCode(visitIdentifier, true, 500)

verify(callback).requestFailedWithStatusCode(true, 500)
verify(callback).requestFailedWithError(true, TurboVisitError(
type = TurboVisitErrorType.HTTP_ERROR,
code = 500,
description = "Request failed"
))
}

@Test
Expand Down
Loading