Skip to content

Instantly share code, notes, and snippets.

@Romain-P
Last active May 26, 2024 01:44
Show Gist options
  • Save Romain-P/feb26ae240de1e0d697f9759d6c922d7 to your computer and use it in GitHub Desktop.
Save Romain-P/feb26ae240de1e0d697f9759d6c922d7 to your computer and use it in GitHub Desktop.
Generic Spring boot with caching webClients service - Simplified but powerful exception-free webclient for external http calls. Made blocking on purpose, but feel free to remove block calls for reactive programming.
data class Response(val some: String, val data: Int)
data class ErrorResponse(val error: String, val some: String, val random: Int)
data class Request(val etc: String)
val url = "https://my-service.com/some/endpoint"
fun simple() {
val (dto, status) = service.post<Response>(url) {
it.bodyValue(Request("test"))
}
dto?.let { println(it.some) }
}
fun simple2() {
val (dto, status, headers, rawBody, jsonException) = service.post<Response>(url) { request ->
request.bodyValue(Request("test"))
request.headers { headers ->
headers["secret-key"] = "abcd"
}
}
if (status == HttpStatus.OK) {
dto?.let {
println(it)
} ?: jsonException?.message?.let {
println("Deserialization error: ${jsonException.message}")
}
} else {
println("Error $status: $rawBody")
}
}
fun advanced() {
//without body
val (dto, status, dtoError) = service.postFallback<Response, ErrorResponse>(url)
dto?.let { println(it) } ?: dtoError?.let { println(it) }
//etc
}
@Service
final class HttpService {
val defaultWebClient: WebClient
val webclients: MutableMap<String, WebClient> = mutableMapOf()
val mapper: ObjectMapper
val strictMapper = ObjectMapper()
init {
defaultWebClient = WebClient.builder()
.clientConnector(
ReactorClientHttpConnector(HttpClient.create()
.secure {
//it may happen that some dest. have invalid cert
it.sslContext(
SslContextBuilder.forClient()
.sslProvider(SslProvider.JDK)
.trustManager(BypassCertificateCheckManager())
.build()
)
}
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.of(5, ChronoUnit.MINUTES)))
)
.defaultHeaders {
it[HttpHeaders.CONTENT_TYPE] = MediaType.APPLICATION_JSON_VALUE
//drop Java user-agent, usually blocked by Cloudflare or others
it[HttpHeaders.USER_AGENT] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"
}
.build()
mapper = Jackson2ObjectMapperBuilder.json()
.featuresToDisable(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
)
.modules(JavaTimeModule())
.build()
}
inline fun <reified T, reified E> getFallback(
url: String,
strictDeserialization: Boolean = false,
config: (WebClient.RequestHeadersSpec<*>) -> Unit = {}): WebClientResponseFallback<T, E>
{
val parsedURL = URI(url)
val baseURL = UriComponentsBuilder.newInstance()
.scheme(parsedURL.scheme)
.host(parsedURL.host)
.port(parsedURL.port)
.toUriString()
var uri = parsedURL.rawPath ?: ""
parsedURL.rawQuery?.let { uri = "$uri?$it" }
val client = webclients.computeIfAbsent(url) {
defaultWebClient.mutate().baseUrl(baseURL).build()
}
val specs = client.get().uri(uri)
config(specs)
val response = specs
.exchangeToMono { response -> response.toEntity(String::class.java) }
.onErrorResume { throwable ->
Mono.just(ResponseEntity.status(500).body("Unexpected error: ${throwable.message}"))
}
.block()!!
var deserializationException: Exception? = null
var deserialized: T? = null
var deserializedOnError: E? = null
val deserializer = if (strictDeserialization)
strictMapper
else
mapper
if (!response.statusCode.isError) {
deserialized = try {
deserializer.readValue(response.body, T::class.java)
} catch (e: Exception) {
deserializationException = e
null
}
} else {
deserializedOnError = try {
deserializer.readValue(response.body, E::class.java)
} catch (e: Exception) {
deserializationException = e
null
}
}
return WebClientResponseFallback(deserialized, response.statusCode, deserializedOnError, response.headers, response.body, deserializationException)
}
inline fun <reified T> get(
url: String,
strictDeserialization: Boolean = false,
config: (WebClient.RequestHeadersSpec<*>) -> Unit = {}): WebClientResponse<T>
{
val parsedURL = URI(url)
val baseURL = UriComponentsBuilder.newInstance()
.scheme(parsedURL.scheme)
.host(parsedURL.host)
.port(parsedURL.port)
.toUriString()
var uri = parsedURL.rawPath ?: ""
parsedURL.rawQuery?.let { uri = "$uri?$it" }
val client = webclients.computeIfAbsent(url) {
defaultWebClient.mutate().baseUrl(baseURL).build()
}
val specs = client.get().uri(uri)
config(specs)
val response = specs
.exchangeToMono { response -> response.toEntity(String::class.java) }
.onErrorResume { throwable ->
Mono.just(ResponseEntity.status(500).body("Unexpected error: ${throwable.message}"))
}
.block()!!
var deserializationException: Exception? = null
var deserialized: T?
val deserializer = if (strictDeserialization)
strictMapper
else
mapper
deserialized = try {
deserializer.readValue(response.body, T::class.java)
} catch (e: Exception) {
deserializationException = e
null
}
return WebClientResponse(deserialized, response.statusCode, response.headers, response.body, deserializationException)
}
inline fun <reified T, reified E> postFallback(
url: String,
strictDeserialization: Boolean = false,
config: (WebClient.RequestBodySpec) -> Unit = {}): WebClientResponseFallback<T, E>
{
val parsedURL = URI(url)
val baseURL = UriComponentsBuilder.newInstance()
.scheme(parsedURL.scheme)
.host(parsedURL.host)
.port(parsedURL.port)
.toUriString()
var uri = parsedURL.rawPath ?: ""
parsedURL.rawQuery?.let { uri = "$uri?$it" }
val client = webclients.computeIfAbsent(url) {
defaultWebClient.mutate().baseUrl(baseURL).build()
}
val specs = client.post().uri(uri)
config(specs)
val response = specs
.exchangeToMono { response -> response.toEntity(String::class.java) }
.onErrorResume { throwable ->
Mono.just(ResponseEntity.status(500).body("Unexpected error: ${throwable.message}"))
}
.block()!!
var deserializationException: Exception? = null
var deserialized: T? = null
var deserializedOnError: E? = null
val deserializer = if (strictDeserialization)
strictMapper
else
mapper
if (!response.statusCode.isError) {
deserialized = try {
deserializer.readValue(response.body, T::class.java)
} catch (e: Exception) {
deserializationException = e
null
}
} else {
deserializedOnError = try {
deserializer.readValue(response.body, E::class.java)
} catch (e: Exception) {
deserializationException = e
null
}
}
return WebClientResponseFallback(deserialized, response.statusCode, deserializedOnError, response.headers, response.body, deserializationException)
}
inline fun <reified T> post(
url: String,
strictDeserialization: Boolean = false,
config: (WebClient.RequestBodySpec) -> Unit = {}): WebClientResponse<T>
{
val parsedURL = URI(url)
val baseURL = UriComponentsBuilder.newInstance()
.scheme(parsedURL.scheme)
.host(parsedURL.host)
.port(parsedURL.port)
.toUriString()
var uri = parsedURL.rawPath ?: ""
parsedURL.rawQuery?.let { uri = "$uri?$it" }
val client = webclients.computeIfAbsent(url) {
defaultWebClient.mutate().baseUrl(baseURL).build()
}
val specs = client.post().uri(uri)
config(specs)
val response = specs
.exchangeToMono { response -> response.toEntity(String::class.java) }
.onErrorResume { throwable ->
Mono.just(ResponseEntity.status(500).body("Unexpected error: ${throwable.message}"))
}
.block()!!
var deserializationException: Exception? = null
var deserialized: T?
val deserializer = if (strictDeserialization)
strictMapper
else
mapper
deserialized = try {
deserializer.readValue(response.body, T::class.java)
} catch (e: Exception) {
deserializationException = e
null
}
return WebClientResponse(deserialized, response.statusCode, response.headers, response.body, deserializationException)
}
data class WebClientResponse<T>(
val deserialized: T?,
val status: HttpStatusCode,
val headers: HttpHeaders,
val rawBody: String?,
val deserializationException: Exception?
)
data class WebClientResponseFallback<T, E>(
val deserialized: T?,
val status: HttpStatusCode,
val deserializedOnError: E?,
val headers: HttpHeaders,
val rawBody: String?,
val deserializationException: Exception?
)
private class BypassCertificateCheckManager : X509ExtendedTrustManager() {
override fun checkClientTrusted(x509Certificates: Array<X509Certificate>, s: String, sslEngine: javax.net.ssl.SSLEngine) {}
override fun checkClientTrusted(x509Certificates: Array<X509Certificate>, s: String, socket: java.net.Socket) {}
override fun checkClientTrusted(x509Certificates: Array<X509Certificate>, s: String) { }
override fun checkServerTrusted(x509Certificates: Array<X509Certificate>, s: String, sslEngine: javax.net.ssl.SSLEngine) {}
override fun checkServerTrusted(x509Certificates: Array<X509Certificate>, s: String, socket: java.net.Socket) {}
override fun checkServerTrusted(x509Certificates: Array<X509Certificate>, s: String) { }
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment