简体   繁体   中英

Micronaut HTTP Client Fails to Bind Response that is Missing Content-Type Header

I've successfully used the Micronaut HTTP Client with several different external services in the past. However, I'm really struggling with one external service. I think it might be related to the fact that the response from the external service does not contain a Content-Type header, but I'm not sure.

The client and response type are defined in the same groovy file.

package us.cloudcard.api.transact

import groovy.transform.ToString
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Produces
import io.micronaut.http.client.annotation.Client

import javax.validation.constraints.NotNull

@Client('${transact.url}')
interface TransactAuthenticationClient {

    @Post
    @Produces(MediaType.TEXT_PLAIN)
    HttpResponse<TransactAuthenticationResponse> authenticate(@NotNull @Body String token)
}

@ToString
class TransactAuthenticationResponse {
    Boolean Expired
    String InstitutionId
    String UserName
    String customCssUrl
    String email
    String role
}

I'm testing it with a simple controller that just calls the client and renders the response status and body.

package us.cloudcard.api

import grails.compiler.GrailsCompileStatic
import grails.converters.JSON
import grails.plugin.springsecurity.annotation.Secured
import io.micronaut.http.HttpResponse
import org.springframework.beans.factory.annotation.Autowired
import us.cloudcard.api.transact.TransactAuthenticationClient
import us.cloudcard.api.transact.TransactAuthenticationResponse

@GrailsCompileStatic
@Secured("permitAll")
class MyController {
    static responseFormats = ['json', 'xml']

    @Autowired
    TransactAuthenticationClient transactAuthenticationClient

    def show(String id) {
        String goodToken = "5753D...REDACTED...647F"
        HttpResponse response = transactAuthenticationClient.authenticate(goodToken)
        TransactAuthenticationResponse authenticationResponse = response.body()
        log.error("status: ${response.status()} body: $authenticationResponse")
        render "status: ${response.status()} body: $authenticationResponse"
    }

}

However, the result I get is

status: OK body: null

Making the same request in Postman results in the correct response邮递员请求

When I debug, I can inspect the HttpResponse object and see all the correct headers, so I know I'm making the request successfully. I just can't bind the response. 在此处输入图像描述

I tried changing the client to bind to a String

    @Post
    @Produces(MediaType.TEXT_PLAIN)
    HttpResponse<String> authenticate(@NotNull @Body String token)

and I got the following response

status: OK body: PooledSlicedByteBuf(ridx: 0, widx: 176, cap: 176/176, unwrapped: PooledUnsafeDirectByteBuf(ridx: 484, widx: 484, cap: 513))

This was interesting because the widx: 176, cap: 176/176 perfectly matched the content length of the successful response.

I am really at a loss, so I would appreciate any help you can give.

Thanks in advance for your help!

TL;DR: The Micronaut HTTP Client is not designed to work for this API

The Micronaut HTTP Client cannot consume APIs that do not include a content-type header in the response. I talked with Jeff Scott Brown about this, and that's just how Micronaut is designed. If there's no content-type header in the response, the client won't know how to parse the response body.

Work-Around

package us.cloudcard.api.transact

import groovy.json.JsonSlurper
import groovy.transform.ToString
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component

@Component
class TransactAuthenticationClient {

    @Value('${transact.url}')
    String transactAuthenticationUrl

    TransactAuthenticationResponse workaround2(String token) {
        HttpPost post = new HttpPost(transactAuthenticationUrl)
        post.addHeader("content-type", "text/plain")
        post.setEntity(new StringEntity(token))

        CloseableHttpClient client = HttpClientBuilder.create().build()
        CloseableHttpResponse response = client.execute(post)

        def bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))
        def json = bufferedReader.getText()
        println "response: \n" + json

        def resultMap = new JsonSlurper().parseText(json)
        return new TransactAuthenticationResponse(resultMap)
    }
}

@ToString(includeNames = true)
class TransactAuthenticationResponse {
    Boolean Expired
    String InstitutionId
    String UserName
    String customCssUrl
    String email
    String role
}

JUST FYI: Before I found the above workaround, I tried this and it also does not work.

    TransactAuthenticationResponse thisAlsoDoesNotWork (String token) {
        String baseUrl = "https://example.com"
        HttpClient client = HttpClient.create(baseUrl.toURL())
        HttpRequest request = HttpRequest.POST("/path/to/endpoint", token)
        HttpResponse<String> resp = client.toBlocking().exchange(request, String)
        String json = resp.body()
        println "json: $json"
        ObjectMapper objectMapper = new ObjectMapper()
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        TransactAuthenticationResponse response = objectMapper.readValue(json, TransactAuthenticationResponse)
        return response
    }

Having the same problem, this is the best solution I found so far:

import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.TypeConverter;
import io.netty.buffer.ByteBuf;

// (...)

ConversionService.SHARED.addConverter(ByteBuf.class, String.class, new TypeConverter<ByteBuf, String>() {
      @Override
      public Optional<String> convert(ByteBuf object, Class<String> targetType, ConversionContext context) {
          return Optional.ofNullable(object).map(bb -> bb.toString(StandardCharsets.UTF_8));
      }
});
HttpRequest<String> req = HttpRequest.POST("<url>", "<body>");
// res is instance of io.micronaut.http.client.netty.FullNettyClientHttpResponse which uses the shared conversion service as "last chance" to convert the response body
HttpResponse<String> res = httpClient.toBlocking().exchange(req);
String responseBody = res.getBody(String.class).get();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM