Search code examples
kotlinkotlin-multiplatformktor

Handling 307 Temporary Redirect in Ktor HttpClient on iOS with Kotlin Multiplatform


I'm developing a Kotlin Multiplatform application and using Ktor HttpClient to make network requests. While the API calls work as expected on Android, handling 307 Temporary Redirect responses on iOS is causing issues. The API call succeeds on Android (with automatic redirection from 307 to 200), but on iOS, it fails with a ClientRequestException and a 401 Unauthorized status.

Here is the relevant part of my API class in the common code:

class LuluApiImpl(engine: HttpClientEngine) {
    private val client = HttpClient(engine) {
        expectSuccess = true
        followRedirects = true

        install(ContentNegotiation) {
            json(
                Json {
                    isLenient = true
                    ignoreUnknownKeys = true
                    prettyPrint = true
                },
            )
        }

        Logging {
            logger = object : Logger {
                override fun log(message: String) {
                    log.v { message }
                }
            }
            level = LogLevel.ALL
        }

        install(HttpTimeout) {
            val timeout = 60000L
            connectTimeoutMillis = timeout
            requestTimeoutMillis = timeout
            socketTimeoutMillis = timeout
        }
    }
}

This is where I'm making the request:

    override suspend fun getProfileData(): ApiResponse<Profile> {
        val response = client.get {
            call("(ommited)/profile")
        }.body<ApiResponse<Profile>>()
        return response
    }

    private fun HttpRequestBuilder.call(path: String) {
        url {
            takeFrom("(ommited)")
            encodedPath += path
        }
        headers {
            credentialsStorageManager.currentAccessToken?.let { bearerAuth(it) }
        }
    }

I'm injecting the Darwin engine using Koin like so

single { Darwin.create() }

On iOS, when the server responds with a 307 Temporary Redirect, the Ktor client throws a ClientRequestException with a 401 error instead of following the redirect. I've noticed the 307 redirect in ProxyMan, but it seems like the redirect is not being handled correctly by the Ktor client on iOS.

I've enabled followRedirects in the client configuration, but it doesn't seem to affect the behavior on iOS.

How can I properly handle 307 redirects in Ktor on iOS, ensuring that the client follows the redirect and completes the request successfully?

Edit: I tried adding ResponseObserver to log the response I'm getting, Same results, in Android I get 307 followed by 200. on iOS I only get 401.


Solution

  • Posting my solution here in case anyone else faces the same issue.

    I initialized my client as such:

    val client: HttpClient
    get() = HttpClient(Darwin.create {
        val delegate = KtorNSURLSessionDelegate()
        val customDelegate = CustomDelegate(delegate)
        val sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration
        val session = NSURLSession.sessionWithConfiguration(sessionConfiguration, customDelegate, null)
        usePreconfiguredSession(session = session, delegate = delegate)
    })
    

    In this setup, usePreconfiguredSession is used to provide a custom delegate to NSURLSession.

    Custom Delegate Implementation:

    class CustomDelegate(private val ktorNSURLSessionDelegate: KtorNSURLSessionDelegate) : NSObject(),
    NSURLSessionDataDelegateProtocol {
    
    override fun URLSession(
        session: NSURLSession,
        task: NSURLSessionTask,
        didCompleteWithError: NSError?
    ) {
        ktorNSURLSessionDelegate.URLSession(session, task, didCompleteWithError)
    }
    
    override fun URLSession(
        session: NSURLSession,
        dataTask: NSURLSessionDataTask,
        didReceiveData: NSData
    ) {
        ktorNSURLSessionDelegate.URLSession(session, dataTask, didReceiveData)
    }
    
    override fun URLSession(
        session: NSURLSession,
        task: NSURLSessionTask,
        willPerformHTTPRedirection: NSHTTPURLResponse,
        newRequest: NSURLRequest,
        completionHandler: (NSURLRequest?) -> Unit
    ) {
        if (newRequest.URL == null) {
            completionHandler(null)
            return
        }
        val nextRequest = newRequest.URL?.let { NSMutableURLRequest(it) }
        listOf("Authorization", "X-Platform", "X-Version", "X-Locale").forEach { header ->
            task.originalRequest?.valueForHTTPHeaderField(header)?.let {
                nextRequest?.addValue(it, forHTTPHeaderField = header)
            }
        }
    
        nextRequest?.HTTPBody = task.originalRequest?.HTTPBody
        completionHandler(nextRequest)
    }
    

    This CustomDelegate class ensures that all headers from the original request are appended to the new request after a redirection, addressing the issue mentioned in this Stack Overflow question.