jooby icon indicating copy to clipboard operation
jooby copied to clipboard

Router with inner incoder/decoder not accepting content type

Open zzj37 opened this issue 2 years ago • 4 comments

For example:

data class SomeBean (
    val name: String
)

class RouterWithDecoder : Kooby({
    install(JacksonModule(jacksonObjectMapper()))

    get("/api/beans") {
        emptyList<SomeBean>()
    }

})

fun main(args: Array<String>) {
    runApp(args) {
        mount(RouterWithDecoder())
    }
}

Getting /api/beans with header Accept: application/json will return 406 Not Acceptable with

{
    "message": "application/json",
    "statusCode": 406,
    "reason": "Not Acceptable"
}

If I replace mount(RouterWithDecoder()) with install(::RouterWithDecoder), the server WILL accept application/json.

However, if install(JacksonModule(jacksonObjectMapper())) is replaced by a customized decoder in RouterWithDecoder, eg.

class RouterWithDecoder : Kooby({
    //   install(JacksonModule(jacksonObjectMapper()))
    val jacksonMapper = jacksonObjectMapper()
    decoder(MediaType.json) { ctx, type ->
        jacksonMapper.readValue(ctx.body.value(), TypeFactory.rawClass(type))
    }

    get("/api/beans") {
        emptyList<SomeBean>()
    }
})

then mount the router by install(::RouterWithDecoder) statement will fail to accept application/json again.


After further tests, I find:

  • Installing JacksonModule globally before mounting routers by mount() method will accept application/json.
  • Adding a customized decoder globally before mounting routers, either by mount() or by install(), will NOT accept application/json

zzj37 avatar Nov 06 '23 03:11 zzj37

Your 2nd second example should work. The way it works is like Parent/Child or MainRoute/SubRoute. So we can define all the routes/API/etc in children while main assemble everything and provide/setup all other services (like jackson here)

jknack avatar Nov 06 '23 14:11 jknack

Maybe the hierarchical structure need some specifications of some sort of "context" or "scoping"? I find each SubRoute implemented by inheriting Kooby class and mounted by mount() method needs to set its own worker otherwise its coroutine routes do not work.

zzj37 avatar Nov 07 '23 01:11 zzj37

yea, doc isn't never good enough.

mount: https://jooby.io/#router-composing-mount

The mount operator only import routes. Services, callbacks, etc…​ are not imported. Main application is responsible for assembly all the resources and services required by imported applications.

install: https://jooby.io/#router-composing-install

This operator lets you for example to deploy Foo as a standalone application or integrate it into a main one called App. The install operator shares the state of the main application, so lazy initialization (and therefore instantiation) of any child applications is mandatory.

I find each SubRoute implemented by inheriting Kooby class and mounted by mount() method needs to set its own worker otherwise its coroutine routes do not work.

This sound like a bug. Can you file it and provide an example?

Thanks

jknack avatar Nov 07 '23 14:11 jknack

It only happens when calling coroutine within the SubRouter.

class RouterWithoutWorker : Kooby({
    coroutine {
        get("/without-worker") { "Without worker!" }
    }
})

class RouterWithoutWorkerNoCoroutine : Kooby({
    get("/without-worker-no-coroutine") { "Without worker, no coroutine!" }
})

class RouterWithWorker(worker: Executor) : Kooby({
    this.worker = worker
    coroutine {
        get("/with-worker") { "With worker!" }
    }
})

fun main(args: Array<String>) {
    runApp(args) {
        mount(RouterWithWorker(this.worker))
        mount(RouterWithoutWorker())
        coroutine {
            mount(RouterWithoutWorkerNoCoroutine())
        }
    }
}

Both RouterWithWorker and RouterWithoutWorkerNoCoroutine will work, but RouterWithoutWorker will fail with:

java.lang.IllegalStateException: Worker executor not ready
	at io.jooby.internal.ForwardingExecutor.execute(ForwardingExecutor.java:18)
	at kotlinx.coroutines.ExecutorCoroutineDispatcherImpl.dispatch(Executors.kt:128)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:322)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:25)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:110)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at io.jooby.kt.CoroutineRouter.launch$jooby_kotlin(CoroutineRouter.kt:100)
        ...

I am not sure if calling coroutine within a SubRoute which would later bemount()ed is in accordance to the design goal.

zzj37 avatar Nov 08 '23 03:11 zzj37