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
159 changes: 159 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,159 @@
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
}

data class UnknownError(
override val statusCode: Int,
override val reasonPhrase: String?
) : HttpError

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
}

return UnknownError(statusCode, reasonPhrase)
}
}
}
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
Loading
Loading