No http client metrics for non 200 response codes in Quarkus Micrometer - quarkus

I have the following Quarkus Rest client code (based on this doc https://quarkus.io/guides/rest-client)
#RegisterRestClient(baseUri = "https://pesho3.free.beeceptor.com")
interface TokenService {
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Produces(MediaType.APPLICATION_JSON)
#ClientHeaderParam(
name = "Authorization",
value = ["Basic asdasd"]
)
fun getToken(
#FormParam("grant_type") grantType: String = "client_credentials",
#FormParam("scope") scope: String = "IIG-HIP-NP/Read"
): JSONObject
}
When I call my getToken() method and get http 200 I get automatically generated metrics in localhost:8080/q/metrics (as stated in this doc https://quarkus.io/guides/micrometer#review-automatically-generated-metrics)
e.g
http_client_requests_seconds_count{clientName="pesho3.free.beeceptor.com",method="POST",outcome="SUCCESS",status="200",uri="root",} 2.0
http_client_requests_seconds_sum{clientName="pesho3.free.beeceptor.com",method="POST",outcome="SUCCESS",status="200",uri="root",} 1.116203
I don't get any metrics for non 200 codes.. How can I expose them ?

I found the solution.. Its this property (not mentioned in Quarkus doc)
microprofile.rest.client.disable.default.mapper=true
The answer was in this doc:
https://download.eclipse.org/microprofile/microprofile-rest-client-1.3/microprofile-rest-client-1.3.html#_default_responseexceptionmapper

Related

Best Pratices: How to modify RestTemplate so if (response is 401 and url is in a certain API list) then auto call login?

I am writing a Spring application using Kotlin that has:
API call is in 2 main list of API:
API belong to internal system: Automatic call login when response is 401 unauthorized
API being called by external service: Return response with error message when 401 unauthorized
Right now I'm using RestTemplate to call API, so my questions is what is the best pratices to modify RestTemplate so if (response is 401 and url is in the second API list) then auto call login?
Example of my code:
List API endpoints:
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration
#Configuration
class AppApiEnpoint(
#Autowired
private val appProperties: AppProperties
) {
var orderCreateUrl: String = appProperties.getUrl("/order/create.json").toString()
}
API call
fun create(contentRequest: OrderSendAgentDto, accessToken: String): ResponseEntity<OrderResponseDto> {
val requestUri = UrlBuilder(appApiEnpoint.orderCreateUrl).toString()
val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_FORM_URLENCODED
val body = HttpUtils.convertObject2Map(contentRequest)
body.add("access_token", accessToken)
val request = HttpEntity(body, headers)
val restTemplate = RestTemplate()
val response = restTemplate.postForEntity(requestUri, request, OrderResponseDto::class.java)
return response
}
Note:
I did come up with a way to wrap the response in a Util method to handle this logic but if so then there will be a lot of code that was used before that I have to fix and new APIs are constantly being added during development. This is not an suitable approach
You can set ResponseErrorHandler to the rest template that should do auto-login, or even use #Retry with retry handlers on API methods that should do auto-login

Springfox swagger - content type multipart/form-data

I have an API for uploading multiple files with below signature - takes in a list of multipart files and a request object.
#ApiOperation(consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
#PostMapping("/upload")
public void uploadMultipleFiles(#RequestParam("req") RequestDTO request, #RequestParam("files") List<MultipartFile> files) {}
When I test this API using Postman it works but I when try using swagger, I noticed the content-type is passed as application/json and the API gives an error 'Current request is not multpart'. I tried adding consumes to #ApiOperation but the content-type is still application/json.
Files in OpenAPI 3.x are better represented by application/octet-stream.
I solved the issue using the below approach
#Operation( // Swagger/OpenAPI 3.x annotation to describe the endpoint
summary = "Small summary of the end-point",
description = "A detailed description of the end-point"
)
#PostMapping(
value = "/uploads",
consumes = {MediaType.MULTIPART_FORM_DATA_VALUE} // Note the consumes in the mapping
)
public void uploadMultipleFiles (
// ------------ Multipart Object ------------
#Parameter(description = "Additional request data") // Swagger/OpenAPI annotation
#RequestParam("req") RequestDTO request, // Spring annotation
// ------------ File uploads go next ------------
#Parameter(
description = "Files to be uploaded",
content = #Content(mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE) // Won't work without OCTET_STREAM as the mediaType.
)
#RequestParam("files") List<MultipartFile> files // Spring annotation
)
More details for file upload in the OpenAPI 3 specification can be found here

How to do java unit test with protobuf for controller?

I have a spring boot rest controller with requestBody & responseBody both protobuf. like below :
#RequestMapping(value = "/position/open", produces = "application/x-protobuf")
#ResponseBody
public MsgProto.Response positionOpen(#RequestBody MsgProto.Request request)throws Exception {
log.info("start /position/open");
return orderPositionService.addOrder(request);
}
Now I want to do a unit test using mockMvc to test the controller, but it failed every time. I believe it is the code below which is wrong to fire an HTTP request with protobuf, any idea how to resolve it?
mockMvc.perform(post("/position/open").contentType("application/x-protobuf")
.content(ObjectsMock.mockMsgProtoRequest().toByteArray())).andDo(print())
.andExpect(status().isOk());
Exception :
Resolved Exception:
Type = org.springframework.web.HttpMediaTypeNotSupportedException
MockHttpServletResponse:
Status = 415
Error message = null
Headers = [Accept:"application/json, application/octet-stream,
application/xml, application/*+json, text/plain, text/xml, application/x-www-
form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, */*"]
I assume the ProtobufHttpMessageConverter is missing here. Spring MVC can't read/write any messages without this specific converter.
You can create it as the following:
#Bean
public ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
Next, make sure to add the HTTP Method to your method, as I assume (from reading your test) you want this to be a HTTP POST handler. You can also add the consumes attribute to state that this endpoint also consumes Protobuf.
#RequestMapping(method = RequestMethod.POST, consumes = "application/x-protobuf", value = "/position/open", produces = "application/x-protobuf")
#ResponseBody
public MsgProto.Response positionOpen(#RequestBody MsgProto.Request request)throws Exception {
log.info("start /position/open");
return orderPositionService.addOrder(request);
}
In addition to this, there is an article on the Spring blog available that covers your usecase and explains how to use Protobuf with Spring MVC.
You need to add Protobuf converter to MockMvc builder
MockMvcBuilders.standaloneSetup(controller)
.setMessageConverters(new ProtobufHttpMessageConverter())
.build()
This fixed the issue for me

swagger annotation content-type not set

I have this spring rest controller:
#RestController
#RequestMapping("/communications")
class CommunicationController(private val service: CommunicationService) {
#ApiOperation(
produces = APPLICATION_JSON_VALUE,
consumes = APPLICATION_JSON_VALUE
)
#GetMapping(
consumes = [APPLICATION_JSON_VALUE],
produces = [APPLICATION_JSON_VALUE]
)
fun findAll(
criterias: CommunicationCriterias,
page: Pageable
): List<CommunicationDTO> = service.findCommunications(criterias, page)
}
When I test this endpoint via the swagger-ui (springfox) interface, i got a 415: content type invalid error. It seems that content-type: application/json is not set in the header.
What is missing ?
There is nothing to consume in HTTP GET request. I think you should remove the consumes from #GetMapping and #ApiOperation.

Supporting application/json and application/x-www-form-urlencoded simultaneously from Spring's rest controller

Am writing a REST endpoint which needs to support both application/x-www-form-urlencoded and application/json as request body simultaneously. I have made below configuration,
#RequestMapping(method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE }, consumes = {
MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE }, path = Constants.ACCESS_TOKEN_V1_ENDPOINT)
public OAuth2Authorization createAccessTokenPost(
#RequestBody(required = false) MultiValueMap<String, String> paramMap) { ..
While it supports application/x-www-form-urlencoded or application/json individually (when I comment out one content type from consumes = {}), but it does not support both simultaneously. Any ideas ?
So RestControllers by default can handle application/json fairly easily and can create a request pojo from a #RequestBody annotated parameter, while application/x-www-form-urlencoded takes a little more work. A solution could be creating an extra RestController method that has the same mapping endpoint to handle the different kinds of requests that come in (application/json, application/x-www-form-urlencoded, etc). This is because application/x-www-form-urlencoded endpoints need to use the #RequestParam instead of the #RequestBody annotation (for application/json).
For instance if I wanted to host a POST endpoint for /emp that takes either application/json or application/x-www-form-urlencoded as Content-Types and uses a service to do something, I could create Overload methods like so
#Autowired
private EmpService empService;
#PostMapping(path = "/emp", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity createEmp(final #RequestHeader(value = "Authorization", required = false) String authorizationHeader,
final #RequestParam Map<String, String> map) {
//After receiving a FORM URLENCODED request, change it to your desired request pojo with ObjectMapper
final ObjectMapper mapper = new ObjectMapper();
final TokenRequest tokenRequest = mapper.convertValue(map, CreateEmpRequest.class);
return empService.create(authorizationHeader, createEmpRequest);
}
#PostMapping(path = "/emp", consumes = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity createEmp(final #RequestHeader(value = "Authorization", required = false) String authorizationHeader,
final #RequestBody CreateEmpRequest createEmpRequest) {
//Receieved a JSON request, the #RequestBody Annotation can handle turning the body of the request into a request pojo without extra lines of code
return empService.create(authorizationHeader, createEmpRequest);
}
As per my findings, spring does not support content types "application/x-www-form-urlencoded", "application/json" and "application/xml" together.
Reason I figured: Spring processes JSON and XML types by parsing and injecting them into the java pojo marked with #RequestBody spring annotation. However, x-www-form-urlencoded must be injected into a MultiValueMap<> object marked with #RequestBody. Two different java types marked with #RequestBody will not be supported simultaneously, as spring may not know where to inject the payload.
A working solution:
"application/x-www-form-urlencoded" can be supported as it is in the API. That is, it can be injected into spring's MultiValueMap<> using an #RequestBody annotation.
To support JSON and XML on the same method, we can leverage servlet specification and spring's class built on top of them to extract the payload as stream.
Sample code:
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.MultiValueMap;
// usual REST service class
#Autowired
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;
#Autowired
private Jaxb2RootElementHttpMessageConverter jaxb2RootElementHttpMessageConverter;
public ResponseEntity<Object> authorizationRequestPost(HttpServletResponse response, HttpServletRequest request,#RequestBody(required = false) MultiValueMap<String, String> parameters) {
// this MultiValueMap<String,String> will contain key value pairs of "application/x-www-form-urlencoded" parameters.
// payload object to be populated
Authorization authorization = null;
HttpInputMessage inputMessage = new ServletServerHttpRequest(request) {
#Override
public InputStream getBody() throws IOException {
return request.getInputStream();
}
};
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
authorization = (Authorization) mappingJackson2HttpMessageConverter.read(Authorization.class, inputMessage);
}
else if (request.getContentType().equals(MediaType.APPLICATION_XML_VALUE)) {
authorization = (Authorization)jaxb2RootElementHttpMessageConverter.read(Authorization.class, inputMessage);
}
else{
// extract values from MultiValueMap<String,String> and populate Authorization
}
// remaining method instructions
}
Point to note that any custom data type/markup/format can be supported using this approach. Spring's org.springframework.http.converter.HttpMessageConverter<> can be extended to write the parsing logic.
Another possible approach could be an AOP style solution which would execute the same logic: parse payload by extracting it from HttpServlet input stream and inject into the payload object.
A third approach will be to write a filter for executing the logic.
It's not possible to handle application/json and application/x-www-form-urlencoded requests simultaneously with a single Spring controller method.
Spring get application/x-www-form-urlencoded data by ServletRequest.getParameter(java.lang.String), the document said:
For HTTP servlets, parameters are contained in the query string or posted form data.
If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.
So, if your method parameter is annotated with #RequestBody, Spring will read request body and parse it to the method parameter object. But application/x-www-form-urlencoded leads Spring to populate the parameter object by invoking ServletRequest.getParameter(java.lang.String).
Just to make it, the above answer doesn't work as even if you do not annotate MultiValueMap with #RequestBody it would always check for contentType==MediaType.APPLICATION_FORM_URLENCODED_VALUE which again in rest of the cases resolves to 415 Unsupported Media Type.

Resources