Can’t you make the client of a testApplication fol...
# ktor
r
Can’t you make the client of a testApplication follow redirects?
a
It’s enabled by default. The following test passes:
Copy code
@Test
fun test() = testApplication {
    application {
        routing {
            get("/redirect") {
                call.respondRedirect("/")
            }

            get("/") {
                call.respondText { "hello" }
            }
        }

    }

    assertEquals("hello", client.get("/redirect").bodyAsText())
}
r
I did some more tests: • Your test passes in my environment • Your test, but modified to use Resources, also passes • But my actual test with the real application/endpoints doesn’t and I don’t know why
The endpoints:
Copy code
post<V1.OldTimeSlots> {
        call.respondRedirect(application.href(V1.EventTimeSlots()))
    }

    post<V1.EventTimeSlots> {
        …
        val response = EventTimeSlotsResponse(…)

        call.respond(HttpStatusCode.OK, response)
    }
2 identical tests, the first one calling the new endpoint, passes. The second one calling the old one, fails:
Copy code
@Test
    fun `EventTimeSlots endpoint should return event timeslots`() = testApplication {
        val client = createClient {
            install(ContentNegotiation) { json() }
            install(Resources)
        }

        val expectedResponse = EventTimeSlotsResponse(…)

        val response = <http://client.post|client.post>(V1.EventTimeSlots()) {
            contentType(ContentType.Application.Json)
            setBody(…)
        }.body<EventTimeSlotsResponse>()

        assertEquals(expectedResponse, response)
    }

    @Test
    fun `OldTimeslots endpoint should still work`() = testApplication {
        val client = createClient {
            install(ContentNegotiation) { json() }
            install(Resources)
        }

        val expectedResponse = EventTimeSlotsResponse(…)

        val response = <http://client.post|client.post>(V1.OldTimeSlots()) {
            contentType(ContentType.Application.Json)
            setBody(…)
        }.body<EventTimeSlotsResponse>()

        assertEquals(expectedResponse, response)
    }
The error:
Copy code
io.ktor.client.call.NoTransformationFoundException: No transformation found: class io.ktor.utils.io.ByteBufferChannel -> class blabla.route.EventTimeSlotsResponse
with response from <http://localhost/v1/timeslots>:
status: 302 Found
response headers: 
Location: /v1/event-timeslots
, Date: Wed, 11 May 2022 15:01:33 GMT
, Server: Ktor/2.0.1
, Content-Length: 0
a
Are you able to provide a sample project or describe specific conditions for reproduction? Otherwise it's difficult to say what went wrong.
r
I need to make a test that reproduces the issue, isolated from my actual app. For now I didn’t manage to reproduce it, as I said your test but with Resources still worked. I need to get closer to what I have
@Aleksei Tirman [JB] I finally have a reproducer. I think the issue comes from having a request body, it works without it.
Copy code
@Resource("/a")
    @Serializable
    class A {

        @Resource("b")
        @Serializable
        data class B(val parent: A = A())

        @Resource("c")
        @Serializable
        data class C(val parent: B = B())

    }

    @Serializable
    data class RequestBody(val value: Int)

    @Serializable
    data class ResponseBody(val value: Int)

    @Test
    fun `Should follow redirects`() = testApplication {

        val value = 1337

        environment {
            config = MapApplicationConfig()
            module {
                install(CallLogging)
                install(DefaultHeaders)
                install(AutoHeadResponse)
                install(ServerContentNegotiation) { json() }
                install(ServerResources)
                routing {
                    post<A.B> {
                        call.respondRedirect(application.href(A.C()))
                    }
                    post<A.C> {
                        val requestBody = call.receive<RequestBody>()
                        call.respond(HttpStatusCode.OK, ResponseBody(requestBody.value))
                    }
                }
            }
        }

        val client = createClient {
            install(ClientContentNegotiation) { json() }
            install(ClientResources)
        }

        val result = <http://client.post|client.post>(A.B()) {
            contentType(ContentType.Application.Json)
            setBody(RequestBody(value))
        }.body<ResponseBody>()

        assertEquals(value, result.value)

    }
a
The problem is that the
HttpRedirect
plugin redirects requests only with
GET
and
HEAD
methods by default. You can disable checking HTTP method to make it work:
Copy code
val client = createClient {
    followRedirects = false
    install(HttpRedirect) {
        checkHttpMethod = false
    }
    install(ClientContentNegotiation) { json() }
    install(ClientResources)
}
r
Oh. And why is that?
a
I don’t know. There is this condition in the code:
Copy code
if (plugin.checkHttpMethod && origin.request.method !in ALLOWED_FOR_REDIRECT) {
    return@intercept origin
}
Where
ALLOWED_FOR_REDIRECT
is
setOf(HttpMethod.Get, HttpMethod.Head)
r
Ok. Weird. I suppose we need to set
followRedirects
to false because it installs the
HttpRedirect
plugin with default config? It’s also weird to have to do that to modify the config
a
Yes
r
With the redirect configuration added, my test now passes. I also need to change my Ktor Client configuration everywhere as not redirecting depending on method is quite unexpected. Thanks!
a
I think it’s somehow related to the security.
r
Various RFCs changed what should be done when receiving a 301/302 regarding the method of the call, now it looks like 307/308 were added to remove any ambiguity
I read something about receiving a 301/302 when doing a PUT/POST to update/create a resource to redirect you to a page with the results, where you should do a GET to get the page
Yeah this method limitation was removed from the 2014 RFC https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.2
I think that the
checkHttpMethod
should apply only to old 301 and 302 codes and not 307 and 308. I’m also gonna use 308 in my case.
178 Views