Этап 1: единый OkHttpClient ч.2 — 401 централизован через interceptor
CI / verify (push) Has been cancelled
CI / release (push) Has been cancelled

- UnauthorizedInterceptor (401→UnauthorizedException) в NetworkFactory,
  флаг throwOnUnauthorized (по умолчанию true).
- Удалены 75 ручных проверок if(code==401) throw в 16 репозиториях.
- AuthVerifier (login-верификация) исключён: throwOnUnauthorized=false —
  401 = 'неверный пароль' со своим сообщением, не session-expired.
- Побочно: 401 теперь ловится единообразно ВЕЗДЕ (включая пути, что раньше
  забывали проверку).
- +3 MockWebServer-теста (401 бросает; 200/403 проходят) — всего 18 тестов.

Проверено: 18 тестов зелёные + assembleRelease BUILD SUCCESSFUL, APK 53 МБ.
This commit is contained in:
b-dev-mobile
2026-07-07 23:29:49 +00:00
parent c45ebd8d49
commit ab8fa70e51
22 changed files with 73 additions and 87 deletions
@@ -189,7 +189,6 @@ object CalDavClient {
.put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("CalDAV create event HTTP ${response.code}")
}
@@ -244,7 +243,6 @@ object CalDavClient {
.delete()
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 204 && response.code != 404) {
error("CalDAV delete event HTTP ${response.code}")
}
@@ -278,7 +276,6 @@ object CalDavClient {
builder.header("If-None-Match", "*")
}
client.newCall(builder.build()).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("CalDAV put event HTTP ${response.code}")
}
@@ -314,7 +311,6 @@ object CalDavClient {
.method("MKCALENDAR", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("CalDAV create calendar HTTP ${response.code}")
}
@@ -347,7 +343,6 @@ object CalDavClient {
.method("MKCOL", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("CalDAV subscribe calendar HTTP ${response.code}")
}
@@ -405,7 +400,6 @@ object CalDavClient {
.header("Destination", destinationHref)
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("CalDAV move HTTP ${response.code}")
}
@@ -644,7 +638,6 @@ object CalDavClient {
.put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("CalDAV create task HTTP ${response.code}")
}
@@ -691,7 +684,6 @@ object CalDavClient {
.put(ics.toRequestBody("text/calendar; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 204) {
error("CalDAV update task HTTP ${response.code}")
}
@@ -705,7 +697,6 @@ object CalDavClient {
.delete()
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 204 && response.code != 404) {
error("CalDAV delete task HTTP ${response.code}")
}
@@ -765,7 +756,6 @@ object CalDavClient {
private fun execute(client: OkHttpClient, req: Request): String {
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
val code = response.code
if (code !in 200..299 && code != 207) {
error("CalDAV error HTTP $code")
@@ -90,7 +90,6 @@ object CardDavClient {
.put(vcard.toRequestBody("text/vcard; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("Не удалось создать контакт (HTTP ${response.code})")
}
@@ -163,7 +162,6 @@ object CardDavClient {
.method("PROPFIND", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
.build()
client.newCall(req).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
val code = response.code
if (code !in 200..299 && code != 207) {
error("CardDAV error HTTP $code")
@@ -27,9 +27,6 @@ object DavClient {
.method("MKCOL", null)
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) {
throw UnauthorizedException()
}
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("DAV MKCOL HTTP ${response.code}")
}
@@ -46,9 +43,6 @@ object DavClient {
.put(body)
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) {
throw UnauthorizedException()
}
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("DAV upload HTTP ${response.code}")
}
@@ -76,9 +70,6 @@ object DavClient {
.method("PROPFIND", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
.build()
return client.newCall(request).execute().use { response ->
if (response.code == 401) {
throw UnauthorizedException()
}
val code = response.code
if (code !in 200..299 && code != 207) {
error("DAV error HTTP $code")
@@ -182,7 +173,6 @@ object DavClient {
.delete()
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 204) {
error("DAV DELETE HTTP ${response.code}")
}
@@ -197,7 +187,6 @@ object DavClient {
.header("Overwrite", "T")
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 201 && response.code != 204) {
error("DAV MOVE HTTP ${response.code}")
}
@@ -221,7 +210,6 @@ object DavClient {
.method("PROPPATCH", body.toRequestBody("application/xml; charset=utf-8".toMediaType()))
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (response.code !in 200..299 && response.code != 207) {
error("DAV PROPPATCH HTTP ${response.code}")
}
@@ -14,9 +14,10 @@ import java.util.concurrent.TimeUnit
* - авторизованные варианты — через `base.newBuilder()` (шарят пул/диспатчер/кэш);
* - экземпляры кэшируются по кредам+таймаутам → переиспользуются, соединения живут.
*
* Публичный API НЕ менялся — все вызывающие (`newAuthedClient*`) работают как прежде.
* NB: 401→UnauthorizedException пока проверяется в репозиториях вручную; централизация
* через interceptor — отдельный шаг (меняет семантику исключений).
* Публичный API совместим — вызывающие (`newAuthedClient*`) работают как прежде.
* 401→UnauthorizedException централизован через interceptor (`throwOnUnauthorized`, по умолч.
* true) — ручные проверки в репозиториях убраны. Login-пути (AuthVerifier), где 401 = «неверный
* пароль» со своим сообщением, передают `throwOnUnauthorized = false`.
*/
object NetworkFactory {
@Volatile
@@ -38,8 +39,9 @@ object NetworkFactory {
trustAllCerts: Boolean = false,
callTimeoutSeconds: Long = 30,
readTimeoutSeconds: Long = 30,
throwOnUnauthorized: Boolean = true,
): OkHttpClient {
val key = "$username|$appPassword|$trustAllCerts|$callTimeoutSeconds|$readTimeoutSeconds"
val key = "$username|$appPassword|$trustAllCerts|$callTimeoutSeconds|$readTimeoutSeconds|$throwOnUnauthorized"
return clients.getOrPut(key) {
base.newBuilder() // общий пул/диспатчер/кэш базового клиента
.callTimeout(callTimeoutSeconds, TimeUnit.SECONDS)
@@ -47,6 +49,7 @@ object NetworkFactory {
.readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
.applyUnsafeSslIfNeeded(trustAllCerts)
.addInterceptor(BasicAuthInterceptor(username, appPassword))
.apply { if (throwOnUnauthorized) addInterceptor(UnauthorizedInterceptor) }
.build()
}
}
@@ -30,7 +30,6 @@ class NotificationsRepository {
.header("Accept", "application/json")
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (!response.isSuccessful || response.body == null) {
error("Уведомления HTTP ${response.code}")
}
@@ -86,7 +85,6 @@ class NotificationsRepository {
.header("Accept", "application/json")
.build()
client.newCall(request).execute().use { response ->
if (response.code == 401) throw UnauthorizedException()
if (!response.isSuccessful) {
error("Уведомления HTTP ${response.code}")
}
@@ -0,0 +1,22 @@
package ru.forbion.f7cloud.core.network
import okhttp3.Interceptor
import okhttp3.Response
/**
* Единая точка обработки 401 для авторизованных запросов: бросает [UnauthorizedException]
* (session expired → logout). Заменяет ~70 ручных `if (code == 401) throw ...` по репозиториям.
*
* Подключается в [NetworkFactory] при `throwOnUnauthorized = true` (по умолчанию). Login-пути
* (проверка пароля при входе) НЕ используют этот interceptor — там 401 = «неверный пароль».
*/
object UnauthorizedInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == 401) {
response.close() // не течём телом — дальше по цепочке оно не читается
throw UnauthorizedException()
}
return response
}
}