Skip to content

Android: fix HttpURLConnection leak in TalkModeVoiceResolver#43780

Merged
obviyus merged 3 commits intoopenclaw:mainfrom
Kaneki-x:fix/android-voice-resolver-conn-leak
Mar 13, 2026
Merged

Android: fix HttpURLConnection leak in TalkModeVoiceResolver#43780
obviyus merged 3 commits intoopenclaw:mainfrom
Kaneki-x:fix/android-voice-resolver-conn-leak

Conversation

@Kaneki-x
Copy link
Copy Markdown
Contributor

Summary

TalkModeVoiceResolver.listVoices() opens an HttpURLConnection to the ElevenLabs API but never calls conn.disconnect(), and the response InputStream is never closed. Each call leaks a connection, which accumulates over repeated voice list fetches and can exhaust the connection pool.

Fix

  • Wrap the connection usage in try/finally { conn.disconnect() } to ensure the connection is released on all paths (success, HTTP error, parse error, exception).
  • Use stream.use { it.readBytes() } to close the InputStream after reading.

Test plan

  • Code review: verified conn.disconnect() runs on all exit paths (normal return, HTTP 4xx/5xx throw, JSON parse exception).
  • Verified stream.use {} closes both inputStream and errorStream paths.
  • Manual test: call voice list multiple times and confirm no connection leak in network profiler.

This PR was authored with AI assistance.

@steipete @obviyus Would appreciate a review when you have a chance.

Made with Cursor

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR fixes a real HttpURLConnection leak in TalkModeVoiceResolver.listVoices() by wrapping connection usage in try/finally { conn.disconnect() } and closing the response stream with stream.use { it.readBytes() }. The structural intent is correct, but there are two issues worth addressing before merging.

  • NPE risk on null errorStream: HttpURLConnection.getErrorStream() can return null when an error response has no body. The current code assigns this to stream and then calls stream.use { it.readBytes() } — since it is a platform type that resolves to null, readBytes() will throw NullPointerException. Fix: stream?.use { it.readBytes() } ?: byteArrayOf().
  • conn.disconnect() defeats connection pooling: On Android 4.4+, HttpURLConnection is backed by OkHttp. Closing the stream via stream.use {} already returns the connection to the pool. The unconditional conn.disconnect() in finally then tears it down, negating keep-alive benefits for repeated listVoices() calls.

Confidence Score: 3/5

  • Not safe to merge as-is — the null errorStream path introduces a NPE that can crash the app on any 4xx/5xx response with no body.
  • The fix addresses the original leak correctly, but introduces a crash path via null errorStream that needs a one-line fix before this is production-safe. The disconnect() concern is a performance issue, not a crash risk.
  • apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeVoiceResolver.kt — lines 89-90 (null errorStream NPE) and lines 104-106 (conn.disconnect() vs. connection reuse)
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeVoiceResolver.kt
Line: 89-90

Comment:
**Null `errorStream` causes NPE on error responses with no body**

`HttpURLConnection.getErrorStream()` returns `null` when the server returns a 4xx/5xx response with no body (per Android docs). On that path `stream` becomes `null`, and `stream.use { it.readBytes() }` will pass `null` as `it` and crash with a `NullPointerException` when `readBytes()` is called on it.

Use the safe-call operator and fall back to an empty array so the subsequent error message path still works cleanly:

```suggestion
        val stream = if (code >= 400) conn.errorStream else conn.inputStream
        val data = stream?.use { it.readBytes() } ?: byteArrayOf()
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeVoiceResolver.kt
Line: 104-106

Comment:
**`conn.disconnect()` defeats HTTP keep-alive / connection reuse**

On Android 4.4+ `HttpURLConnection` is backed by OkHttp. Simply closing the response stream (already done by `stream.use {}` above) returns the underlying TCP connection to the pool for keep-alive reuse. Calling `conn.disconnect()` unconditionally in `finally` then immediately tears that connection down, which negates the pooling benefit and adds latency for every subsequent call to `listVoices()`.

The `finally` block is still useful for the case where an exception is thrown before `stream` is ever opened (e.g. a connection timeout), but in the happy path it is counterproductive. Consider disconnecting only on error, or removing it entirely and relying on stream-close for cleanup:

```kotlin
} catch (e: Exception) {
    conn.disconnect()
    throw e
}
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 63e946e

Comment on lines +89 to +90
val stream = if (code >= 400) conn.errorStream else conn.inputStream
val data = stream.use { it.readBytes() }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null errorStream causes NPE on error responses with no body

HttpURLConnection.getErrorStream() returns null when the server returns a 4xx/5xx response with no body (per Android docs). On that path stream becomes null, and stream.use { it.readBytes() } will pass null as it and crash with a NullPointerException when readBytes() is called on it.

Use the safe-call operator and fall back to an empty array so the subsequent error message path still works cleanly:

Suggested change
val stream = if (code >= 400) conn.errorStream else conn.inputStream
val data = stream.use { it.readBytes() }
val stream = if (code >= 400) conn.errorStream else conn.inputStream
val data = stream?.use { it.readBytes() } ?: byteArrayOf()
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeVoiceResolver.kt
Line: 89-90

Comment:
**Null `errorStream` causes NPE on error responses with no body**

`HttpURLConnection.getErrorStream()` returns `null` when the server returns a 4xx/5xx response with no body (per Android docs). On that path `stream` becomes `null`, and `stream.use { it.readBytes() }` will pass `null` as `it` and crash with a `NullPointerException` when `readBytes()` is called on it.

Use the safe-call operator and fall back to an empty array so the subsequent error message path still works cleanly:

```suggestion
        val stream = if (code >= 400) conn.errorStream else conn.inputStream
        val data = stream?.use { it.readBytes() } ?: byteArrayOf()
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +104 to 106
} finally {
conn.disconnect()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conn.disconnect() defeats HTTP keep-alive / connection reuse

On Android 4.4+ HttpURLConnection is backed by OkHttp. Simply closing the response stream (already done by stream.use {} above) returns the underlying TCP connection to the pool for keep-alive reuse. Calling conn.disconnect() unconditionally in finally then immediately tears that connection down, which negates the pooling benefit and adds latency for every subsequent call to listVoices().

The finally block is still useful for the case where an exception is thrown before stream is ever opened (e.g. a connection timeout), but in the happy path it is counterproductive. Consider disconnecting only on error, or removing it entirely and relying on stream-close for cleanup:

} catch (e: Exception) {
    conn.disconnect()
    throw e
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeVoiceResolver.kt
Line: 104-106

Comment:
**`conn.disconnect()` defeats HTTP keep-alive / connection reuse**

On Android 4.4+ `HttpURLConnection` is backed by OkHttp. Simply closing the response stream (already done by `stream.use {}` above) returns the underlying TCP connection to the pool for keep-alive reuse. Calling `conn.disconnect()` unconditionally in `finally` then immediately tears that connection down, which negates the pooling benefit and adds latency for every subsequent call to `listVoices()`.

The `finally` block is still useful for the case where an exception is thrown before `stream` is ever opened (e.g. a connection timeout), but in the happy path it is counterproductive. Consider disconnecting only on error, or removing it entirely and relying on stream-close for cleanup:

```kotlin
} catch (e: Exception) {
    conn.disconnect()
    throw e
}
```

How can I resolve this? If you propose a fix, please make it concise.

@Kaneki-x
Copy link
Copy Markdown
Contributor Author

Kaneki-x commented Mar 12, 2026

@greptile-apps Addressed both review comments:

  1. Null errorStream NPE: Changed to stream?.use { it.readBytes() } ?: byteArrayOf() so a null errorStream falls back to an empty byte array instead of crashing.

  2. HTTP keep-alive preservation: Replaced finally { conn.disconnect() } with catch (e: Exception) { conn.disconnect(); throw e } so disconnect() only runs on error. In the happy path, stream.use {} closes the stream and returns the TCP connection to the pool for keep-alive reuse.

@Kaneki-x
Copy link
Copy Markdown
Contributor Author

@obviyus @steipete Review feedback has been addressed. Could you take a look when you get a chance? Thanks!

@obviyus obviyus self-assigned this Mar 13, 2026
@obviyus obviyus force-pushed the fix/android-voice-resolver-conn-leak branch from ef7f7d2 to ef341de Compare March 13, 2026 09:28
@obviyus obviyus merged commit 84428bb into openclaw:main Mar 13, 2026
6 checks passed
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Mar 13, 2026

Landed on main.

Thanks @Kaneki-x.

mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 13, 2026
* main: (168 commits)
  fix: stabilize macos daemon onboarding
  fix(ui): keep shared auth on insecure control-ui connects (openclaw#45088)
  docs(plugins): clarify workspace shadowing
  fix(node-host): harden perl approval binding
  fix(node-host): harden pnpm approval binding
  fix(discovery): add missing domain to wideArea Zod config schema (openclaw#35615)
  chore(gitignore): add docker-compose override (openclaw#42879)
  feat(ios): add onboarding welcome pager (openclaw#45054)
  fix(signal): add groups config to Signal channel schema (openclaw#27199)
  fix: restore web fetch firecrawl config in runtime zod schema (openclaw#42583)
  fix: polish Android QR scanner onboarding (openclaw#45021)
  fix(android): use Google Code Scanner for onboarding QR
  fix(config): add missing params field to agents.list[] validation schema (openclaw#41171)
  docs(contributing): update Android app ownership
  fix(agents): rephrase session reset prompt to avoid Azure content filter (openclaw#43403)
  test(config): cover requiresOpenAiAnthropicToolPayload in compat schema fixture
  fix(agents): respect explicit user compat overrides for non-native openai-completions (openclaw#44432)
  Android: fix HttpURLConnection leak in TalkModeVoiceResolver (openclaw#43780)
  Docker: add OPENCLAW_TZ timezone support (openclaw#34119)
  fix(agents): avoid injecting memory file twice on case-insensitive mounts (openclaw#26054)
  ...
hougangdev pushed a commit to hougangdev/clawdbot that referenced this pull request Mar 14, 2026
…w#43780)

* Android: fix HttpURLConnection leak in TalkModeVoiceResolver.listVoices

* fix null errorStream NPE and preserve HTTP keep-alive

* fix: restore voice resolver disconnect cleanup

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
ecochran76 pushed a commit to ecochran76/openclaw that referenced this pull request Mar 14, 2026
…w#43780)

* Android: fix HttpURLConnection leak in TalkModeVoiceResolver.listVoices

* fix null errorStream NPE and preserve HTTP keep-alive

* fix: restore voice resolver disconnect cleanup

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
Interstellar-code pushed a commit to Interstellar-code/operator1 that referenced this pull request Mar 16, 2026
…w#43780)

* Android: fix HttpURLConnection leak in TalkModeVoiceResolver.listVoices

* fix null errorStream NPE and preserve HTTP keep-alive

* fix: restore voice resolver disconnect cleanup

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
(cherry picked from commit 84428bb)
sbezludny pushed a commit to sbezludny/openclaw that referenced this pull request Mar 27, 2026
…w#43780)

* Android: fix HttpURLConnection leak in TalkModeVoiceResolver.listVoices

* fix null errorStream NPE and preserve HTTP keep-alive

* fix: restore voice resolver disconnect cleanup

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 27, 2026
…w#43780)

* Android: fix HttpURLConnection leak in TalkModeVoiceResolver.listVoices

* fix null errorStream NPE and preserve HTTP keep-alive

* fix: restore voice resolver disconnect cleanup

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
(cherry picked from commit 84428bb)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 27, 2026
…w#43780)

* Android: fix HttpURLConnection leak in TalkModeVoiceResolver.listVoices

* fix null errorStream NPE and preserve HTTP keep-alive

* fix: restore voice resolver disconnect cleanup

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
(cherry picked from commit 84428bb)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants