Why a suspended call has no continuous stack
Kotlin compiles a suspend function into a state machine. The compiler rewrites the function so that it takes a Continuation argument, moves its local variables into a generated object, and returns either the result or a marker value that means the work has not finished. When the function suspends, it returns that marker and its frame leaves the stack. The caller resumes later through the continuation, and the stack at that moment begins in the dispatcher that ran the resume.
That fact decides how you read a trace. A request inside a coroutine has no single stack that runs from the button press to the socket write. The evidence is split across at least two stacks: the frame that started the call, and the frame inside the dispatcher or thread pool that completed it. Anything that assumes one continuous stack will lose the thread at the first suspension point.
The dispatch chain between a repository and the network
A common chain starts in a ViewModel, calls a suspend function on a repository, reaches a Retrofit interface method or a Ktor client call, and ends in an HTTP engine. Each suspend boundary adds a state machine and a possible thread change.
Retrofit suspend methods do not run the transfer on the calling thread. The generated code enqueues the call on the OkHttp dispatcher and returns the marker value, so the coroutine suspends and an OkHttp thread performs the I/O. When the response arrives, OkHttp invokes a callback that resumes the continuation, and the coroutine continues on whichever dispatcher its context names. A withContext block around the call forces a dispatch to the requested dispatcher before the call begins, which is why thread names in a log change more often than the call graph suggests.
Read the generated classes to see the chain. A suspend lambda becomes a class with an invokeSuspend method, a label field that holds the state, and a reference to the captured continuation. A suspend method on a Retrofit service appears in decompiled Java with an extra Continuation parameter and an Object return type. The path literal stays on the interface annotation, so the route remains readable even when the surrounding names are obfuscated.
Where the signing step sits
Request signing usually happens in an OkHttp interceptor added with addInterceptor. An interceptor of that kind runs on the thread that calls proceed, which is the thread that enqueued the call, so the signing code and the coroutine that triggered it share a stack up to that point. A breakpoint in the interceptor shows the request builder before the body is written.
The signing step cannot itself be a suspend function when it lives inside a synchronous interceptor, because the interceptor interface is not suspend. Apps handle that in three ways: they compute the value earlier and keep it in memory, they read it from a cache that an earlier suspend call filled, or they bridge with runBlocking or a blocking future. The bridge is the case worth finding, because it adds a nested event loop that hides the original coroutine from the stack. A store and read pattern places the signing work one call before the request, so the trace that matters is the call that refreshed the value.
Authentication interceptors, Ktor Auth plugins and Ktor DefaultRequest blocks occupy the same position between the assembled request and the socket. Whichever mechanism the app uses, the signature is computed while the request is still on the calling side of the transfer, and the input to it is the method, path, query, body and selected headers.
Following the flow when the stack is not continuous
Breakpoints do more here than a stack trace. Set them at the request execution entry point in the HTTP engine, at the continuation resume method, and at the dispatch method of the coroutine dispatcher. Hitting the dispatch breakpoint first shows which dispatcher took the work. Hitting the resume breakpoint afterwards shows the code that consumed the response.
- Log the thread name and the coroutine context at each layer, so the record survives after the stack has unwound.
- Search decompiled output for suspend methods by their extra Continuation parameter and Object return type, which is how they appear after compilation.
- Hook the interceptor chain and the request body write method to capture the exact bytes the engine sends.
- Take a thread dump while the request is in flight, because the OkHttp thread and the coroutine thread appear as separate entries that meet at the call object.
Combining those views removes the need for one continuous stack. The continuation object links them, and its identity connects the frame that suspended with the frame that resumed. Work that crosses into a native signing routine leaves the Kotlin layer entirely, and native Android reverse engineering covers that next step. Signing behaviour that lives in an interceptor is described under OkHttp interceptor reverse engineering.
Related work
Reviewed 28 September 2026 · SReverse research desk