What would you like fabrikt to generate?
We have a use case where we need to dynamically adjust the host URL we call for a client. (For context, one of our backends has multiple instances with different URLs in the same environment, and we have to call a different instance based on a request parameter on our side. We cannot change that setup.)
This can be done in Spring HTTP interfaces by adding a UriBuilderFactory to every method (see spring-projects/spring-framework#30935 (comment)).
It would be great if fabrikt provides an option to automatically add the UriBuilderFactory to all methods of a client.
I'm currently working around this with an interceptor and placeholders in the URL, which works. But it does its magic in the background, and the request parameter we use to decide on the instance goes unused in our own controllers, which is confusing to developers. With the UriBuilderFactory included in the method, they would have to explicitly provide the request parameter to build the URL.
Workaround example
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.boot.restclient.RestClientCustomizer
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.http.client.support.HttpRequestWrapper
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import org.springframework.web.context.annotation.RequestScope
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.UriComponentsBuilder
import java.net.URI
import java.net.URISyntaxException
import java.util.concurrent.atomic.AtomicReference
@Component
@RequestScope
class HostReplacementHolder {
private val replacement: AtomicReference<String?> = AtomicReference<String?>(null)
fun getCurrentReplacement(): String? {
return replacement.get()
}
fun setCurrentReplacement(value: String?) {
replacement.set(value)
}
}
@Component
class HostReplacementExtractionFilter(private val hostReplacementHolder: HostReplacementHolder) :
OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
val replacement = request.getParameter("replacement")
hostReplacementHolder.setCurrentReplacement(replacement)
filterChain.doFilter(request, response)
}
}
@Component
class DynamicUrlRestClientCustomizer(private val dynamicUrlInterceptor: DynamicUrlInterceptor) :
RestClientCustomizer {
override fun customize(restClientBuilder: RestClient.Builder) {
restClientBuilder.requestInterceptors { interceptors ->
interceptors.add(dynamicUrlInterceptor)
}
}
}
private const val PLACEHOLDER = "MY_PLACEHOLDER"
@Component
class DynamicUrlInterceptor(private val hostReplacementHolder: HostReplacementHolder) :
ClientHttpRequestInterceptor {
override fun intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution,
): ClientHttpResponse {
val uri: URI = request.uri
if (uri.toString().contains(PLACEHOLDER)) {
val replacement =
hostReplacementHolder.getCurrentReplacement()
?: error(
"Trying to call an API that contains a placeholder without a replacement request parameter."
)
return execution.execute(DynamicUrlHttpRequestWrapper(request, replacement), body)
}
return execution.execute(request, body)
}
}
private class DynamicUrlHttpRequestWrapper(request: HttpRequest, private val replacement: String) :
HttpRequestWrapper(request) {
override fun getURI(): URI {
try {
val newHost = super.uri.authority.replace(PLACEHOLDER, replacement)
.substringBefore(":") // remove port
val originalUri = super.uri.toString()
val newUri = UriComponentsBuilder.fromUriString(originalUri).host(newHost).build()
return newUri.toUri()
} catch (e: URISyntaxException) {
throw RuntimeException(e)
}
}
}
Example Spec
"/my-api": {
"get": {
"operationId": "doStuff",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
Desired Output
public interface MyClient {
@HttpExchange(
url="/my-api",
method="GET",
accept=["*/*"],
)
public fun doStuff(
uriBuilderFactory: UriBuilderFactory,
@PathVariable("id") id: UUID,
): String
}
What would you like fabrikt to generate?
We have a use case where we need to dynamically adjust the host URL we call for a client. (For context, one of our backends has multiple instances with different URLs in the same environment, and we have to call a different instance based on a request parameter on our side. We cannot change that setup.)
This can be done in Spring HTTP interfaces by adding a
UriBuilderFactoryto every method (see spring-projects/spring-framework#30935 (comment)).It would be great if fabrikt provides an option to automatically add the
UriBuilderFactoryto all methods of a client.I'm currently working around this with an interceptor and placeholders in the URL, which works. But it does its magic in the background, and the request parameter we use to decide on the instance goes unused in our own controllers, which is confusing to developers. With the
UriBuilderFactoryincluded in the method, they would have to explicitly provide the request parameter to build the URL.Workaround example
Example Spec
Desired Output