Not authorized

This commit is contained in:
Philipp Heckel 2026-01-11 23:27:12 -05:00
parent 88bfee1ea7
commit 297ac864a4
6 changed files with 61 additions and 19 deletions

View file

@ -20,9 +20,12 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.heckel.ntfy.service.isConnectionRefused
import io.heckel.ntfy.service.NotAuthorizedException
import io.heckel.ntfy.service.WebSocketNotSupportedException
import io.heckel.ntfy.service.hasCause
import kotlinx.coroutines.flow.Flow
import java.lang.reflect.Type
import java.net.ConnectException
@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true), Index(value = ["upConnectorToken"], unique = true)])
data class Subscription(
@ -105,7 +108,15 @@ data class ConnectionDetails(
}
fun isConnectionRefused(): Boolean {
return error?.let { isConnectionRefused(it) } ?: false
return error?.hasCause<ConnectException>() ?: false
}
fun isWebSocketNotSupported(): Boolean {
return error?.hasCause<WebSocketNotSupportedException>() ?: false
}
fun isNotAuthorized(): Boolean {
return error?.hasCause<NotAuthorizedException>() ?: false
}
}

View file

@ -2,7 +2,6 @@ package io.heckel.ntfy.service
import okhttp3.internal.http2.StreamResetException
import java.io.EOFException
import java.net.ConnectException
interface Connection {
fun start()
@ -10,6 +9,26 @@ interface Connection {
fun since(): String?
}
/**
* Exception thrown when the server does not support WebSocket connections.
* This typically happens when the server returns a non-101 response during the WebSocket upgrade.
*/
class WebSocketNotSupportedException(
responseCode: Int,
responseMessage: String?,
cause: Throwable? = null
) : Exception("WebSocket upgrade failed with HTTP $responseCode: $responseMessage", cause)
/**
* Exception thrown when the server responds with HTTP 401/403
*/
class NotAuthorizedException(
responseCode: Int,
responseMessage: String?,
cause: Throwable? = null
) : Exception("User not authorized, HTTP $responseCode: $responseMessage", cause)
/**
* Represents a unique connection identifier that changes every time a
* connection needs to be re-established.
@ -25,16 +44,9 @@ data class ConnectionId(
val reconnectVersion: Long // Incremented to force reconnection for this baseUrl
)
/**
* Checks if the throwable or any of its causes is of the specified type.
*/
inline fun <reified T : Throwable> Throwable.hasCause(): Boolean {
var current: Throwable? = this
while (current != null) {
if (current is T) return true
current = current.cause
}
return false
fun isResponseCode(response: okhttp3.Response?, vararg codes: Int): Boolean {
val responseCode = response?.code ?: return false
return responseCode in codes
}
/**
@ -46,9 +58,13 @@ fun isConnectionBrokenException(t: Throwable): Boolean {
}
/**
* Returns true if the exception indicates the connection was refused
* (e.g., server is down or address is incorrect).
* Checks if the throwable or any of its causes is of the specified type.
*/
fun isConnectionRefused(t: Throwable): Boolean {
return t.hasCause<ConnectException>()
}
inline fun <reified T : Throwable> Throwable.hasCause(): Boolean {
var current: Throwable? = this
while (current != null) {
if (current is T) return true
current = current.cause
}
return false
}

View file

@ -80,6 +80,7 @@ class JsonConnection(
retryMillis = nextRetryMillis(retryMillis, startTime)
val nextRetryTime = System.currentTimeMillis() + retryMillis
val error = if (isConnectionBrokenException(e)) null else e
// FIXME add NotAuthorizedException
connectionDetailsListener(subscriptionIds, ConnectionState.CONNECTING, error, nextRetryTime)
Log.w(TAG, "[$url] Retrying connection in ${retryMillis / 1000}s ...")
delay(retryMillis)

View file

@ -185,7 +185,17 @@ class WsConnection(
errorCount++
val retrySeconds = RETRY_SECONDS.getOrNull(errorCount) ?: RETRY_SECONDS.last()
val nextRetryTime = System.currentTimeMillis() + (retrySeconds * 1000L)
val error = if (isConnectionBrokenException(t)) null else t
// Special cases:
// - Ignore broken connections in the UI, we don't want to show warning icons
// - Handle authentication errors
// - Handle servers that do not support WebSockets
val error = when {
isConnectionBrokenException(t) -> null
isResponseCode(response, 401, 403) -> NotAuthorizedException(response!!.code, response.message, t)
isResponseCode(response, 101) -> WebSocketNotSupportedException(response!!.code, response.message, t)
else -> t
}
connectionDetailsListener(subscriptionIds, ConnectionState.CONNECTING, error, nextRetryTime)
scheduleReconnect(retrySeconds)
}

View file

@ -197,6 +197,8 @@ class ConnectionErrorFragment : DialogFragment() {
if (details != null && details.hasError()) {
errorTextView.text = when {
details.isConnectionRefused() -> getString(R.string.connection_error_dialog_connection_refused)
details.isWebSocketNotSupported() -> getString(R.string.connection_error_dialog_websocket_not_supported)
details.isNotAuthorized() -> getString(R.string.connection_error_dialog_not_authorized)
else -> getErrorDisplayText(details.error)
}
val stackTrace = details.getStackTraceString()

View file

@ -293,6 +293,8 @@
<string name="connection_error_dialog_title">Connection error</string>
<string name="connection_error_dialog_message">There was a problem connecting to %1$s. The app will keep trying to reconnect in the background.</string>
<string name="connection_error_dialog_connection_refused">Connection refused. The server may be down or the address may be incorrect.</string>
<string name="connection_error_dialog_websocket_not_supported">WebSocket not supported. The server may not support WebSocket connections, or the address may be incorrect.</string>
<string name="connection_error_dialog_not_authorized">Not authorized. The server returned a HTTP 401/403 response. Please check if your username and password are correct.</string>
<string name="connection_error_dialog_copy">Copy</string>
<string name="connection_error_dialog_retry_now">Retry now</string>
<string name="connection_error_dialog_retry_countdown">Retrying in %1$ds…</string>