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