View test reports →

Coverage Summary for Class: WiretapKtorSsePluginKt (dev.skymansandy.wiretap.plugin.sse)

Class Method, % Branch, % Line, % Instruction, %
WiretapKtorSsePluginKt 100% (5/5) 100% (5/5) 100% (12/12)
WiretapKtorSsePluginKt$WiretapKtorSsePlugin$1 100% (3/3) 100% (9/9) 100% (57/57)
WiretapKtorSsePluginKt$WiretapKtorSsePlugin$1$install$1 100% (1/1) 100% (2/2) 100% (24/24)
WiretapKtorSsePluginKt$WiretapKtorSsePlugin$1$install$2 100% (1/1) 66.7% (4/6) 100% (18/18) 98.2% (164/167)
Total 100% (10/10) 66.7% (4/6) 100% (34/34) 98.8% (257/260)


 /*
  * Copyright (c) 2026 skymansandy. All rights reserved.
  */
 
 package dev.skymansandy.wiretap.plugin.sse
 
 import dev.skymansandy.wiretap.domain.model.SseConnection
 import dev.skymansandy.wiretap.domain.model.SseStatus
 import dev.skymansandy.wiretap.domain.model.config.sse.WiretapSseConfig
 import dev.skymansandy.wiretap.helper.markers.ExperimentalWiretapSseApi
 import dev.skymansandy.wiretap.helper.util.currentTimeMillis
 import dev.skymansandy.wiretap.plugin.sse.util.SsePluginDeps
 import io.ktor.client.HttpClient
 import io.ktor.client.plugins.HttpClientPlugin
 import io.ktor.client.plugins.sse.SSESession
 import io.ktor.client.request.HttpRequestPipeline
 import io.ktor.client.statement.HttpResponseContainer
 import io.ktor.client.statement.HttpResponsePipeline
 import io.ktor.util.AttributeKey
 
 internal val WiretapSseEnabledKey = AttributeKey<Boolean>("WiretapSseEnabled")
 
 /**
  * Configuration holder for the installed plugin.
  *
  * The class is public because [HttpClientPlugin] surfaces it as a type
  * parameter, but the constructor and [enabled] flag are internal —
  * consumers configure the plugin via the [WiretapSseConfig] DSL, not by
  * reading state off the handler.
  */
 @ExperimentalWiretapSseApi
 class WiretapSsePluginHandler internal constructor(
     internal val enabled: Boolean,
     internal val consoleLogging: Boolean,
 )
 
 /**
  * Ktor client plugin that automatically intercepts SSE sessions
  * to log connections and events via Wiretap.
  *
  * All SSE sessions are wrapped transparently at the [SSESession]
  * level — no manual [wiretapped] call is needed. The plugin intercepts
  * at [HttpResponsePipeline.Parse] (before Ktor's SSE plugin wraps the
  * session into [io.ktor.client.plugins.sse.ClientSSESession] at Transform),
  * ensuring all event I/O is logged transparently.
  *
  * **Ktor-version coupling:** this implementation depends on Ktor internals
  * that [createClientPlugin] does not expose — specifically the response
  * pipeline ([HttpResponsePipeline.Parse]), the [HttpClientPlugin]
  * interface, and the [io.ktor.utils.io.InternalAPI]-marked
  * [SSESession.bodyBuffer]. If Ktor renames these phases, restructures the
  * SSE plugin's transform, or changes the [SSESession] surface, this
  * plugin will need to be revisited.
  *
  * Usage:
  * ```kotlin
  * HttpClient {
  *     install(SSE)
  *     install(WiretapKtorSsePlugin) {
  *         enabled = true
  *     }
  * }
  * ```
  */
 @ExperimentalWiretapSseApi
 val WiretapKtorSsePlugin =
     object : HttpClientPlugin<WiretapSseConfig, WiretapSsePluginHandler> {
 
         override val key = AttributeKey<WiretapSsePluginHandler>("WiretapSsePlugin")
 
         override fun prepare(block: WiretapSseConfig.() -> Unit): WiretapSsePluginHandler {
             val config = WiretapSseConfig().apply(block)
             return WiretapSsePluginHandler(
                 enabled = config.enabled,
                 consoleLogging = config.consoleLogging,
             )
         }
 
         override fun install(plugin: WiretapSsePluginHandler, scope: HttpClient) {
             val deps = SsePluginDeps(plugin.consoleLogging)
 
             // Store enabled flag in request attributes
             scope.requestPipeline.intercept(HttpRequestPipeline.State) {
                 context.attributes.put(WiretapSseEnabledKey, plugin.enabled)
                 proceed()
             }
 
             // Wrap the raw SSESession BEFORE Ktor's SSE plugin processes it
             // at Transform phase. This ensures all event I/O is logged
             // transparently, without requiring an explicit wiretapped() call.
             scope.responsePipeline.intercept(HttpResponsePipeline.Parse) {
                 val (info, body) = subject
                 val sseSession = body as? SSESession ?: return@intercept
                 if (!plugin.enabled) return@intercept
 
                 val url = context.request.url.toString()
                 val requestHeaders = context.request.headers.entries()
                     .associate { (key, values) -> key to values.joinToString(", ") }
 
                 val connectionId = deps.sseLogManager.createConnection(
                     SseConnection(
                         url = url,
                         requestHeaders = requestHeaders,
                         status = SseStatus.Open,
                         timestamp = currentTimeMillis(),
                     ),
                 )
 
                 val wrappedSession = LoggingSseSession(
                     delegate = sseSession,
                     connectionId = connectionId,
                     url = url,
                     sseLogManager = deps.sseLogManager,
                 )
                 proceedWith(HttpResponseContainer(info, wrappedSession))
             }
         }
     }