Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
NavruzshoevDaniel opened this issue Apr 2, 2025 · 0 comments

Comments

@NavruzshoevDaniel
Copy link

NavruzshoevDaniel commented Apr 2, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant