Marco Garofalo
09/30/2026, 8:41 AMdave
09/30/2026, 9:28 AMs4nchez
09/30/2026, 10:28 AMRoutingHttpHandler vs HttpHandler
For instance:
import io.ktor.http.HttpHeaders
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.filter.OpenTelemetryTracing
import org.http4k.filter.ServerFilters
import org.http4k.routing.RequestWithContext
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
fun main() {
val serverAsRouting: RoutingHttpHandler = routes("/foo/{*}" bind { _: Request -> Response(OK) });
val serverAsHandler: HttpHandler = serverAsRouting
val clientForRouting = ServerFilters.OpenTelemetryTracing(spanCompletionMutator = { _, r, _ -> println((r as? RequestWithContext)?.xUriTemplate)}).then(serverAsRouting)
val clientForHandler = ServerFilters.OpenTelemetryTracing(spanCompletionMutator = { _, r, _ -> println((r as? RequestWithContext)?.xUriTemplate)}).then(serverAsHandler)
println("--- Routing ---")
println(clientForRouting(Request(Method.GET, "/foo/bar")).bodyString())
println("--- Handler ---")
println(clientForHandler(Request(Method.GET, "/foo/bar")).bodyString())
}
will produce:
--- Routing ---
foo/{*}
--- Handler ---
null
The routing info is guaranteed to be there if you're running the server independently, but it should still be possible to test this particular behaviour in-memoryMarco Garofalo
09/30/2026, 10:29 AMJames Richardson
09/30/2026, 9:35 PMJames Richardson
09/30/2026, 9:37 PMMarco Garofalo
10/01/2026, 8:35 AMJames Richardson
10/01/2026, 9:34 PMMarco Garofalo
10/02/2026, 9:21 AMdave
10/02/2026, 9:28 AMMarco Garofalo
10/02/2026, 9:37 AMMarco Garofalo
10/02/2026, 10:43 AMJames Richardson
10/02/2026, 10:59 AMuritemplate =If request is RoutedMessage and request.uritemplate != null {
request.uritemplate.toString
} else { <http://request.uri.path.tr|request.uri.path.tr>imstart('/') }
Then when making outgoing requests, you can use a uritemplate too
Val template = Request(GET, "/thing/{thing}")
Val thingpath = Path.of("thing")
client(template).with(thingpath of "secret")
It wont show up in the outgoing or incoming eventsJames Richardson
10/02/2026, 10:59 AMJames Richardson
10/02/2026, 11:00 AMMarco Garofalo
10/02/2026, 11:16 AMJames Richardson
10/02/2026, 11:23 AMs4nchez
10/03/2026, 8:28 PMChaoticHttpHandler is a simple HttpHandler , so if you replace the FakeUpstream class with the code below you'll get the same result as the embedded server:
fun FakeUpstream(): RoutingHttpHandler = routes(
"/details/{pii}" bind GET to { Response(Status.OK) }
)Marco Garofalo
10/04/2026, 1:33 PMMarco Garofalo
10/04/2026, 2:02 PMHttpTransaction.xUriTemplate , I tried adding a ClientFilters.CleanProxy() before forwarding to the functional upstream and it did solve my issue.
The doc for that filter says
This Filter is used to clean the Request and Response when proxying directly to another system. The purpose
of this is to remove any routing metadata that we may have attached to it before sending it onwards.
That would mean that the outermost handler we would inject in our app while testing would look something like:
externalSystems = ClientFilters.CleanProxy().then(reverseProxy(
"upstream1-host" to upstream1,
// ...
"upstreamN-host" to upstreamN
))
Are there any major drawbacks for the above?Marco Garofalo
10/04/2026, 2:52 PMs4nchez
10/04/2026, 5:20 PMAre there any major drawbacks for the above?Not that I can think of. Using the
CleanProxy ensures RequestWithContext is not propagated which is what you need in this case.