ktorio / ktorio/ktor

3.4.3 regression: embedded Netty handler issuing httpClient.get() that 302s back to localhost no longer delivers original response to browser

Open
#5,589 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Kotlin
Stars
14.5k
Forks
1.3k
Avg merge
2d 13h
Merged PRs (30d)
49

Description

## Summary

After upgrading from Ktor 3.4.2 to 3.4.3 (no other changes), an OAuth callback flow on JVM stops working: the original browser request to a local embedded Netty server never receives its response body, even though the handler completes successfully. Pinning back to 3.4.2 (server + client) fully restores the behavior; nothing else changes.

## Setup (real-world OAuth-on-localhost pattern)

1. Embedded Ktor Netty server on `localhost:8081` with one route `GET /oauth-callback`.
2. The OS browser is opened (`Desktop.browse(authUrl)`) to a remote OAuth provider.
3. Provider redirects the **browser** to `http://localhost:8081/oauth-callback?code=...&state=...`.
4. The route handler then issues `httpClient.get(\"$BFF/auth/callback\") { parameter(\"code\", code); parameter(\"state\", state) }` against a remote backend.
5. The remote backend responds **302** with `Location: http://localhost:8081/oauth-callback?code=...&state=...&session=...` and `Set-Cookie: ...`.
6. Default `HttpClient` (`followRedirects = true`) follows the 302 — back into the same embedded server.
7. Handler then calls `call.respondHtml(...)` and resumes the suspending coroutine waiting on the outer `suspendCancellableCoroutine`.

On 3.4.2: browser receives the success HTML, tab finishes loading. On 3.4.3: the browser tab hangs forever on the URL it was navigating to *before* hitting `localhost:8081` (in our case the upstream OAuth provider's page); meanwhile the handler **does** complete and the coroutine resumes successfully (we can see the resulting session token consumed by the rest of the app within ~1 second of the user's account-picker click). So state-wise the handler ran end-to-end — the regression is purely that the response body never reaches the browser tab that started the navigation.

## Bisect

Single-commit bisect in our repo: `compose 1.10.3 → 1.11.0-beta03`, `kotlin 2.3.20 → 2.3.21`, and `ktor 3.4.2 → 3.4.3` were all bumped together. Re-pinning **only** ktor + ktor-server to 3.4.2 (leaving Compose/Kotlin at the new versions) fixed it. Pinning only one of `ktor` or `ktor-server` was not tested individually yet.

## Workarounds

- Pin both `ktor` and `ktor-server` to 3.4.2.
- Or: switch the in-handler `HttpClient` to one with `followRedirects = false` and parse the 302 manually. We tried this on 3.4.3 and it did **not** fix the symptom on its own — which suggests the regression is on the **server** side, not in the client redirect plugin. (The recursive client→server reentrancy is a real code smell, but it isn't what's broken in 3.4.3.)

## Environment

- Ktor: 3.4.3 (broken) vs 3.4.2 (working)
- Engine: `ktor-server-netty`, default `ktor-client` engine on JVM
- JDK: ships with Compose Multiplatform 1.11.0-beta03 desktop runtime
- Platform: Windows 11
- Browsers tested: multiple (same symptom)

## Notes

- I don't yet have a minimal Ktor-only standalone reproducer; happy to put one together if the symptom isn't immediately recognizable from the description. The pattern of \"handler reentrantly hit by its own httpClient call\" is unusual but legal and was working in 3.4.2.
- Repo where this surfaced: a Kotlin Multiplatform desktop app's OAuth-on-localhost flow. Relevant handler is at `OAuth2Handler.jvm.kt` in our codebase, structurally:

```kotlin
embeddedServer(Netty, port = 8081) {
routing {
get(\"/oauth-callback\") {
try {
val response = httpClient.get(\"\$BFF/auth/callback\") {
parameter(\"code\", code); parameter(\"state\", state)
}
if (response.status == HttpStatusCode.Found) {
val token = response.headers[\"Set-Cookie\"]?.let { /* parse */ }
call.respondHtml(HttpStatusCode.OK) { renderSuccessPage() }
continuation.resume(token!!)
}
} finally {
server?.stop(1000, 2000)
}
}
}
}.start(wait = false)
```

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the embedded ktor-server-netty flow with the `/oauth-callback` route and an in-handler `httpClient.get()` that follows a 302 back to localhost, comparing Ktor 3.4.2 with 3.4.3. Use `OAuth2Handler.jvm.kt` as the reported integration shape and trace why `call.respondHtml(...)` does not reach the original browser request. Done means a regression test captures the response on 3.4.3 while preserving the successful handler completion.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.