Skip to content

Commit 2e952cd

Browse files
authored
Improve error handling when using Response from Retrofit (#5487)
1 parent 713cc94 commit 2e952cd

File tree

5 files changed

+99
-183
lines changed

5 files changed

+99
-183
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
package io.homeassistant.companion.android.common.data.authentication
22

3-
class AuthorizationException : Exception()
3+
import okhttp3.ResponseBody
4+
5+
class AuthorizationException : Exception {
6+
constructor() : super()
7+
constructor(message: String, httpCode: Int, errorBody: ResponseBody?) : super("$message, httpCode: $httpCode, errorBody: ${errorBody?.string()}")
8+
}

common/src/main/kotlin/io/homeassistant/companion/android/common/data/authentication/impl/AuthenticationRepositoryImpl.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ class AuthenticationRepositoryImpl @AssistedInject constructor(
4444
serverManager.updateServer(
4545
server.copy(
4646
session = ServerSessionInfo(
47-
it.accessToken,
48-
it.refreshToken!!,
49-
System.currentTimeMillis() / 1000 + it.expiresIn,
50-
it.tokenType,
51-
installId,
47+
accessToken = it.accessToken,
48+
refreshToken = it.refreshToken!!,
49+
tokenExpiration = System.currentTimeMillis() / 1000 + it.expiresIn,
50+
tokenType = it.tokenType,
51+
installId = installId,
5252
),
5353
),
5454
)
@@ -159,11 +159,11 @@ class AuthenticationRepositoryImpl @AssistedInject constructor(
159159
serverManager.updateServer(
160160
server.copy(
161161
session = ServerSessionInfo(
162-
refreshedToken.accessToken,
163-
refreshToken,
164-
System.currentTimeMillis() / 1000 + refreshedToken.expiresIn,
165-
refreshedToken.tokenType,
166-
installId,
162+
accessToken = refreshedToken.accessToken,
163+
refreshToken = refreshToken,
164+
tokenExpiration = System.currentTimeMillis() / 1000 + refreshedToken.expiresIn,
165+
tokenType = refreshedToken.tokenType,
166+
installId = installId,
167167
),
168168
),
169169
)
@@ -173,7 +173,7 @@ class AuthenticationRepositoryImpl @AssistedInject constructor(
173173
) {
174174
revokeSession()
175175
}
176-
throw AuthorizationException()
176+
throw AuthorizationException("Failed to refresh token", it.code(), it.errorBody())
177177
}
178178
}
179179

Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package io.homeassistant.companion.android.common.data.integration
22

3+
import okhttp3.ResponseBody
4+
35
class IntegrationException : Exception {
46
constructor() : super()
57
constructor(message: String) : super(message)
68
constructor(cause: Throwable) : super(cause)
9+
constructor(message: String, cause: Throwable) : super(message, cause)
10+
constructor(message: String, httpCode: Int, errorBody: ResponseBody?) : super("$message, httpCode: $httpCode, errorBody: ${errorBody?.string()}")
711
}

common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt

Lines changed: 77 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import io.homeassistant.companion.android.common.data.integration.impl.entities.
1919
import io.homeassistant.companion.android.common.data.integration.impl.entities.FireEventRequest
2020
import io.homeassistant.companion.android.common.data.integration.impl.entities.GetConfigIntegrationRequest
2121
import io.homeassistant.companion.android.common.data.integration.impl.entities.GetZonesIntegrationRequest
22+
import io.homeassistant.companion.android.common.data.integration.impl.entities.IntegrationRequest
2223
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitRequest
2324
import io.homeassistant.companion.android.common.data.integration.impl.entities.RateLimitResponse
2425
import io.homeassistant.companion.android.common.data.integration.impl.entities.RegisterDeviceIntegrationRequest
@@ -37,13 +38,17 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.As
3738
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineEventType
3839
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AssistPipelineIntentEnd
3940
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
41+
import io.homeassistant.companion.android.common.util.FailFast
42+
import io.homeassistant.companion.android.database.server.Server
4043
import java.util.concurrent.TimeUnit
4144
import javax.inject.Named
4245
import kotlinx.coroutines.flow.Flow
4346
import kotlinx.coroutines.flow.filter
4447
import kotlinx.coroutines.flow.flow
4548
import kotlinx.coroutines.flow.map
4649
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
50+
import okhttp3.ResponseBody
51+
import retrofit2.Response
4752
import timber.log.Timber
4853

4954
class IntegrationRepositoryImpl @AssistedInject constructor(
@@ -127,44 +132,25 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
127132

128133
override suspend fun updateRegistration(deviceRegistration: DeviceRegistration, allowReregistration: Boolean) {
129134
val request = RegisterDeviceIntegrationRequest(createUpdateRegistrationRequest(deviceRegistration))
130-
var causeException: Exception? = null
131-
for (it in server.connection.getApiUrls()) {
132-
try {
133-
val response = integrationService.callWebhook(it.toHttpUrlOrNull()!!, request)
134-
// The server should return a body with the registration, but might return:
135-
// 200 with empty body for broken direct webhook
136-
// 404 for broken cloudhook
137-
// 410 for missing config entry
138-
if (response.isSuccessful) {
139-
if (response.code() == 200 && (response.body()?.contentLength() ?: 0) == 0L) {
140-
throw IllegalStateException("update_registration returned empty body")
141-
} else {
142-
persistDeviceRegistration(deviceRegistration)
143-
return
135+
server.callWebhookOnUrls(request, onSuccess = { response ->
136+
// The server should return a body with the registration, but might return:
137+
// 200 with empty body for broken direct webhook
138+
if (response.code() == 200 && response.body()?.contentLength() == 0L) {
139+
Timber.w("update_registration returned empty body")
140+
if (allowReregistration) {
141+
Timber.w("Device registration broken and reregistration allowed, reregistering")
142+
try {
143+
registerDevice(deviceRegistration)
144+
} catch (e: Exception) {
145+
throw IntegrationException("Device registration broken and reregistration failed", e)
144146
}
145-
} else if (response.code() == 404 || response.code() == 410) {
146-
throw IllegalStateException("update_registration returned code ${response.code()}")
147-
}
148-
} catch (e: Exception) {
149-
if (causeException == null) causeException = e
150-
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
151-
}
152-
}
153-
154-
if (causeException != null) {
155-
if (allowReregistration && (causeException is IllegalStateException)) {
156-
Timber.w(causeException, "Device registration broken, reregistering")
157-
try {
158-
registerDevice(deviceRegistration)
159-
} catch (e: Exception) {
160-
throw IntegrationException(e)
147+
} else {
148+
throw IntegrationException("Device registration broken and reregistration not allowed.")
161149
}
162150
} else {
163-
throw IntegrationException(causeException)
151+
persistDeviceRegistration(deviceRegistration)
164152
}
165-
} else {
166-
throw IntegrationException("Error calling integration request update_registration")
167-
}
153+
})
168154
}
169155

170156
override suspend fun getRegistration(): DeviceRegistration {
@@ -243,137 +229,38 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
243229

244230
override suspend fun updateLocation(updateLocation: UpdateLocation) {
245231
val updateLocationRequest = createUpdateLocation(updateLocation)
246-
247-
var causeException: Exception? = null
248-
for (it in server.connection.getApiUrls()) {
249-
var wasSuccess = false
250-
try {
251-
wasSuccess =
252-
integrationService.callWebhook(
253-
it.toHttpUrlOrNull()!!,
254-
updateLocationRequest,
255-
).isSuccessful
256-
} catch (e: Exception) {
257-
if (causeException == null) causeException = e
258-
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
259-
}
260-
// if we had a successful call we can return
261-
if (wasSuccess) {
262-
return
263-
}
264-
}
265-
266-
if (causeException != null) {
267-
throw IntegrationException(causeException)
268-
} else {
269-
throw IntegrationException("Error calling integration request update_location")
270-
}
232+
server.callWebhookOnUrls(updateLocationRequest)
271233
}
272234

273235
override suspend fun callAction(
274236
domain: String,
275237
action: String,
276238
actionData: Map<String, Any?>,
277239
) {
278-
var wasSuccess = false
279-
280-
val actionRequest =
281-
ActionRequest(
282-
domain,
283-
action,
284-
actionData,
285-
)
286-
287-
var causeException: Exception? = null
288-
for (it in server.connection.getApiUrls()) {
289-
try {
290-
wasSuccess =
291-
integrationService.callWebhook(
292-
it.toHttpUrlOrNull()!!,
293-
CallServiceIntegrationRequest(
294-
actionRequest,
295-
),
296-
).isSuccessful
297-
} catch (e: Exception) {
298-
if (causeException == null) causeException = e
299-
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
300-
}
301-
// if we had a successful call we can return
302-
if (wasSuccess) {
303-
return
304-
}
305-
}
306-
307-
if (causeException != null) {
308-
throw IntegrationException(causeException)
309-
} else {
310-
throw IntegrationException("Error calling integration request call_service")
311-
}
240+
server.callWebhookOnUrls(
241+
CallServiceIntegrationRequest(
242+
ActionRequest(
243+
domain,
244+
action,
245+
actionData,
246+
),
247+
),
248+
)
312249
}
313250

314251
override suspend fun scanTag(data: Map<String, String>) {
315-
var wasSuccess = false
316-
317-
var causeException: Exception? = null
318-
for (it in server.connection.getApiUrls()) {
319-
try {
320-
wasSuccess =
321-
integrationService.callWebhook(
322-
it.toHttpUrlOrNull()!!,
323-
ScanTagIntegrationRequest(
324-
data,
325-
),
326-
).isSuccessful
327-
} catch (e: Exception) {
328-
if (causeException == null) causeException = e
329-
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
330-
}
331-
// if we had a successful call we can return
332-
if (wasSuccess) {
333-
return
334-
}
335-
}
336-
337-
if (causeException != null) {
338-
throw IntegrationException(causeException)
339-
} else {
340-
throw IntegrationException("Error calling integration request scan_tag")
341-
}
252+
server.callWebhookOnUrls(ScanTagIntegrationRequest(data))
342253
}
343254

344255
override suspend fun fireEvent(eventType: String, eventData: Map<String, Any>) {
345-
var wasSuccess = false
346-
347-
val fireEventRequest = FireEventRequest(
348-
eventType,
349-
eventData.plus(Pair("device_id", deviceId)),
256+
server.callWebhookOnUrls(
257+
FireEventIntegrationRequest(
258+
FireEventRequest(
259+
eventType,
260+
eventData.plus(Pair("device_id", deviceId)),
261+
),
262+
),
350263
)
351-
352-
var causeException: Exception? = null
353-
for (it in server.connection.getApiUrls()) {
354-
try {
355-
wasSuccess =
356-
integrationService.callWebhook(
357-
it.toHttpUrlOrNull()!!,
358-
FireEventIntegrationRequest(
359-
fireEventRequest,
360-
),
361-
).isSuccessful
362-
} catch (e: Exception) {
363-
if (causeException == null) causeException = e
364-
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
365-
}
366-
// if we had a successful call we can return
367-
if (wasSuccess) {
368-
return
369-
}
370-
}
371-
372-
if (causeException != null) {
373-
throw IntegrationException(causeException)
374-
} else {
375-
throw IntegrationException("Error calling integration request fire_event")
376-
}
377264
}
378265

379266
override suspend fun getZones(): List<Entity> {
@@ -727,25 +614,9 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
727614

728615
val integrationRequest = RegisterSensorIntegrationRequest(registrationData)
729616

730-
var causeException: Exception? = null
731-
for (it in server.connection.getApiUrls()) {
732-
try {
733-
integrationService.callWebhook(it.toHttpUrlOrNull()!!, integrationRequest)
734-
.let {
735-
// If we created sensor or it already exists
736-
if (it.isSuccessful || it.code() == 409) {
737-
return
738-
}
739-
}
740-
} catch (e: Exception) {
741-
if (causeException == null) causeException = e
742-
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
743-
}
744-
}
745-
if (causeException != null) {
746-
throw IntegrationException(causeException)
747-
} else {
748-
throw IntegrationException("Error calling integration request register_sensor")
617+
server.callWebhookOnUrls(integrationRequest) { response ->
618+
// If we created sensor or it already exists
619+
response.isSuccessful || response.code() == 409
749620
}
750621
}
751622

@@ -797,6 +668,42 @@ class IntegrationRepositoryImpl @AssistedInject constructor(
797668
}
798669
}
799670

671+
private suspend fun Server.callWebhookOnUrls(
672+
request: IntegrationRequest,
673+
onSuccess: suspend (response: Response<ResponseBody>) -> Unit = {},
674+
isValidResponse: (response: Response<ResponseBody>) -> Boolean = { response -> response.isSuccessful },
675+
) {
676+
var firstCauseException: Exception? = null
677+
val httpURLs = connection.getApiUrls().mapNotNull { it.toHttpUrlOrNull() }
678+
679+
if (httpURLs.isEmpty()) {
680+
throw IntegrationException("No valid url can be found in server connection ${if (BuildConfig.DEBUG) connection.getApiUrls() else ""}")
681+
}
682+
683+
httpURLs.forEach { url ->
684+
try {
685+
val response = integrationService.callWebhook(url, request)
686+
if (isValidResponse(response)) {
687+
onSuccess(response)
688+
// If the response is successful we return and ignore potential firstCauseException
689+
return
690+
} else {
691+
throw IntegrationException(
692+
"Error calling webhook",
693+
response.code(),
694+
response.errorBody(),
695+
)
696+
}
697+
} catch (e: Exception) {
698+
if (firstCauseException == null) firstCauseException = e
699+
// Ignore failure until we are out of URLS to try, but use the first exception as cause exception
700+
}
701+
}
702+
703+
firstCauseException?.let { throw it }
704+
FailFast.fail { "Error calling webhook without cause." }
705+
}
706+
800707
private suspend fun createUpdateRegistrationRequest(deviceRegistration: DeviceRegistration): RegisterDeviceRequest {
801708
val oldDeviceRegistration = getRegistration()
802709
val pushToken = deviceRegistration.pushToken ?: oldDeviceRegistration.pushToken

common/src/main/kotlin/io/homeassistant/companion/android/common/sensors/SensorReceiverBase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ abstract class SensorReceiverBase : BroadcastReceiver() {
349349
val exceptionOk = e is IntegrationException &&
350350
(e.cause is IOException || e.cause is CancellationException)
351351
if (exceptionOk) {
352-
Timber.w("Exception while updating sensors: ${e::class.java.simpleName}: ${e.cause?.let { it::class.java.name } }")
352+
Timber.w(e, "Exception while updating sensors")
353353
} else {
354354
Timber.e(e, "Exception while updating sensors.")
355355
}

0 commit comments

Comments
 (0)