Springboot external api call request and response capture in database - java

From my backend application(springboot, java8) i will make multiple external api call's. I have a requirement to log all the requests and response data's (including headers, request and response body) into database(MongoDB).
Below is my sample code, this how i am trying to capture request and responses on each external api calls. On exception i will store status as 'FAILED'.
In my project multiple modules will be added on new 3rd party api integration, so in each module for every different external api calls i have to capture all the requests and reponses like this. I am not satisfied with below approach. Kindly suggest best approach to solve this.
Sample Service layer method
public ResponseDTOFromExternalApi externalApiCallServiceMethod(String clientId, RequestDTO requestDTO) {
ExternalApiCallRequestObj externalApiCallRequestObj = prepareExternalApiRequestObjFromRequestDTO(requestDTO);
ApiCall apiCall = ApiCall.builder()
.clientId(clientId)
.status("SUBMITTED")
.requestPayload(externalApiCallRequestObj)
.build();
apiCall = apiCallRepository.save(apiCall);
ExternalApiCallReponseObj externalApiCallReponseObj = externalApiCallService.callExternalApi1(externalApiCallRequestObj);
apiCall = apiCallRepository.findById(apiCall.getId());
apiCall.setResponsePayload(externalApiCallReponseObj);
apiCall.setStatus("COMPLETED");
apiCallRepository.save(apiCall);
return toDTO(externalApiCallReponseObj);
}
Sample Domain for api calls
#Document("api_calls")
#Builder
#Data
public class ApiCall {
#Id
private String id;
private String clientId;
private String status;
private Object requestPayload;
private Object responsePayload;
}

Spring's WebClient already has the ability to log all request and response data by adding exchange filters.
By using it for your network requests the only thing left to do is to write this information in your mongodb.
Here is tutorial on logging requests and responses:
https://www.baeldung.com/spring-log-webclient-calls
Cheers

You may use Spring AOP to address this cross cutting concern.
Assuming ExternalApiCallService is a spring managed bean , following code will intercept all the callExternalApi1() and can log the same to database.
#Component
#Aspect
public class ExternalCallLoggerAspect {
#Autowired
ApiCallRepository apiCallRepository;
#Pointcut("execution(* *..ExternalApiCallService.callExternalApi1(..))")
public void externalApiCallService(){}
#Around("externalApiCallService() && args(request)")
public ExternalApiCallReponseObj logCalls(ProceedingJoinPoint pjp,ExternalApiCallRequestObj request){
Object result=null;
String status = "COMPLETED";
ExternalApiCallReponseObj response = null;
// Build the apiCall from request
ApiCall apiCall = ApiCall.builder()
.clientId(clientId)
.status("SUBMITTED")
.requestPayload(request)
.build();
//save the same to db
apiCall = apiCallRepository.save(apiCall);
// Proceed to call the external Api and get the result
try {
result = pjp.proceed();
} catch (Throwable e) {
status = "FAILED";
}
//Update the response
apiCall = apiCallRepository.findById(apiCall.getId());
apiCall.setStatus(status);
apiCallRepository.save(apiCall);
if(result != null) {
response = (ExternalApiCallReponseObj)result;
apiCall.setResponsePayload(response);
}
//continue with response
return response;
}
}
Note
1.There is a typo with the name ExternalApiCallReponseObj
2.The aspect code is verified that it works and the logic was included later on untested. Please make the required corrections
Ideally the original method should be stripped down to this
public ResponseDTOFromExternalApi externalApiCallServiceMethod(String clientId, RequestDTO requestDTO) {
return toDTO(externalApiCallService.callExternalApi1(prepareExternalApiRequestObjFromRequestDTO(requestDTO)));
}
More about Spring AOP here
Update : on a second thought , if all the external api calls are through a single method , say ExternalApiCallService.callExternalApi1() , this logging logic can be moved to that common point , isn't it ?

Related

How to pick values from a redirected_url using Springboot

I want to be able to fetch a param from the redirect url whenever it is automated. I am having difficulties doing this as I am getting a bad request after I created another endpoint to effect this.
I have an endpoint that works fine. The endpoint is a get method. Loading the endpoint takes a user to a page where they need to provide some necessary details. Once these details have been verified, the user is redirected to my redirecr_uri. The redirect_uri now contains important information like session_id, code, etc. The most important thing I need is the code. I need to pass the code into yet another endpoint which will return an access token.
I have manually done this process and it works but I want it to be done automatically because I can't keep doing that when I push the code to staging or production.
Here is the endpoint that redirects as well as the method.
#GetMapping("/get-token")
public RedirectView getBvn() throws UnirestException {
return nibss.getAccessToken();
}
This is the method that the controller calls
public RedirectView getAccessToken() throws UnirestException {
String url = "https://idsandbox.nibss-plc.com.ng/oxauth/authorize.htm?scope=profile&acr_values=otp&response" +
"_type=code&redirect_uri=https://www.accionmfb.com/&client_id=0915cd00-67f2-4768-99ac-1b2ff9f1da2e";
RedirectView redirectView = new RedirectView();
redirectView.setUrl(url);
return redirectView;
}
When the user provides the right information they are redirected to something like this
https://www.accionmfb.com/?code=9ad91f13-4698-4030-8a8f-a857e6a9907e&acr_values=otp&scope=profile&session_state=fa525cabc5b62854c73315d0322fd830c12a5941b89fd8e6e518da369e386572.b78a3d21-e98e-4e9a-8d60-afca779d9fad&sid=fd60ab92-ef37-4a5b-99b9-f8f52321985d
It is important to state that this 3rd party API I am trying to consume uses oath2.0 client authentication.
I created this endpoint to get the code from the redirected_uri
#GetMapping("/redirect-url")
public void handleRedirect(#RequestParam("code") String code) throws UnirestException {
if(Objects.nonNull(code) || !code.isEmpty()){
nibss.getToken(code);
log.info("Code is not being passed {}", code);
} else {
log.info("Code is not being passed {}", code);
}
}
public String getToken(String code) throws UnirestException {
log.info("This is the code here oooooooooo {}", code);
String url = "https://idsandbox.nibss-plc.com.ng/oxauth/restv1/token";
String parameters = "client_id=0915cd00-67f2-4768-99ac-1b2ff9f1da2e&code="+code+"&redirect_uri=https://www.accionmfb.com/&grant_type=authorization_code";
HttpResponse<String> apiResponse = Unirest.post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic MDkxNWNkMDAtNjdmMi00NzY4LTk5YWMtMWIyZmY5ZjFkYTJlOlRVRnEwcGFBQXRzbzBZOEcxMkl2WFZHUmx6WG5zaERiaGt1dzI1YUM=")
.body(parameters)
.asString();
//JSONObject apiJson = apiResponse.getBody().getObject();
//return apiJson.getString("access_token");
JSONObject json = new JSONObject(apiResponse.getBody());
String accessToken = json.getString("access_token");
log.info(accessToken);
return accessToken;
}
But this is not working, I get 400 whenever I hit the second endpoint. What am I doing wrong?
The redirect_uri that you are passing to the OAuth server is https://www.accionmfb.com which does not include the path /redirect-url so the redirect never hits your method.
Either register and pass a callback uri like redirect_uri=https://www.accionmfb.com/redirect-url
Or change #GetMapping("/redirect-url") to #GetMapping("/")

Stateless Resource Sever effiently validating Facebook tokens

I am writing a Resource Server backend for a mobile app front end. The app is responsible for getting a token from Facebook and passing it to the backend for authz, but as they do not use JWT bearer tokens as other providers do (eg. google, apple)... and which Spring has some very nice libraries for... I have had to write this custom bit to validate the token and collect some basic user information.
This works fairly well, but response time can be variably sluggish if I need to run this for each request to my stateless application. I am considering some form of caching for this token after it has been validated (with a ttl of course)... but am generally curious if there are any established techniques I should be using.
#Data
class FacebookDetails {
private String id;
private String name;
private String email;
}
class Facebook {
public AuthenticationProvider getAuthenticationProvider() {
WebClient facebookWebClient = WebClient.create("https://graph.facebook.com/v10.0");
OpaqueTokenIntrospector introspector = = token -> {
Optional<ResponseEntity<FacebookDetails>> maybeResponse = facebookWebClient.get()
.uri("/me?fields=id,name,email&access_token=" + token)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(FacebookDetails.class)
.onErrorResume(e -> Mono.empty())
.blockOptional(Duration.ofMinutes(1L));
if (maybeResponse.isEmpty()) {
throw new BadOpaqueTokenException("token is invalid");
}
FacebookDetails details = maybeResponse.get().getBody();
return details.getPrincipal();
};
return new OpaqueTokenAuthenticationProvider(introspector);
}
}
One could cache the access token as long as it is valid for. To find out how long that is, you could interpret the expires_in claim in the JSON response from the Facebook token endpoint, as described here: https://developers.facebook.com/docs/facebook-login/access-tokens/refreshing/

Combine file upload and request body on a single endpoint in rest controller

The UI for my webapp has the ability to either upload a file(csv), or send the data as json in request body. However either a file upload, or a json request would be present in the request and not both. I am creating a spring rest controller which combine file upload and also accepts the request json values as well.
With the below endpoint tested from postman, I am not getting exception:
org.apache.tomcat.util.http.fileupload.FileUploadException: the request was rejected because no multipart boundary was found
#RestController
public class MovieController {
private static final Logger LOGGER = LoggerFactory.getLogger(MovieController.class);
#PostMapping(value="/movies", consumes = {"multipart/form-data", "application/json"})
public void postMovies( #RequestPart String movieJson, #RequestPart(value = "moviesFile") MultipartFile movieFile ) {
// One of the below value should be present and other be null
LOGGER.info("Movies Json Body {}", movieJson);
LOGGER.info("Movies File Upload {}", movieFile);
}
}
Appreciate any help in getting this issue solved?
Note: I was able to build two separate endpoint for file upload and json request, but that won't suffice my requirement. Hence I'm looking for a solution to combine both
Try something like:
#RequestMapping(value = "/movies", method = RequestMethod.POST, consumes = { "multipart/form-data", "application/json" })
public void postMovies(
#RequestParam(value = "moviesFile", required = false) MultipartFile file,
UploadRequestBody request) {
In RequestBody you can add the parameters you want to send.
This will not send the data as JSON.
Edit:- I forgot to add the variable for the Multipart file and I mistakenly used the RequestBody which is reserved keyword in spring.
Hope it helps.
I would suggest to create two separate endpoints. This splits and isolates the different functionality and reduces the complexity of your code. In addition testing would be easier and provides better readability.
Your client actually has to know which variable to use. So just choose different endpoints for your request instead of using different variables for the same endpoint.
#PostMapping(value="/movies-file-upload", consumes = {"multipart/form-data"})
public void postMoviesFile(#RequestPart(value = "moviesFile") MultipartFile movieFile ) {
LOGGER.info("Movies File Upload {}", movieFile);
}
#PostMapping(value="/movies-upload", consumes = {"application/json"})
public void postMoviesJson( #RequestPart String movieJson) {
LOGGER.info("Movies Json Body {}", movieJson);
}

API call with Java + STS returning "Content type 'application/octet-stream' not supported"

I am working on part of an API, which requires making a call to another external API to retrieve data for one of its functions. The call was returning an HTTP 500 error, with description "Content type 'application/octet-stream' not supported." The call is expected to return a type of 'application/json."
I found that this is because the response received doesn't explicitly specify a content type in its header, even though its content is formatted as JSON, so my API defaulted to assuming it was an octet stream.
The problem is, I'm not sure how to adjust for this. How would I get my API to treat the data it receives from the other API as an application/json even if the other API doesn't specify a content type? Changing the other API to include a contenttype attribute in its response is infeasible.
Code:
The API class:
#RestController
#RequestMapping(path={Constants.API_DISPATCH_PROFILE_CONTEXT_PATH},produces = {MediaType.APPLICATION_JSON_VALUE})
public class GetProfileApi {
#Autowired
private GetProfile GetProfile;
#GetMapping(path = {"/{id}"})
public Mono<GetProfileResponse> getProfile(#Valid #PathVariable String id){
return GetProfile.getDispatchProfile(id);
}
The service calling the external API:
#Autowired
private RestClient restClient;
#Value("${dispatch.api.get_profile}")
private String getDispatchProfileUrl;
#Override
public Mono<GetProfileResponse> getDispatchProfile(String id) {
return Mono.just(id)
.flatMap(aLong -> {
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return restClient.get(getDispatchProfileUrl, headers);
}).flatMap(clientResponse -> {
HttpStatus status = clientResponse.statusCode();
log.info("HTTP Status : {}", status.value());
return clientResponse.bodyToMono(GetProfileClientResponse.class);
// the code does not get past the above line before returning the error
}).map(GetProfileClientResponse -> {
log.debug("Response : {}",GetProfileClientResponse);
String id = GetProfileClientResponse.getId();
log.info("SubscriberResponse Code : {}",id);
return GetProfileResponse.builder()
// builder call to be completed later
.build();
});
}
The GET method for the RestClient:
public <T> Mono<ClientResponse> get(String baseURL, MultiValueMap<String,String> headers){
log.info("Executing REST GET method for URL : {}",baseURL);
WebClient client = WebClient.builder()
.baseUrl(baseURL)
.defaultHeaders(httpHeaders -> httpHeaders.addAll(headers))
.build();
return client.get()
.exchange();
}
One solution I had attempted was setting produces= {MediaType.APPLICATION_JSON_VALUE} in the #RequestMapping of the API to produces= {MediaType.APPLICATION_OCTET_STREAM_VALUE}, but this caused a different error, HTTP 406 Not Acceptable. I found that the server could not give the client the data in a representation that was requested, but I could not figure out how to correct it.
How would I be able to treat the response as JSON successfully even though it does not come with a content type?
Hopefully I have framed my question well enough, I've kinda been thrust into this and I'm still trying to figure out what's going on.
Are u using jackson library or jaxb library for marshalling/unmarshalling?
Try annotating Mono entity class with #XmlRootElement and see what happens.

Spring Integration application with external Web Services monitoring

Currently I have an application with Spring Integration DSL that has AMQP inbound gateway with different service activators, each service activator has kind of logic to decide, transform and call external web services (currently with CXF), but all this logic is in code without Spring Integration components.
These service activators are monitored, in the output channel that returns data from this application is an AMQP adapter that sends headers to a queue (after that, all headers are processed and saved in a database for future analysis). This works well, these service activators even have elapsed time in headers.
Now, the problem is, that I need to monitor external web service calls, like elapsed time in each operation, which service endpoint and operation was called, if an error occurred.
I've been thinking that logic code in each service activator should be converted into a Spring Integration flow, in each service activator, would call a new gateway with the name of the operation of the web service in a header, and monitoring every flow as currently I had been doing.
So, I'm not sure if this manual approach is the better approach, so I wonder if there is a way to get the name of the service operation with some kind of interceptor or something similar with CXF or Spring WS to avoid setting the name of the operation in headers in a manual way? What would be your recommendation?
To have more context here is the Spring Integration configuration:
#Bean
public IntegrationFlow inboundFlow() {
return IntegrationFlows.from(Amqp.inboundGateway(simpleMessageListenerContainer())
.mappedReplyHeaders(AMQPConstants.AMQP_CUSTOM_HEADER_FIELD_NAME_MATCH_PATTERN)
.mappedRequestHeaders(AMQPConstants.AMQP_CUSTOM_HEADER_FIELD_NAME_MATCH_PATTERN)
.errorChannel(gatewayErrorChannel())
.requestChannel(gatewayRequestChannel())
.replyChannel(gatewayResponseChannel())
)
.enrichHeaders(new Consumer<HeaderEnricherSpec>() {
#Override
public void accept(HeaderEnricherSpec t) {
t.headerExpression(AMQPConstants.START_TIMESTAMP, "T(java.lang.System).currentTimeMillis()");
}
})
.transform(getCustomFromJsonTransformer())
.route(new HeaderValueRouter(AMQPConstants.OPERATION_ROUTING_KEY))
.get();
}
#Bean
public MessageChannel gatewayRequestChannel() {
return MessageChannels.publishSubscribe().get();
}
#Bean
public MessageChannel gatewayResponseChannel() {
return MessageChannels.publishSubscribe().get();
}
private IntegrationFlow loggerOutboundFlowTemplate(MessageChannel fromMessageChannel) {
return IntegrationFlows.from(fromMessageChannel)
.handle(Amqp.outboundAdapter(new RabbitTemplate(getConnectionFactory()))
.exchangeName(LOGGER_EXCHANGE_NAME)
.routingKey(LOGGER_EXCHANGE_ROUTING_KEY)
.mappedRequestHeaders("*"))
.get();
}
And here is a typical service activator, as you can see, all this logic could be an integration flow:
#ServiceActivator(inputChannel="myServiceActivator", outputChannel = ConfigurationBase.MAP_RESPONSE_CHANNEL_NAME)
public Message<Map<String, Object>> myServiceActivator(Map<String, Object> input, #Header(AMQPConstants.SESSION) UserSession session) throws MyException {
Message<Map<String, Object>> result = null;
Map<String, Object> mapReturn = null;
ExternalService port = serviceConnection.getExternalService();
try {
if (input.containsKey(MappingConstants.TYPE)) {
Request request = transformer
.transformRequest(input, session);
Response response = port
.getSomething(request);
utils.processBackendCommonErrors(response.getCode(), response.getResponse());
mapReturn = transformer.convertToMap(response);
} else {
Request request = transformer
.transformRequest(input, session);
Response response = port
.getSomethingElse(request);
utils.processBackendCommonErrors(response.getCode(),
response.getResponse());
mapReturn = transformer.convertToMap(response);
}
} catch (RuntimeException e) {
String message = "unexcepted exception from the back-end";
logger.warn(message, e);
throw MyException.generateTechnicalException(message, null, e);
}
result = MessageBuilder.withPayload(mapReturn)
.build();
return result;
}
So far so good. Or I don't understand the problem, or you are not clear where it is.
Anyway you always can proxy any Spring Service with the AOP, since it looks like you are pointing to the code:
Response response = port
.getSomething(request);
When this (or similar) method is called, some MethodInterceptor can perform desired tracing logic and send result to some MessageChannel for further analysis or anything else to do:
public Object invoke(MethodInvocation invocation) throws Throwable {
// Extract required operation name and start_date from the MethodInvocation
Object result = invocation.proceed();
// Extract required data from the response
// Build message and send to the channel
return result;
}

Categories