From 7ec61e762d2192c545ccab1f26bae87dd1567b4e Mon Sep 17 00:00:00 2001 From: Sid Date: Thu, 11 Jun 2026 10:58:59 +0530 Subject: [PATCH] Show captured exception for failed requests instead of '(failed)' (#1) When a call throws before a response arrives, the plugin rendered an opaque "(failed)" in the Response card and surfaced nothing else. Now both the Response and Overview cards display the captured exception text, so the failure is diagnosable. Library: broaden the interceptor's catch from IOException to Throwable, so a downstream interceptor that throws a RuntimeException emits an error transaction (and rethrows unchanged) instead of leaving it stuck pending. Bumps plugin to 0.9.10; updates CHANGELOG and plugin.xml change-notes. Fixes #1 --- CHANGELOG.md | 13 +++++++++ build.gradle.kts | 2 +- .../logpose/LogPoseInterceptor.kt | 11 +++++--- .../logpose/ui/OverviewPanel.kt | 23 +++++++++++++++ .../logpose/ui/TransactionDetailView.kt | 28 ++++++++++++++----- src/main/resources/META-INF/plugin.xml | 3 ++ 6 files changed, 68 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1eb2c9..377e681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ format may still change. ## [Unreleased] +## [0.9.10] + +### Fixed +- Failed requests no longer render an opaque `"(failed)"` in the Response card. When a call + throws before a response arrives (connection reset, timeout, cleartext-not-permitted, or a + downstream interceptor that converts errors into exceptions), the Response card and the + Overview card now show the captured exception text, so the failure is actually diagnosable. + ([#1](https://github.com/siddharthjaswal/logpose/issues/1)) +- **Library:** the interceptor now captures *any* throwable from the chain, not just + `IOException`. A downstream interceptor throwing a `RuntimeException` previously left the + transaction stuck "pending" forever; it now emits an error transaction (and rethrows + unchanged). Ships via JitPack on the next tag. + ## [0.9.9] ### Added diff --git a/build.gradle.kts b/build.gradle.kts index c24d912..b7ae8b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "io.github.siddharthjaswal" -version = "0.9.9" +version = "0.9.10" repositories { mavenCentral() diff --git a/logpose-android/src/main/kotlin/io/github/siddharthjaswal/logpose/LogPoseInterceptor.kt b/logpose-android/src/main/kotlin/io/github/siddharthjaswal/logpose/LogPoseInterceptor.kt index 293d798..327d552 100644 --- a/logpose-android/src/main/kotlin/io/github/siddharthjaswal/logpose/LogPoseInterceptor.kt +++ b/logpose-android/src/main/kotlin/io/github/siddharthjaswal/logpose/LogPoseInterceptor.kt @@ -6,7 +6,6 @@ import io.github.siddharthjaswal.logpose.internal.BodyCapture import io.github.siddharthjaswal.logpose.wire.Transaction import okhttp3.Interceptor import okhttp3.Response -import java.io.IOException import java.util.UUID import io.github.siddharthjaswal.logpose.wire.Request as WireRequest import io.github.siddharthjaswal.logpose.wire.Response as WireResponse @@ -57,19 +56,23 @@ class LogPoseInterceptor @JvmOverloads constructor( emitter.emit(Transaction(id = id, startedAtMillis = startedAt, request = wireRequest)) } + // Catch *any* failure, not just IOException: a downstream interceptor (auth, error + // mapping) can throw a RuntimeException after OkHttp produced a response, and OkHttp's + // own connection failures surface as IOException. Either way we emit the error-shaped + // transaction (so it never stays stuck "pending") and rethrow unchanged. val response: Response = try { chain.proceed(request) - } catch (e: IOException) { + } catch (t: Throwable) { emitter.emit( Transaction( id = id, startedAtMillis = startedAt, request = wireRequest, - error = e.toString(), + error = t.toString(), durationMillis = elapsedMs(startNs), ) ) - throw e + throw t } emitter.emit( diff --git a/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/OverviewPanel.kt b/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/OverviewPanel.kt index 5e9b133..8429818 100644 --- a/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/OverviewPanel.kt +++ b/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/OverviewPanel.kt @@ -50,6 +50,14 @@ class OverviewPanel : CardPanel(null) { font = JBUI.Fonts.create("JetBrains Mono", 12) border = JBUI.Borders.empty(2, 0) } + // Shown only for failed calls (no response): the exception text from the interceptor. + private val errorText = JBTextArea(1, 10).apply { + isEditable = false; isOpaque = false; lineWrap = true; wrapStyleWord = false + foreground = Theme.danger + font = JBUI.Fonts.create("JetBrains Mono", 12) + border = JBUI.Borders.empty(2, 0) + } + private lateinit var errorRow: JPanel private val chips = JPanel().apply { isOpaque = false layout = BoxLayout(this, BoxLayout.X_AXIS) @@ -155,6 +163,8 @@ class OverviewPanel : CardPanel(null) { add(row(hbox(statusPill, Box.createHorizontalStrut(JBUI.scale(8)), methodPill), fill = false)) add(vGap(8)) add(row(url, fill = true)) + errorRow = row(errorText, fill = true) + add(errorRow) add(vGap(8)) add(row(chips, fill = false)) add(vGap(12)) @@ -166,6 +176,7 @@ class OverviewPanel : CardPanel(null) { fun show(tx: Transaction?, dup: DuplicateDetector.Mark? = null) { applyDuplicate(tx, dup) + applyError(tx) if (tx == null) { statusPill.set("—", Theme.textDim, Theme.bg2) methodPill.set("", Theme.textDim, null) @@ -223,6 +234,18 @@ class OverviewPanel : CardPanel(null) { statusPill.set("${spinnerChar(frame)} pending", Theme.accent, Theme.tint(Theme.accent, 30)) } + /** Shows the exception text for a failed call (no response), hidden otherwise. */ + private fun applyError(tx: Transaction?) { + if (!::errorRow.isInitialized) return + val err = tx?.error?.takeIf { tx.response == null } + if (err == null) { + errorRow.isVisible = false + return + } + errorText.text = err + errorRow.isVisible = true + } + /** Shows/hides the duplicate warning strip and styles it by severity. */ private fun applyDuplicate(tx: Transaction?, dup: DuplicateDetector.Mark?) { if (!::dupRow.isInitialized) return diff --git a/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/TransactionDetailView.kt b/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/TransactionDetailView.kt index 7b81d5c..3d7beda 100644 --- a/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/TransactionDetailView.kt +++ b/src/main/kotlin/io/github/siddharthjaswal/logpose/ui/TransactionDetailView.kt @@ -85,12 +85,21 @@ class TransactionDetailView(project: com.intellij.openapi.project.Project) : JPa } private fun renderResponse(tx: Transaction) { - if (tx.isPending()) { - response.setStatus("…") - response.showMessage("Waiting for response…") - } else { - response.setStatus(tx.response?.let { "${it.code} ${it.message}".trim() } ?: "—") - response.setElement(responseJson(tx)) + when { + tx.isPending() -> { + response.setStatus("…") + response.showMessage("Waiting for response…") + } + // The call never returned a response — OkHttp (or a downstream interceptor) threw. + // Show the captured exception instead of a bare "(failed)" so the failure is diagnosable. + tx.error != null -> { + response.setStatus("Failed") + response.setElement(responseJson(tx)) + } + else -> { + response.setStatus(tx.response?.let { "${it.code} ${it.message}".trim() } ?: "—") + response.setElement(responseJson(tx)) + } } } @@ -130,7 +139,12 @@ class TransactionDetailView(project: com.intellij.openapi.project.Project) : JPa } private fun responseJson(tx: Transaction): JsonElement { - val r = tx.response ?: return JsonPrimitive(if (tx.error != null) "(failed)" else "(pending)") + val r = tx.response ?: return buildJsonObject { + // No response object reached the interceptor. Surface the exception text (connection + // reset, timeout, cleartext-not-permitted, a downstream interceptor's throw, …) which + // the wire transaction carries in `error` — this is the most useful thing we can show. + if (tx.error != null) put("error", tx.error) else put("status", "(pending)") + } return buildJsonObject { put("code", r.code) if (r.message.isNotBlank()) put("message", r.message) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c41a66c..7378065 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -26,6 +26,9 @@ +
  • Failed requests now show why — instead of an opaque "(failed)", + the Response and Overview cards display the captured exception (connection reset, + timeout, cleartext-not-permitted, …) so the failure is diagnosable.
  • Duplicate-call detection — repeated identical requests fired in a quick burst are flagged with a DUP ×N tag; overlapping non-idempotent calls (double-submits) are highlighted in red, with a warning in the overview