Skip to content

PolygonRestClient.fetchResult closes HttpClient too early, causing JobCancellationException #300

@lakshya-linamani

Description

@lakshya-linamani

Hi 👋

We’ve run into a reproducible problem using PolygonRestClient with Ktor. The issue comes from the way the library currently manages its HttpClient.

Problem

In PolygonRestClient, the helper methods are implemented like this:

private inline fun <R> withHttpClient(codeBlock: (client: HttpClient) -> R) =
    httpClientProvider.buildClient().use(codeBlock)

internal suspend inline fun <reified T> fetchResult(
    urlBuilderBlock: URLBuilder.() -> Unit,
    vararg options: PolygonRestOption
): T {
    val url = baseUrlBuilder.apply(urlBuilderBlock).build()
    return withHttpClient { httpClient ->
        httpClient.get(url) {
            options.forEach { this.it() }
            headers["User-Agent"] = Version.userAgent
        }
    }.body()
}

This combination causes two problems:

  1. withHttpClient wraps the client in .use { … }, so the client is closed immediately after the lambda returns.
  2. .body() is called after the client has already been closed.

As a result, deserialization frequently fails with:

kotlinx.coroutines.JobCancellationException: Parent job is Completed; job=SupervisorJobImpl{Completed}@...

Why this happens

  • HttpClient.get() is suspendable. The request pipeline is not finished when the block returns.
  • Because .use { … } closes the client right away, the pipeline is cancelled.
  • Then .body() tries to deserialize from a closed client → cancellation exception.

Suggested Fix

  • Remove .use { … } and let the client live across calls.

    • HttpClient is designed to be a long-lived, reusable instance.
    • Close it once when the application shuts down, not per request.
  • Move .body() inside the withHttpClient block, so the response is fully materialized before the client is closed (if you insist on keeping .use).

For example:

private inline fun <R> withHttpClient(codeBlock: (client: HttpClient) -> R): R =
    codeBlock(httpClientProvider.buildClient())

internal suspend inline fun <reified T> fetchResult(
    urlBuilderBlock: URLBuilder.() -> Unit,
    vararg options: PolygonRestOption
): T {
    val url = baseUrlBuilder.apply(urlBuilderBlock).build()
    return withHttpClient { httpClient ->
        httpClient.get(url) {
            options.forEach { opt -> opt(this) }
            header(HttpHeaders.UserAgent, Version.userAgent)
        }.body()
    }
}

Impact

This makes all REST calls brittle — in a Ktor server context, almost every call to PolygonRestClient eventually fails with JobCancellationException.

Workarounds

The only workaround today is to bypass PolygonRestClient.fetchResult entirely and re-implement REST calls with your own HttpClient. But this defeats the purpose of using the official SDK.


Environment:

  • Ktor 2.3.x
  • Kotlin 1.9/2.0
  • polygon-io/client-jvm (latest release)
  • JDK 17

Would you be open to a PR to fix this? I can draft one if you agree with the proposed change.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions