[http-client] Add client filter scopes#1662
Open
DamianReeves wants to merge 3 commits into
Open
Conversation
getkyo#1658 replaced ScalaTest with kyo-test. Resolve HttpWebSocketTest.scala by moving the PR's two new filter/query-string websocket tests from the old `in runNotNative {` form to the kyo-test `.notNative in {` form, matching how main migrated the adjacent connectTimeout test.
fwbrasil
reviewed
Jun 7, 2026
| val transportStream = new ConnectionBackedStream(connection) | ||
| val path = url.rawQuery match | ||
| case Present(q) => s"${url.path}?$q" | ||
| case Absent => url.path |
Collaborator
There was a problem hiding this comment.
let's please avoid URL manipulation outside of HttpUrl
fwbrasil
reviewed
Jun 7, 2026
| (filteredReq: HttpRequest[Any]) => | ||
| val filteredPath = filteredReq.url.rawQuery match | ||
| case Present(q) => s"${filteredReq.url.path}?$q" | ||
| case Absent => filteredReq.url.path |
fwbrasil
reviewed
Jun 7, 2026
| HttpResponse(HttpStatus.SwitchingProtocols).addField("body", result) | ||
| } | ||
| }.asInstanceOf[HttpResponse["body" ~ A] < (Async & Abort[HttpException | HttpResponse.Halt])] | ||
| ).map(_.fields.body).asInstanceOf[A < (S & Async & Abort[HttpException])] |
Collaborator
There was a problem hiding this comment.
why are these casts necessary? are they safe?
fwbrasil
reviewed
Jun 7, 2026
| val conn = new ChannelBackedStream(streamCtx.inbound, streamCtx.outbound) | ||
| val path = request.queryRawString match | ||
| case Present(query) => s"${request.pathAsString}?$query" | ||
| case Absent => request.pathAsString |
fwbrasil
reviewed
Jun 7, 2026
|
|
||
| private val local: Local[(HttpClient, HttpClientConfig)] = Local.init((defaultClient, HttpClientConfig())) | ||
| private val local: Local[(HttpClient, HttpClientConfig, HttpFilter.Passthrough[Nothing])] = | ||
| Local.init((defaultClient, HttpClientConfig(), HttpFilter.noop)) |
Collaborator
There was a problem hiding this comment.
could we move the filter to HttpClientConfig?
fwbrasil
reviewed
Jun 7, 2026
|
|
||
| /** Applies multiple client filters for all `HttpClient` calls within the given computation. */ | ||
| def withFilters[A, S](filters: Seq[HttpFilter.Passthrough[Nothing]])(v: A < S)(using Frame): A < S = | ||
| withFilter(filters.foldLeft(HttpFilter.noop: HttpFilter.Passthrough[Nothing])(_.andThen(_)))(v) |
Collaborator
There was a problem hiding this comment.
Reasoning about the filters running in a computation becomes more difficult with the new features. Can we have methods to inspect the current enabled filters + to disable them in nested scopes?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds programmatic and scoped client filter configuration for
kyo-http, building on the existingHttpFiltermodel and the existingHttpFilter.FactorySPI.The goal is to make cross-cutting client concerns first-class without requiring every typed route or convenience call to repeat the same filter setup. Supported scenarios include auth headers, request IDs, logging, tracing, metrics, and other request/response middleware that should apply consistently across outgoing HTTP requests.
Closes #1660
What changed
clientFiltertoHttpClientConfig, defaulting toHttpFilter.noop.HttpClientConfig.filter(...)andHttpClientConfig.filters(...)builder methods that append filters in order.HttpClient.withFilter(...)andHttpClient.withFilters(...)for dynamic, scoped filter configuration.HttpFilter.FactoryServiceLoader filtersHttpClientConfigfiltersHttpClient.withFilterfiltersHost,Upgrade,Connection,Sec-WebSocket-Version, andSec-WebSocket-Key.kyo-httpREADME with the new client filter configuration options and composition order.Why
kyo-httpalready has a solid filter abstraction and an SPI path throughHttpFilter.Factory, but the client side had two important gaps:This PR keeps the existing route-level filter support, keeps SPI support for library-provided filters such as tracing, and adds config plus scoped layers for application-controlled middleware.
Behavior notes
HttpClientConfigapply to outgoing HTTP requests and WebSocket upgrade handshakes.HttpClient.withFilterorHttpClient.withFiltersblock.HttpClientConfigremains a case class. Its equality now includes theclientFilterfield by reference, similar to the existing function-valuedretryOnfield.Pros
HttpFilter.Passthroughabstraction instead of introducing a separate middleware type.Tradeoffs
HttpClientConfigequality now includes the filter reference, so two configs with behaviorally equivalent but separately allocated filters will not compare equal.Validation
sbt 'kyo-http/doctest': passed, 64 doctest blocks, 0 failuressbt 'kyo-http/test': passed, 2,184 tests, 0 failed, 30 pendinggit diff --check: passedRelated issue