Stuck with Api response Ktor - ios

I am trying to build a KMM application using Ktor for our ApiServices. I have created a BaseApiClass where I have all of the api related code.
Code for BaseApiClass :-
class BaseAPIClass {
//Create Http Client
private val httpClient by lazy {
HttpClient {
defaultRequest {
host = ApiEndPoints.Base.url
contentType(ContentType.Application.Json)
header(CONNECTION, CLOSE)
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
install(HttpTimeout) {
requestTimeoutMillis = NETWORK_REQUEST_TIMEOUT
}
expectSuccess = false
// JSON Deserializer
install(JsonFeature) {
val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
}
serializer = KotlinxSerializer(json)
}
}
}
// Api Calling Functions I have few more similar to this but issue is random and comes in any of the api
#Throws(Exception::class)
suspend fun sampleApi(requestBody: RequestBody?) : Either<CustomException, BaseResponse<EmptyResponseModel>> {
return try {
val response = httpClient.post<BaseResponse<EmptyResponseModel>> {
url(ApiEndPoints.sample.url)
if (requestBody != null) {
body = requestBody
}
}
Success(response)
}
catch (e: Exception) {
Failure(e as CustomException)
}
}
Here's how I call the api from iOS app :-
val apiClass = BaseApiClass()
func callApi() {
apiClass.sampleApi(requestBody: .init(string: "value here")) { (result, error) in
result?.fold(failed: { (error) -> Any? in
// Error here
}, succeeded: { (result) -> Any? in
// Success here
})
}
}
Now here if I try to call similar few more api's with the same object i.e apiClass then after few calls it get stuck inside my function callApi it don't send even api request (Because I can't see Request Logs printed in my console) and because of that I cannot do any other operations as I don't get anything from api.
As soon as I change my screen or close the app and try to call the same api then it works good.
But instead of creating a object only at one time like this apiClass = BaseApiClass() if I try to do with BaseApiClass().sampleApi(request params here) {// completion handler here} it works fine I don't get any issues with this.
I am not sure what causes this to happen everything works good in Android this is faced only with iOS.

Try to set LogLevel.NONE in the install(Logging) block.
At the moment I resolved in this way because it seems a bug of Ktor.
See: https://youtrack.jetbrains.com/issue/KTOR-2711
It should be fixed in the version 1.6.0.

Are you using the multithreaded variant of the Coroutines library? The official docs state that you should use this variant when working with Ktor. See here

After all the efforts and trying a lot of debugging skills I got to understand that my completion handler in the shared module is never called even if I receive the response the response from api.
The only solution I have achieved is creating the different HTTP Client using expect and actual mechanism. By making separate clients I have not encountered the issue yet.
If you have any other answers or solutions I would be happy to have a look at it.

Related

InvalidMutabilityException: mutation attempt of frozen kotlin.native.internal Exception

I'm very new to the Kotlin Multiplatform and Swift language, I have a problem with KMM only the iOS part, I have successfully run this on Android but it fails on IOS due to concurrency issues.
Kotlin code snippet :
#Throws(Exception::class)
suspend fun getResponse(data: String): String {
var response: String
client.responsePipeline.intercept(HttpResponsePipeline.Transform) { (_, body) ->
when (context.response.status) {
HttpStatusCode.OK -> response = body as String
}
}
response = client.post(BASE_URL) {
contentType(ContentType.Application.Json)
body = data
}
return response
}
iOS code snippet :
#State var response: String = ""
Button("Click") {
Repository().getResponse(data: "hello world") { data, error in
if data != nil {
response.self = "\(data)"
}
}
}
I get HttpClient: {"output":"...","statusCode":200 } from the Api which I want but it fails anyways.
I tried wrap the post request with CoroutineScope(Dispatchers.Main){
withContext(Dispatchers.Default){}}
But no luck, any idea why?
Assuming you're on recent Kotlin and library versions, you should enable the new memory model. Put this in gradle.properties
kotlin.native.binary.memoryModel=experimental
See KaMP Kit for an example.
Migrating to Ktor 2.0.0 solved many problems for me.

Kotlin Multiplaform iOS Exceptions showing in Xcode, not in Firebase

We have noticed in our project, we are getting allot of exceptions logged in Xcode (->Organiser->Crashes) logged in production...
However these Share crashes don't give any extra debug infomation, nor are they logged in our Firebase exceptions.
This is how network API calls are being made using ktor 1.6:
#Throws(CancellationException::class, ResponseException::class, Exception::class)
suspend fun getProfileGoals(profileId : Int) : ProfileGoal{
val url = "$base/api/goals/$profileId"
var client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
useAlternativeNames = false
coerceInputValues = true
encodeDefaults = true
})
}
return client.get(url){
headers {
header(DEVICE_ID, deviceId)
}
}
}
And this is how it is used in iOS:
func getGoals(){
profilesApi?.getProfileGoals(profileId: Int32(selfProfile!.profileId), completionHandler: { profileGoal, error in
if let profileGoal = profileGoal {
self.profileGoals = profileGoal
....
}
}
self.collectionView.reloadData()
})
}
There are obviously lots of other API calls, but they are all done in a similar way.
I guess my question is, is anything obviously wrong or done the incorrect way or how to get these exceptions appearing in Firebase Crashlytics, which may hopefully have more info for debugging

Moya - unable to call apis with authentication credentials

I am developing an iOS app using django rest framework for apis. But currently I am not be able to getting ahead when calling apis with authentication credentials.
I succeeded in calling the api using Postman and curl by setting Header as Authentication Bearer <token>.. but I continuously failed at calling it from iOS app. I am using Moya for calling api. And I don't know what I should do next.
What I tried: (when calling Moya)
let token = "abcde12345sometoken"
let plugin = AccessTokenPlugin(tokenClosure: token)
let provider = MoyaProvider<AccountAPI>(plugins : [plugin])
provider.request(.getAccountProfile(oauth_id: oauth_id, provider: "facebook")) { (result) in
// doing something with result
}
and configured API as:
extension AccountAPI : TargetType, AccessTokenAuthorizable {
// codes conforming variables to TargetType protocol
public var authorizationType: AuthorizationType {
switch self {
case .getFacebookAccountToken:
return .none
default:
return .bearer
}
}
public var headers: [String: String]? {
switch self {
case .getFacebookAccountToken, .getEmailAccountToken: // post requests
return ["Content-type":"application/x-www-form-urlencoded"]
default:
return ["Content-type":"application/json"]
}
}
}
Is there anything I should consider when using Moya for authentication or maybe with Info.plist and so on?
Or the document says this approach is for JWT token, and maybe my method is not for JWT and something else..? Give me some advice!
For my case, I use
Moya 12.0.1
MultiTarget
example:
plugins = [AccessTokenPlugin(tokenClosure: {
let token = ...
return token
})]
MoyaProvider<MultiTarget>(
plugins: plugins
)
.request(MultiTarget(myAPI)) {
...
}
But it never calls tokenClosure
Solution
you need to add this extension
extension MultiTarget: AccessTokenAuthorizable {
public var authorizationType: AuthorizationType {
guard let target = target as? AccessTokenAuthorizable else { return .none }
return target.authorizationType
}
}
source: https://github.com/Moya/Moya/blob/master/Sources/Moya/Plugins/AccessTokenPlugin.swift#L62
After a few hours of trying this and that.. I found out that it was the api endpoint redirects itself based on the content-language.. so the header that I set is dead when being redirected. So either setting the i18n url in advance or setting the content-language header would solve my problem.

How to cancel multiple networking requests using Moya

I am currently using Moya to structure my networking calls. Per their docs, I have configured it as the following:
enum SomeAPIService {
case endPoint1(with: Object)
case endPoint2(duration: Int)
}
When calling an endpoint (in this case, endPoint1), I do the following:
let provider = MoyaProvider<SomeAPIService>()
provider.request(.endPoint1(object: object)) { (result) in
switch result {
case let .success(moyaResponse):
finished(Result.success(value: moyaResponse.value))
case let .failure(error):
let backendError = BackendError(localizedTitle: "", localizedDescription: "Some error", code: moyaResponse.statusCode)
finished(Result.failure(error: backendError))
}
})
My goal is, upon the user performing an action, cancel all the networking requests that's happening.
Accordingly, Moya does allow one to cancel requests from the discussion here. From the most upvoted comment, someone mentioned let request_1 = MoyaRequestXXXXX and then ruest_1.cancel()
My problem is:
How would I keep pointer to the requests?
provider doesn't have a cancel() function - so how should I be calling it?
Any help is much appreciated.
Edit:
Per the helpful suggestion about using [Cancellable], I did the following:
(1) In my app's singleton instance called Operator, I added var requests = [Cancellable]()
(2) Every API call is added to the requests array as a Cancellable, like so:
let provider = MoyaProvider<SomeAPIService>()
Operator.shared.requests.append(provider as! Cancellable) //causing error
provider.request(.endPoint1(object: object)) { (result) in
//rest of the block omitted
I think I am not getting the syntax correct, and am adding the provider and not the request. However, since the request is itself a block, where would be the place to add the request?
The request method returns a Cancellable. From the documentation we can read:
The request() method returns a Cancellable, which has only one public function, cancel(), which you can use to cancel the request.
So according to this, I made a simple test and call:
var requests: [Cancellable] = []
#objc func doRequests() {
for i in 1...20 {
let request = provider.request(MyApi.someMethod) {
result in
print(result)
}
requests.append(request)
}
requests.forEach { cancellable in cancellable.cancel() } // here I go through the array and cancell each request.
requests.removeAll()
}
I set up a proxy using Charles and it seems to be working as expected. No request was sent - each request was cancelled.
So, the answer to your questions is:
You can keep it in [Cancellable] array.
Go through the array and cancel each request that you want to cancel.
EDIT
The main problem is that you adding the provider to the array and you try to map provider as Cancellable, so that cause the error.
You should add reqest to the array. Below you can see the implementation.
let provider = MoyaProvider<SomeAPIService>()
let request = provider.request(.endPoint1(object: object)) { // block body }
Operator.shared.requests.append(request)
//Then you can cancell your all requests.
I would just cancel the current provider session + tasks:
provider.manager.session.invalidateAndCancel()

Parse PFCloud.callInBackground completion block never called i.e no response received

I have defined a function in the Parse Cloud Code called "relatedWords". When I try call this function in my iOS app, the completion block/closure is never called i.e no response is received.
I've tested the function in the Parse API Console and it is working fine there, so I know it's not an issue with the cloud code.
Any ideas on what the issue is?
My swift code:
func fetchRelatedKeyWordsForWord(word: String)
{
PFCloud.callFunctionInBackground("relatedWords", withParameters: ["hashtag": word]) { (response, error) -> Void in
//This is never called
print(response)
print(error)
}
}
Snippet of the cloud code:
Parse.Cloud.define("relatedWords", function(request, response) {
Parse.Cloud.useMasterKey();
var hashtag = request.params.hashtag;
...
...
//Run a query
var query = new Parse.Query(parseClassName);
query.find({
success: function(results) {
if (results.length != 0) {
console.log("Found Objects! Returning Objects");
response.success(results);
return;
}
Edit:
I figured out the problem. It was silly mistake by me. The reason the cloud code was not getting called is that I had not setup parse in my ApplicationDidFinishLaunching i.e I did not call Parse.setApplicationId("...", clientKey: "...")
I figured out the problem. It was silly mistake by me. The reason the cloud code was not getting called is that I had not setup parse in my ApplicationDidFinishLaunching i.e I did not call Parse.setApplicationId("...", clientKey: "...")
I figured out the problem.
you can use other server, other vise pay money on parse and solve the problem.

Resources