Validate request param names for optional fields - spring boot - spring

If you have an endpoint described like this :
#GetMapping("/hello")
public String sayHello(#RequestParam(name= "my_id", required=false) myId, #RequestParam(name= "my_name") myName){
// return something
}
Issue:
The consumers of this endpoint could send invalid param names in the request;
/hello?my_name=adam&a=1&b=2 and it will work normally.
Goal:
Be able to validate optional request parameters names, using a proper way to do it.
Actual solution:
I've implemented a solution (For me it's not the right solution), where I've extends a HandlerInterceptorAdapter and register it in the WebMvcConfigurer :
/**
*
* Interceptor for unknown request parameters
*
*/
public class MethodParamNamesHandler extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ArrayList<String> queryParams = Collections.list(request.getParameterNames());
MethodParameter[] methodParameters = ((HandlerMethod) handler).getMethodParameters();
Map<String, String> methodParametersMap = new HashMap<>();
if(Objects.nonNull(methodParameters)){
methodParametersMap = Arrays.stream(methodParameters)
.map(m -> m.getParameter().getName())
.collect(Collectors.toMap(Function.identity(),Function.identity()));
}
Set<String> unknownParameters = collectUnknownParameters(methodParametersMap, queryParams);
if(!CollectionUtils.isEmpty(unknownParameters)){
throw new InvalidParameterNameException("Invalid parameters names", unknownParameters);
}
return super.preHandle(request,response,handler);
}
/**
* Extract unknown properties from a list of parameters
* #param allParams
* #param queryParam
* #return
*/
private Set<String> collectUnknownParameters(Map<String, String> allParams, List<String> queryParam){
return queryParam.stream()
.filter(param -> !allParams.containsKey(param))
.collect(Collectors.toSet());
}
}
Drawbacks:
The drawbacks of this solution is that it's based on method parameters,
It will take the name myId instead of taking my_id, this could have a workaround by transforming the uppercase word to snake-case; but this is not a good solution because you can have something like this : sayHello(#RequestParam(name= "my_id", required=false) anotherName).
Question:
Is there a proper way to achieve this ?

Related

Should someRestController be made for receiving lang parameter for i18n along with LocaleResolver?

I am developing Spring Boot application, and I need to work with i18n. I watched a lot of tutorials and I implemented new class LocaleConfiguration
#Configuration
public class LocaleConfiguration implements WebMvcConfigurer {
/**
* * #return default Locale set by the user
*/
#Bean(name = "localeResolver")
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
/**
* an interceptor bean that will switch to a new locale based on the value of the language parameter appended to a request:
*
* #param registry
* #language should be the name of the request param i.e localhost:8010/api/get-greeting?language=fr
* <p>
* Note: All requests to the backend needing Internationalization should have the "language" request param
*/
#Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
}
And also, I made few messages_code.propertie files with proper languages. I set thymeleaf template just to see if everything is working and that is okay. FrontEnd developer just need to send me lang param and that is it. But my question is, should I make a new controller which will handle that call with lang parameter or all that is somehow automatically done via this LocaleConfiguration class?
Because I get proper translations when I make this call in Postman/Browser:
http://localhost:8080/?lang=fra
So my question is, do I need to make new Controller to handle that or is it automatically done by LocaleResolver class?
I will answer your question first answer is LocaleResolver !
Because you have LocaleResolver Bean, and add localeChangeInterceptor, And its class hierarchy is
LocaleChangeInterceptor is an interceptor. It is known from the source code that it is executed before the request reaches RequestMapping. Its role is simply to obtain the request parameters from the request (the default is locale), and then set the current locale in LocaleResolver.
source code:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
//Note here
**String newLocale = request.getParameter(getParamName());**
if (newLocale != null) {
if (checkHttpMethod(request.getMethod())) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
}
catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
}
else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
See i have to comment.
String newLocale = request.getParameter(getParamName());
transfer
/**
* Return the name of the parameter that contains a locale specification
* in a locale change request.
*/
public String getParamName() {
return this.paramName;
}
among them this.paramName is
/**
* Default name of the locale specification parameter: "locale".
*/
public static final String DEFAULT_PARAM_NAME = "locale";
So do you understand

How to reject the request and send custom message if extra params present in Spring boot REST Api

#PostMapping()
public ResponseEntity<ApiResponse> createContact(
#RequestBody ContactRequest contactRequest) throws IOException {
}
How to reject the API request, if extra params present in the request, by default spring boot ignoring extra parameters.
I believe you can add an HttpServletRequest as a parameter to the controller method (createContact in this case). Then you'll get access to all the parameters that come with the requests (query params, headers, etc.):
#PostMapping
public ResponseEntity<ApiResponse> createContact(HttpServletRequest request,
#RequestBody ContactRequest contactRequest) throws IOException {
boolean isValid = ...// validate for extra parameters
if(!isValid) {
// "reject the request" as you call it...
}
}
First add an additional parameter to the method. This gives you access to information about the request. If Spring sees this parameter then it provides it.
#PostMapping()
public ResponseEntity<ApiResponse> createContact(
#RequestBody ContactRequest contactRequest,
WebRequest webRequest) throws IOException {
if (reportUnknownParameters(webRequest) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
I do something like this to get the bad request into the log.
private boolean reportUnknownParameters(WebRequest webRequest) {
LongAdder unknownCount = new LongAdder();
webRequest.getParameterMap().keySet().stream()
.filter(key -> !knownParameters.contains(key))
.forEach(key -> {
unknownCount.increment();
log.trace("unknown request parameter \"{}\"=\"{}\"", key, webRequest.getParameter(key));});
return unknownCount.longValue() > 0;
}
add #RequestParam annotation in your methods parameter list and add it as a map, then you can access for it's key list and check if it contains anything else other than your required params.
public ResponseEntity<ApiResponse> createContact(#RequestParam Map<String,String> requestParams, #RequestBody ContactRequest contactRequest) throws IOException {
//Check for requestParams maps keyList and then pass accordingly.
}

setExpectedResponseType() method in HttpRequestExecutingMessageHandler

Below is the configuration of HttpRequestExecutingMessageHandler
#ServiceActivator(inputChannel = "rtpRequestChannel")
#Bean
public MessageHandler httResponseMessageHandler(MessageChannel rtpResponseChannel) {
HttpRequestExecutingMessageHandler handler = new HttpRequestExecutingMessageHandler(
"http://localhost:8080/rtp");
handler.setHttpMethod(HttpMethod.POST);
handler.setOutputChannel(rtpResponseChannel);
handler.setShouldTrack(true);
handler.setStatsEnabled(true);
return handler;
}
Below is the POST method in the REST controller class:
#RequestMapping(value = "/rtp", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<RTPResponse> persistRTP(#RequestBody RTPRequest request) {
System.out.println("In post method " + request);
if (request != null) {
return new ResponseEntity<RTPResponse>(new RTPResponse("12:12:2017", "Test", null, "100", "100"), HttpStatus.OK);
}
return new ResponseEntity<RTPResponse>(new RTPResponse("12:12:2017", "Dummy", null, "Dummy", "Dummy"), HttpStatus.OK);
}
Below is the config of the service activator method:
#Override
#ServiceActivator(inputChannel="rtpResponseChannel")
public void makeCall(ResponseEntity<RTPResponse> message) {
System.out.println("Message: " + message.getBody());
System.out.println(message.getClass().getCanonicalName());
}
I am receiving null in the body of the ResponseEntity object. Which configuration am I missing?
Edit 1:
When I use the setExpectedResponseType(), with the same controller configuration as above.
#ServiceActivator(inputChannel = "rtpRequestPostOperationRequestChannel")
#Bean
public MessageHandler httResponseMessageHandler(MessageChannel rtpRequestPostOperationResponseChannel) {
HttpRequestExecutingMessageHandler handler = new HttpRequestExecutingMessageHandler(
"http://localhost:8080/rtp");
handler.setHttpMethod(HttpMethod.POST);
handler.setOutputChannel(rtpRequestPostOperationResponseChannel);
handler.setExpectedResponseType(RTPResponse.class);
return handler;
}
The RTPResponse object is not wrapped in the ResponseEntity.
I get the error as below:
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method makeCall(rtp.model.RTPResponse) cannot be found on rtp.RTPRequestServiceClient type
Edit 2:
In other words, what configuration should I use on the HttpRequestExecutingMessageHandler to get hold of the message object so that I have the extracted body in the message payload and all the headers to the MessageHeaders, including status.
I tried using GenericMessage being passed to the setExpectedResponseType method of HttpRequestExecutingMessageHandler class.
But it gave me the error as below which is understandable:
Can not construct instance of org.springframework.messaging.support.GenericMessage: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
But you said yourself - setExpectedResponseType().
You really miss exactly this configuration.
In that case the body of response entity is empty:
private class ResponseEntityResponseExtractor<T> implements ResponseExtractor<ResponseEntity<T>> {
#Nullable
private final HttpMessageConverterExtractor<T> delegate;
public ResponseEntityResponseExtractor(#Nullable Type responseType) {
if (responseType != null && Void.class != responseType) {
this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
}
else {
this.delegate = null;
}
}
#Override
public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
if (this.delegate != null) {
T body = this.delegate.extractData(response);
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
}
else {
return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
}
}
}
If you don't like to provide a Class<?> for that option, you can consider to use:
/**
* Specify the {#link Expression} to determine the type for the expected response
* The returned value of the expression could be an instance of {#link Class} or
* {#link String} representing a fully qualified class name.
* #param expectedResponseTypeExpression The expected response type expression.
* Also see {#link #setExpectedResponseType}
*/
public void setExpectedResponseTypeExpression(Expression expectedResponseTypeExpression) {
instead. In this case you really can resolve the target expected response type against a requestMessage and also get access to the whole BeanFactory for some other beans calls.

Spring RestRemplate postforobject with request parameter having integer value

I have a method in Spring rest service.
#RequestMapping(value = "test/process", method = RequestMethod.POST)
public #ResponseBody MyResponse processRequest(String RequestId, int count)
I am using Spring RestTemplate to call this service like this.
RestTemplate restTemplate = this.getRestTemplate();
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("RequestId", RequestId);
map.add("count", count);
restTemplate.postForObject(url, map,MyResponse.class);
When I try to invoke the client method I get the exception that no suitable HttpMessageConverter found for request type [java.lang.Integer]
org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [java.lang.Integer]
at org.springframework.http.converter.FormHttpMessageConverter.writePart(FormHttpMessageConverter.java:310)
at org.springframework.http.converter.FormHttpMessageConverter.writeParts(FormHttpMessageConverter.java:270)
at org.springframework.http.converter.FormHttpMessageConverter.writeMultipart(FormHttpMessageConverter.java:260)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:200)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:1)
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:596)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:444)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:409)
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:287)
I know one of the ways is to pass all the parameters as String. But I might need to pass complex data types as parameters later.
What is the ways to achieve this.
I have googled and some option seem to be writing my own converters. How should I start about solving this problem.
The root cause of this error is that by specifying an Integer in the LinkedMultiValueMap, the RestTemplate will take that to mean that your request is a multipart request. There is no HttpMessageConverter registered by default that can handle writing values of type Integer to a request body.
As you said, you can handle this situation by changing the count to be a String. After all, there is no Integer type in HTTP request parameters. However, you were worried
But I might need to pass complex data types as parameters later.
Assume something like this
public #ResponseBody MyResponse processRequest(String RequestId, int count, Complex complex) {
with
public class Complex {
private String someValue;
private int intValue;
public String getSomeValue() {
return someValue;
}
public void setSomeValue(String someValue) {
this.someValue = someValue;
}
public int getIntValue() {
return intValue;
}
public void setIntValue(int intValue) {
this.intValue = intValue;
}
public String toString() {
return someValue + " " + intValue;
}
}
The the following will work just fine
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("RequestId", "asd");
map.add("count", "42");
map.add("someValue", "complex");
map.add("intValue", "69");
restTemplate.postForObject(url, map,MyResponse.class);
Remember that the request parameters are used to populate the fields of model attributes by their names.
An even better solution would have you using a serialization standard like JSON or XML.

Jersey PreMatching Filter : Adding parameters to modified URI

I have a rest api. Eg :
http://localhost:8080/api/user/view?name=user&lastname=demo
I want to modify my URI to maintain version.
I want to add '/v1/' between '/user/view'
So that my URI will look like as follows :
http://localhost:8080/api/user/v1/view?name=user&lastname=demo
I am able to modify my URI and create a new one , but I am not understanding how to pass parameters to the modified URI.
The following is my code :
#Provider
#PreMatching
public class RewriteUrl implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
UriInfo uriInfo = requestContext.getUriInfo();
String path = uriInfo.getPath();
MultivaluedMap<String, String> parameters = uriInfo.getQueryParameters();
path=path.replaceFirst("/","/v1/");
URI baseUri = uriInfo.getBaseUriBuilder().path(path).build();
URI requestUri = uriInfo.getRequestUri();
requestContext.setRequestUri(URI.create(baseUri.toString()));
}
}
Using the above code I am getting the correct URI :
http://localhost:8080/api/user/v1/view
But I am not understanding how to pass the parameters to the new URI.
I also want to know that is this the right and secure way to do this ?Or I am doing wrong.
Please let me know if their is a better way to do this.(adding 'v1' in the URI).
The following I found while debugging :
PRE - MATCHING FILTER
uriInfo.getRequestUri().toString() http://localhost:8080/api/user/view?name=user&lastname=demo
uriInfo.getAbsolutePath().toString() http://localhost:8080/api/user/view
uriInfo.getBaseUri().toString() http://localhost:8080/api/
uriInfo.getPath().toString() user/view
parameters.toString() {lastname=[demo], name=[user]}
POST FILTER
uriInfo.getRequestUri().toString() http://localhost:8080/api/user/v1/view
uriInfo.getAbsolutePath().toString() http://localhost:8080/api/user/v1/view
uriInfo.getBaseUri().toString() http://localhost:8080/api/
uriInfo.getPath().toString() user/v1/view
parameters.toString() {}
Not sure if still relevant, I also had to implement this recently.
Here is my solution:
#Provider
#PreMatching
public class RewriteUrl implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws
IOException {
UriInfo uriInfo = requestContext.getUriInfo();
String path = uriInfo.getPath();
path = path.replaceFirst("/","/v1/");
UriBuilder uriBuilder = UriBuilder
.fromUri(uriInfo.getBaseUri())
.path(path)
.replaceQuery(uriInfo.getRequestUri().getQuery());
requestContext.setRequestUri(uriBuilder.build());
}
}

Resources