Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = "io.github.siddharthjaswal"
version = "0.9.9"
version = "0.9.10"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@

<change-notes><![CDATA[
<ul>
<li><b>Failed requests now show why</b> — instead of an opaque <code>"(failed)"</code>,
the Response and Overview cards display the captured exception (connection reset,
timeout, cleartext-not-permitted, …) so the failure is diagnosable.</li>
<li><b>Duplicate-call detection</b> — repeated identical requests fired in a quick
burst are flagged with a <code>DUP ×N</code> tag; overlapping non-idempotent
calls (double-submits) are highlighted in red, with a warning in the overview
Expand Down
Loading