diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/ImportScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/ImportScreen.kt index af9297a..9f4fc7f 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/ImportScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/ImportScreen.kt @@ -323,8 +323,18 @@ fun ImportScreen(settings: ServerSettings, reloadToken: Int, onImported: (Sessio } } } - } catch (_: ApiException) { + } catch (e: kotlinx.coroutines.CancellationException) { + // The screen leaving, not a failure -- and swallowing it would leave this + // loop reconnecting to a stream nobody is watching. + throw e + } catch (_: Exception) { // Retried below; the listing is the truth in the meantime. + // + // Any failure, not only an [ApiException]. A stream is an optimisation over + // the listing here, so nothing it can do is worth taking the app down for -- + // and catching only the failure that was expected means an unexpected one + // reaches the top of the app and closes it, from a screen that is merely + // loading a list. } finally { stream.close() } diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt index ce673f8..e523c0d 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -718,8 +718,15 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: () apply(entry) } } - } catch (e: ApiException) { - streamError = e.message + } catch (e: kotlinx.coroutines.CancellationException) { + // Leaving the screen or going below STARTED. Not a failure, and + // swallowing it would leave this loop reconnecting forever. + throw e + } catch (e: Exception) { + // Any failure, not only an [ApiException]: the stream reconnects from its + // cursor, so there is nothing a failure here can cost that is worth + // closing the app over. Reported on the screen either way. + streamError = e.message ?: e::class.simpleName } finally { stream.close() } diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/Sse.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/Sse.kt index 1e1897f..bbfbd16 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/Sse.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/Sse.kt @@ -47,9 +47,16 @@ class Sse(private val settings: ServerSettings) { * recovered from, indefinitely. */ fun run(path: String, onOpen: () -> Unit, onFrame: (name: String?, data: String) -> Unit) { - val connection = URL("${settings.baseUrl}$path").openConnection() as HttpURLConnection - this.connection = connection + // Opening is inside the try, not before it. Everything this method can fail at owes the + // caller the same kind of failure -- both callers retry an [ApiException] and let anything + // else reach the top of the app -- and a connection that could not even be constructed + // used to escape as a raw `IOException` from a line no `catch` covered. + var connection: HttpURLConnection? = null try { + connection = + (URL("${settings.baseUrl}$path").openConnection() as HttpURLConnection).also { + this.connection = it + } connection.applyPinnedTls() connection.connectTimeout = CONNECT_TIMEOUT_MS // No read timeout: between events there is nothing to read for as long as the thing @@ -90,7 +97,7 @@ class Sse(private val settings: ServerSettings) { ) } } finally { - connection.disconnect() + connection?.disconnect() this.connection = null } }