Skip to content

[BUG][Kotlin][WebClient] ApiClient is not compatible with spring-web 6 Observation API #21017

Closed
@NavruzshoevDaniel

Description

@NavruzshoevDaniel
Description

This issue is related to this one. I'm facing the same issue with the Kotlin implementation.

I'll briefly describe the problem.

When Spring uses WebClient, it sets a special attribute URI_TEMPLATE_ATTRIBUTE which represents the URI path. However, the only function that does not set this attribute is public RequestBodySpec uri(Function<UriBuilder, URI> uriFunction) (see here).

This attribute is needed to correctly set the URI tag for http_client_requests_seconds_* metrics (see DefaultClientRequestObservationConvention.java#L106 and DefaultWebClient.java#L464).

Currently, the Kotlin OpenAPI Generator implementation does not support this attribute (see ApiClient.kt.mustache#L42).

However, the Java implementation does include this attribute (see ApiClient.mustache#L684).

Could you add this attribute to the Kotlin implementation as well?

openapi-generator version
  • Openapi-generator 7.12.0
  • Spring Boot 3.3.9
Related issues/PRs

#15143

Suggest a fix

Update this file with the following implementation.

package {{packageName}}.infrastructure;

import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.http.ResponseEntity
import org.springframework.http.client.MultipartBodyBuilder
import org.springframework.util.LinkedMultiValueMap
import reactor.core.publisher.Mono

private val URI_TEMPLATE_ATTRIBUTE = WebClient::class.java.name + ".uriTemplate";

{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}open class ApiClient(protected val client: WebClient) {

    protected inline fun <reified I : Any, reified T: Any?> request(requestConfig: RequestConfig<I>): Mono<ResponseEntity<T>> {
        return prepare(defaults(requestConfig))
            .retrieve()
            .toEntity(object : ParameterizedTypeReference<T>() {})
    }

    protected fun <I : Any> prepare(requestConfig: RequestConfig<I>) =
        client.method(requestConfig)
            .uri(requestConfig)
            .headers(requestConfig)
            .body(requestConfig)

    protected fun <I> defaults(requestConfig: RequestConfig<I>) =
        requestConfig.apply {
            if (body != null && headers[HttpHeaders.CONTENT_TYPE].isNullOrEmpty()) {
                headers[HttpHeaders.CONTENT_TYPE] = MediaType.APPLICATION_JSON_VALUE
            }
            if (headers[HttpHeaders.ACCEPT].isNullOrEmpty()) {
                headers[HttpHeaders.ACCEPT] = MediaType.APPLICATION_JSON_VALUE
            }
        }

    private fun <I> WebClient.method(requestConfig: RequestConfig<I>)=
        method(HttpMethod.valueOf(requestConfig.method.name))

    private fun <I> WebClient.RequestBodyUriSpec.uri(requestConfig: RequestConfig<I>) =
        uri { builder ->
            attribute(URI_TEMPLATE_ATTRIBUTE, requestConfig.path)
            builder
                .path(requestConfig.path)
                .queryParams(LinkedMultiValueMap(requestConfig.query))
                .build(requestConfig.params)
        }

    private fun <I> WebClient.RequestBodySpec.headers(requestConfig: RequestConfig<I>) =
        apply { requestConfig.headers.forEach { (name, value) -> header(name, value) } }

    private fun <I : Any> WebClient.RequestBodySpec.body(requestConfig: RequestConfig<I>): WebClient.RequestBodySpec {
        when {
            requestConfig.headers[HttpHeaders.CONTENT_TYPE] == MediaType.MULTIPART_FORM_DATA_VALUE -> {
                val builder = MultipartBodyBuilder()
                (requestConfig.body as Map<String, PartConfig<*>>).forEach { (name, part) ->
                    if (part.body != null) {
                        val partBuilder = builder.part(name, part.body)
                        val partHeaders = part.headers
                        partHeaders.forEach { partBuilder.header(it.key, it.value) }
                    }
                }
                return apply { bodyValue(builder.build()) }
            }
            else -> {
                return apply { if (requestConfig.body != null) bodyValue(requestConfig.body) }
            }
        }
    }
}

{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}inline fun <reified T: Any> parseDateToQueryString(value : T): String {
        {{#toJson}}
        /*
        .replace("\"", "") converts the json object string to an actual string for the query parameter.
        The moshi or gson adapter allows a more generic solution instead of trying to use a native
        formatter. It also easily allows to provide a simple way to define a custom date format pattern
        inside a gson/moshi adapter.
        */
        {{#jackson}}
        return Serializer.jacksonObjectMapper.writeValueAsString(value).replace("\"", "")
        {{/jackson}}
        {{/toJson}}
        {{^toJson}}
        return value.toString()
        {{/toJson}}
    }

#21020

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions