Думаю многие разработчики сталкивались с ситуацией, когда какой то из эндпоинтов сервера не готов, но его нужно добавить в приложение. Особенно часто такое бывает с негативными кейсами вроде всевозможных ошибок, которые нужно обрабатывать в приложении, и которые часто делаются в последнюю очередь.
Способов решения в данном случае много, можно например подставлять данные на разных этапах их обработки или использовать OkHttp MockWebServer
https://github.com/square/okhttp/tree/master/mockwebserver
или подобные решения.
Подумав над возможными способами, я сделал interceptor, чтобы менять ответ сервера на самом раннем этапе из возможных. А нем используется map, в которой по ключу в виде HttpUrl лежит упрощенная модель ответа сервера, далее из chain вытаскивается url, и если он есть в map, вытаскивается модель ответа и подставляются данные из нее. Я не стал добавлять headers, но это можно очень просто сделать, как и добавить сколько угодно ответов по разным url. Теоретически можно отслеживать не только url, но и какие то другие параметры. Вот что в итоге получилось:
data class MockResponse(
val isEnabled: Boolean,
val body: String,
val message: String,
val serverCode: Int,
val contentType: String = "application/json"
)const val DEBUG_MOCK_INTERCEPTOR_ENABLED = false
val mockResponses = mutableMapOf<String, MockResponse>().apply {
put(
key = "https://n",
value = MockResponse(
isEnabled = false,
body = "",
message = "",
serverCode = 200,
)
)import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class MockInterceptor : Interceptor {
companion object {
private const val TAG = "MockInterceptorTag"
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val mockResponse = mockResponses[request.url.toString()]
return if (BuildConfig.DEBUG &&
DEBUG_MOCK_INTERCEPTOR_ENABLED &&
mockResponse != null &&
mockResponse.isEnabled
) {
logDebug("Intercepted request: ${request.url}", TAG)
Response.Builder()
.code(mockResponse.serverCode)
.message(mockResponse.message)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(
mockResponse.body
.toByteArray()
.toResponseBody(mockResponse.contentType.toMediaTypeOrNull())
)
.addHeader("Content-type", "application/json")
.build()
} else {
chain.proceed(request)
}
}
}single<OkHttpClient>(named(OKHTTP)) {
logDebug("create OkHttpClient)", TAG)
OkHttpClient.Builder().apply {
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {
// NO IMPLEMENTATION
}
@Throws(CertificateException::class)
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {
// NO IMPLEMENTATION
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
)
val protocolSSL = "SSL"
val sslContext = SSLContext.getInstance(protocolSSL).apply {
init(null, trustAllCerts, SecureRandom())
}
sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
followRedirects(true)
followSslRedirects(true)
addInterceptor(get<ContentTypeInterceptor>())
addInterceptor(get<HeaderInterceptor>())
addInterceptor(get<HttpLoggingInterceptor>(named(LOGGING_INTERCEPTOR)))
addInterceptor(get<HttpLoggingInterceptor>(named(LOG_TO_FILE_INTERCEPTOR)))
if (BuildConfig.DEBUG && DEBUG_MOCK_INTERCEPTOR_ENABLED) {
logDebug("add MockInterceptor", TAG)
addInterceptor(get<MockInterceptor>())
}
connectTimeout(TIMEOUT, TimeUnit.SECONDS)
writeTimeout(TIMEOUT, TimeUnit.SECONDS)
readTimeout(TIMEOUT, TimeUnit.SECONDS)
}.build()
}также приведу полезные интерсепторы и конверторы
import okhttp3.Interceptor
import okhttp3.Response
class ContentTypeInterceptor : Interceptor {
companion object {
private const val TAG = "ContentTypeInterceptorTag"
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
val currentContentType = originalResponse.header("Content-Type")
val source = originalResponse.body?.source()
source?.request(Long.MAX_VALUE)
val buffer = source?.buffer
val responseBodyString = buffer?.clone()?.readString(Charsets.UTF_8)
val newContentType = when {
responseBodyString?.trim()?.startsWith("{") == true -> "application/json"
responseBodyString?.trim()?.startsWith("<") == true -> "application/xml"
else -> null
}
return if (
newContentType != null && (currentContentType == null || !currentContentType.contains(
newContentType,
ignoreCase = true
))
) {
logDebug("Content type replace with $newContentType", TAG)
originalResponse.newBuilder()
.header("Content-Type", newContentType)
.build()
} else {
originalResponse
}
}
}import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class HeaderInterceptor : okhttp3.Interceptor,
KoinComponent {
companion object {
private const val TAG = "HeaderInterceptorTag"
}
private val preferences: PreferencesRepository by inject()
override fun intercept(chain: okhttp3.Interceptor.Chain): okhttp3.Response {
logDebug("intercept", TAG)
val original = chain.request()
val request = original.newBuilder()
.header("Content-Type", "application/json")
.header("Cookie", "KEYCLOAK_LOCALE=bg")
.method(original.method, original.body)
val token = preferences.readApplicationInfo()?.accessToken
if (!token.isNullOrEmpty()) {
logDebug("add token: $token", TAG)
request.header("Authorization", "Bearer $token")
}
return chain.proceed(request.build())
}
}import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
import java.lang.reflect.Type
class NullOrEmptyConverterFactory : Converter.Factory(), KoinComponent {
companion object {
private const val TAG = "NullOrEmptyConverterFactoryTag"
}
private val gsonConverterFactory: GsonConverterFactory by inject()
private val simpleXmlConverterFactory: SimpleXmlConverterFactory by inject()
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
return Converter<ResponseBody, Any?> { body ->
if (body.contentLength() != 0L) {
val contentType = body.contentType()
val source = body.source()
source.request(Long.MAX_VALUE)
val bufferClone = source.buffer.clone()
val subtype = contentType?.subtype
val clonedBody =
bufferClone.clone().asResponseBody(contentType, bufferClone.size)
try {
when {
subtype?.contains("xml", ignoreCase = true) == true -> {
val converter = simpleXmlConverterFactory.responseBodyConverter(
type,
annotations,
retrofit
)
converter?.convert(body)
}
subtype?.contains("json", ignoreCase = true) == true -> {
val converter = gsonConverterFactory.responseBodyConverter(
type,
annotations,
retrofit
)
converter?.convert(body)
}
else -> {
body.string()
}
}
} catch (e: Exception) {
logError("Content type not valid, Exception: ${e.message}", e, TAG)
clonedBody.string()
}
} else null
}
}
}import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import okhttp3.ResponseBody
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
// тут смысл в том, что если приходит List, он преобразуется в класс
class ArrayConverterFactory : Converter.Factory(), KoinComponent {
companion object {
private const val TAG = "ArrayConverterFactoryTag"
}
val gson: Gson by inject()
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
return Converter<ResponseBody, Any?> { body ->
when {
type is Class<*> && type == SomeResponse::class.java -> {
SomeResponse(
data = generateList(
body = body,
type = object : TypeToken<String>() {}.type,
)
)
}
else -> {
retrofit.nextResponseBodyConverter<Any?>(
this@ArrayConverterFactory,
type,
annotations
).convert(body)
}
}
}
}
private fun <T> generateList(body: ResponseBody, type: Type): List<T> {
val listType = TypeToken.getParameterized(List::class.java, type).type
return gson.fromJson(body.charStream(), listType)
}
}import okhttp3.Interceptor
import okhttp3.Response
class TempHeaderSavingInterceptor : Interceptor {
companion object {
private const val MOCK_STEP_HEADER_FLOW = "X-Flow"
private const val MOCK_STEP_HEADER_STEP = "X-State"
private var lastRequestHeaderFlow: String? = null
private var lastRequestHeaderStep: String? = null
}
override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().newBuilder()
lastRequestHeaderFlow?.let {
builder.header(MOCK_STEP_HEADER_FLOW, it)
}
lastRequestHeaderStep?.let {
builder.header(MOCK_STEP_HEADER_STEP, it)
}
val req = builder.build()
return chain.proceed(req).also { response ->
response.headers[MOCK_STEP_HEADER_FLOW]?.let {
lastRequestHeaderFlow = it
}
response.headers[MOCK_STEP_HEADER_STEP]?.let {
lastRequestHeaderStep = it
}
}
}
}