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 @@ -10,7 +10,9 @@ import dev.hotwire.turbo.demo.util.SIGN_IN_URL
import dev.hotwire.turbo.fragments.TurboWebFragment
import dev.hotwire.turbo.nav.TurboNavGraphDestination
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.errors.HttpError
import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE
import dev.hotwire.turbo.errors.TurboVisitError
import dev.hotwire.turbo.visit.TurboVisitOptions

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

override fun onVisitErrorReceived(location: String, errorCode: Int) {
when (errorCode) {
401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE))
else -> super.onVisitErrorReceived(location, errorCode)
override fun onVisitErrorReceived(location: String, error: TurboVisitError) {
if (error is HttpError.ClientError.Unauthorized) {
navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE))
} 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.errors.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.errors.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
154 changes: 154 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/HttpError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package dev.hotwire.turbo.errors

import android.webkit.WebResourceResponse

/**
* Errors representing HTTP status codes received from the server.
*/
sealed interface HttpError : TurboVisitError {
val statusCode: Int
val reasonPhrase: String?

/**
* Errors representing HTTP client errors in the 400..499 range.
*/
sealed interface ClientError : HttpError {
data object BadRequest : ClientError {
override val statusCode = 400
override val reasonPhrase = "Bad Request"
}

data object Unauthorized : ClientError {
override val statusCode = 401
override val reasonPhrase = "Unauthorized"
}

data object Forbidden : ClientError {
override val statusCode = 403
override val reasonPhrase = "Forbidden"
}

data object NotFound : ClientError {
override val statusCode = 404
override val reasonPhrase = "Not Found"
}

data object MethodNotAllowed : ClientError {
override val statusCode = 405
override val reasonPhrase = "Method Not Allowed"
}

data object NotAccessible : ClientError {
override val statusCode = 406
override val reasonPhrase = "Not Accessible"
}

data object ProxyAuthenticationRequired : ClientError {
override val statusCode = 407
override val reasonPhrase = "Proxy Authentication Required"
}

data object RequestTimeout : ClientError {
override val statusCode = 408
override val reasonPhrase = "Request Timeout"
}

data object Conflict : ClientError {
override val statusCode = 409
override val reasonPhrase = "Conflict"
}

data object MisdirectedRequest : ClientError {
override val statusCode = 421
override val reasonPhrase = "Misdirected Request"
}

data object UnprocessableEntity : ClientError {
override val statusCode = 422
override val reasonPhrase = "Unprocessable Entity"
}

data object PreconditionRequired : ClientError {
override val statusCode = 428
override val reasonPhrase = "Precondition Required"
}

data object TooManyRequests : ClientError {
override val statusCode = 429
override val reasonPhrase = "Too Many Requests"
}

data class Other(
override val statusCode: Int,
override val reasonPhrase: String?
) : ClientError
}

/**
* Errors representing HTTP server errors in the 500..599 range.
*/
sealed interface ServerError : HttpError {
data object InternalServerError : ServerError {
override val statusCode = 500
override val reasonPhrase = "Internal Server Error"
}

data object NotImplemented : ServerError {
override val statusCode = 501
override val reasonPhrase = "Not Implemented"
}

data object BadGateway : ServerError {
override val statusCode = 502
override val reasonPhrase = "Bad Gateway"
}

data object ServiceUnavailable : ServerError {
override val statusCode = 503
override val reasonPhrase = "Service Unavailable"
}

data object GatewayTimeout : ServerError {
override val statusCode = 504
override val reasonPhrase = "Gateway Timeout"
}

data object HttpVersionNotSupported : ServerError {
override val statusCode = 505
override val reasonPhrase = "Http Version Not Supported"
}

data class Other(
override val statusCode: Int,
override val reasonPhrase: String?
) : ServerError
}

companion object {
fun from(errorResponse: WebResourceResponse): HttpError {
return getError(errorResponse.statusCode, errorResponse.reasonPhrase)
}

fun from(statusCode: Int): HttpError {
return getError(statusCode, null)
}

private fun getError(statusCode: Int, reasonPhrase: String?): HttpError {
if (statusCode in 400..499) {
return ClientError::class.sealedSubclasses
.mapNotNull { it.objectInstance }
.firstOrNull { it.statusCode == statusCode }
?: ClientError.Other(statusCode, reasonPhrase)
}

if (statusCode in 500..599) {
return ServerError::class.sealedSubclasses
.map { it.objectInstance }
.firstOrNull { it?.statusCode == statusCode }
?: ServerError.Other(statusCode, reasonPhrase)
jayohms marked this conversation as resolved.
Show resolved Hide resolved
}

throw IllegalArgumentException("Invalid HTTP error status code: $statusCode")
jhutarek marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
17 changes: 17 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/LoadError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.hotwire.turbo.errors

/**
* Errors representing when turbo.js or the native adapter fails
* to load on a page.
*/
sealed interface LoadError : TurboVisitError {
val description: String

data object NotPresent : LoadError {
override val description = "Turbo Not Present"
}

data object NotReady : LoadError {
override val description = "Turbo Not Ready"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.hotwire.turbo.errors

/**
* Represents all possible errors received when attempting to load a page.
*/
sealed interface TurboVisitError
130 changes: 130 additions & 0 deletions turbo/src/main/kotlin/dev/hotwire/turbo/errors/WebError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package dev.hotwire.turbo.errors

import androidx.webkit.WebResourceErrorCompat
import androidx.webkit.WebViewClientCompat
import androidx.webkit.WebViewFeature
import androidx.webkit.WebViewFeature.isFeatureSupported

/**
* Errors representing WebViewClient.ERROR_* errors received
* from the WebView when attempting to load a page.
* https://developer.android.com/reference/android/webkit/WebViewClient
*/
sealed interface WebError : TurboVisitError {
val errorCode: Int
val description: String?

data object Unknown : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNKNOWN
override val description = "Unknown"
}

data object HostLookup : WebError {
override val errorCode = WebViewClientCompat.ERROR_HOST_LOOKUP
override val description = "Host Lookup"
}

data object UnsupportedAuthScheme : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNSUPPORTED_AUTH_SCHEME
override val description = "Unsupported Auth Scheme"
}

data object Authentication : WebError {
override val errorCode = WebViewClientCompat.ERROR_AUTHENTICATION
override val description = "Authentication"
}

data object ProxyAuthentication : WebError {
override val errorCode = WebViewClientCompat.ERROR_PROXY_AUTHENTICATION
override val description = "Proxy Authentication"
}

data object Connect : WebError {
override val errorCode = WebViewClientCompat.ERROR_CONNECT
override val description = "Connect"
}

data object IO : WebError {
override val errorCode = WebViewClientCompat.ERROR_IO
override val description = "IO"
}

data object Timeout : WebError {
override val errorCode = WebViewClientCompat.ERROR_TIMEOUT
override val description = "Timeout"
}

data object RedirectLoop : WebError {
override val errorCode = WebViewClientCompat.ERROR_REDIRECT_LOOP
override val description = "Redirect Loop"
}

data object UnsupportedScheme : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNSUPPORTED_SCHEME
override val description = "Unsupported Scheme"
}

data object FailedSslHandshake : WebError {
override val errorCode = WebViewClientCompat.ERROR_FAILED_SSL_HANDSHAKE
override val description = "Failed SSL Handshake"
}

data object BadUrl : WebError {
override val errorCode = WebViewClientCompat.ERROR_BAD_URL
override val description = "Bad URL"
}

data object File : WebError {
override val errorCode = WebViewClientCompat.ERROR_FILE
override val description = "File"
}

data object FileNotFound : WebError {
override val errorCode = WebViewClientCompat.ERROR_FILE_NOT_FOUND
override val description = "File Not Found"
}

data object TooManyRequests : WebError {
override val errorCode = WebViewClientCompat.ERROR_TOO_MANY_REQUESTS
override val description = "Too Many Requests"
}

data object UnsafeResource : WebError {
override val errorCode = WebViewClientCompat.ERROR_UNSAFE_RESOURCE
override val description = "Unsafe Resource"
}

data class Other(
override val errorCode: Int,
override val description: String?
) : WebError

companion object {
fun from(error: WebResourceErrorCompat): WebError {
val errorCode = if (isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
error.errorCode
} else {
0
}

val description = if (isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) {
error.description.toString()
} else {
null
}

return getError(errorCode, description)
}

fun from(errorCode: Int): WebError {
return getError(errorCode, null)
}

private fun getError(errorCode: Int, description: String?): WebError {
return WebError::class.sealedSubclasses
.mapNotNull { it.objectInstance }
.firstOrNull { it.errorCode == errorCode }
?: Other(errorCode, description)
}
}
}
Loading
Loading