Java Spring - how to handle missing required request parameters - java

Consider the following mapping:
#RequestMapping(value = "/superDuperPage", method = RequestMethod.GET)
public String superDuperPage(#RequestParam(value = "someParameter", required = true) String parameter)
{
return "somePage";
}
I want to handle the missing parameter case by not adding in required = false. By default, 400 error is returned, but I want to return, let's say, a different page. How can I achieve this?

If a required #RequestParam is not present in the request, Spring will throw a MissingServletRequestParameterException exception. You can define an #ExceptionHandler in the same controller or in a #ControllerAdvice to handle that exception:
#ExceptionHandler(MissingServletRequestParameterException.class)
public void handleMissingParams(MissingServletRequestParameterException ex) {
String name = ex.getParameterName();
System.out.println(name + " parameter is missing");
// Actual exception handling
}
I want to return let's say a different page. How to I achieve this?
As the Spring documentation states:
Much like standard controller methods annotated with a #RequestMapping
annotation, the method arguments and return values of
#ExceptionHandler methods can be flexible. For example, the
HttpServletRequest can be accessed in Servlet environments and the
PortletRequest in Portlet environments. The return type can be a
String, which is interpreted as a view name, a ModelAndView object, a
ResponseEntity, or you can also add the #ResponseBody to have the
method return value converted with message converters and written to
the response stream.

An alternative
If you use the #ControllerAdvice on your class and if it extends the Spring base class ResponseEntityExceptionHandler. A pre-defined function has been created on the base class for this purpose. You have to override it in your handler.
#Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String name = ex.getParameterName();
logger.error(name + " parameter is missing");
return super.handleMissingServletRequestParameter(ex, headers, status, request);
}
This base class is very useful, especially if you want to process the validation errors that the framework creates.

You can do this with Spring 4.1 onwards and Java 8 by leveraging the Optional type. In your example that would mean your #RequestParam String will have now type of Optional<String>.
Take a look at this article for an example showcasing this feature.

Maybe not that relevant, but I came across to a similar need: change the 5xx error to 4xx error for authentication header missing.
The controller is as follows:
#RequestMapping("list")
public ResponseEntity<Object> queryXXX(#RequestHeader(value = "Authorization") String token) {
...
}
When you cURL it without the authorization header you get a 5xx error:
curl --head -X GET "http://localhost:8081/list?xxx=yyy" -H "accept: */*"
HTTP/1.1 500
...
To change it to 401 you can
#ExceptionHandler(org.springframework.web.bind.MissingRequestHeaderException.class)
#ResponseBody
public ResponseEntity<Object> authMissing(org.springframework.web.bind.MissingRequestHeaderException ex) {
log.error(ex.getMessage(), ex);
return IResponse.builder().code(401).message(ex.getMessage()).data(null).build();
}
#Data
public class IResponse<T> implements Serializable {
private Integer code;
private String message = "";
private T data;
...
}
You can verify it by an automation test:
#Test
void testQueryEventListWithoutAuthentication() throws Exception {
val request = get("/list?enrollEndTime=1619176774&enrollStartTime=1619176774&eventEndTime=1619176774&eventStartTime=1619176774");
mockMvc.perform(request).andExpect(status().is4xxClientError());
}

Related

How can I redirect URL in case of exception or invalid input with Sping?

I have a GET method inside a controller that get a lot of parameters. I want to redirect the URL in case of exception or invalid input and return (print to the client) the exception with addition message.
My function looks like this:
#GetMapping("/v1.0/hello/{userName}")
public ClientResponse getDetails(#PathVariable(value = "userName") String userName,
#RequestParam(value = "expInp1", required = false) int expInp1,
#RequestParam(value = "expInp2", required = false) int expInp2,
#RequestParam(value = "expInp3", required = false) int expInp3){
// do something...
return clientResponse;
}
ClientResponse is an object that contain all the relevant details and can't be changed.
For example, if someone inserts the following URL /v1.0/hello/{userName}?expInp4=XXX, I want to redirect them to /v1.0/hello/ with a suitable message.
Is there a Spring annotation doing that without a lot of code? And if not, what is the best way in my case?
You can take a look at #RestControllerAdvice coupled with #ExceptionHandler annotation.
you can follow these steps to create a redirect system:
create your own exception
public class RedirectException extends RuntimeException{
private String redirectUrl;
public RedirectException(String message, String rUrl) {
super(message);
this.redirectUrl = rUrl;
}
}
create your controller advice
#RestControllerAdvice
public class ErrorController {
#ExceptionHandler(RedirectExecption.class)
public void handleRedirect(RedirectException re, HttpServletResponse res) {
res.sendRedirect(re.getRedirectUrl);
}
}
when you want to send a redirect response in you running code just throw a redirectException with the redirecturl you want to reach
p.s. you can use the same mechanism for building an error handling system.
create a new class with #RestControllerAdvice or #ControllerAdvice annotation to handle the exception globally in Spring Boot.
Refer this link

How to send a 401 as unauthorized user and a 404 if a database query returned a null object in a Spring boot webapp REST API controller?

I'm using a bit of a personalized security back-end due to the nature of the app and was trying out how to implement a few simple error returns in my REST API controller. It's simple enough to do in a html page controller like I have in the following:
#Controller
public class HomeController {
#Autowired
private UserService userService;
#GetMapping("/home.html")
public String home(Model model) {
String redirect = "home";
if(!userService.getCurrentUser().isCanAccessService()) {
redirect = "unauthorized";
}
return redirect;
}
}
I can easily just redirect it to the unauthorized page that I made since I'm returning the string value here. However, when I go to a REST API it's not as simple:
#RestController
public class bagelController {
#Autowired
private bagelService bagelService;
#Autowired
private UserService userService;
#GetMapping("/rest/bagel/search")
public Bagel searchBagel (#RequestParam(value = "bagel", required = false) String bagel,
#RequestParam(value = "bagelInd", required = false, defaultValue = "1") int bagelInd) {
Bagel bagel;
if(!userService.getCurrentUser().isBagelEditAccess()) {
bagel = null;
// I want to return a 401 or direct to my unathorized page if I get an invalid user here.
}
else {
bagel = bagelService.getbagel(bagel, bagelInd);
// if my bagel object returns null, I want to return a 404 or direct to a 404 not
found page here.
}
return bagel;
}
You can have a ControllerAdvice which handles exceptions and their HTTP return code. Then you can annotate a method in it the following way for example:
#ExceptionHandler(NoSuchEntityException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
This will return a 404 code every time it encounters a NoSuchEntityException (custom exception). So you can throw such an exception when you check if an entity is null. You can use the same thing for 401 or any other HTTP code as well.
One way to do this.
#GetMapping("/rest/bagel/search")
public ResponseEntity<Bagel> searchBagel (#RequestParam(value = "bagel", required = false) String bagel,
#RequestParam(value = "bagelInd", required = false, defaultValue = "1") int bagelInd) {
Bagel bagel = null;
if(!userService.getCurrentUser().isBagelEditAccess()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
else {
bagel = bagelService.getbagel(bagel, bagelInd);
if(bagel == null) {
return ResponseEntity.notFound().build();
}
}
return ResponseEntity.ok(bagel);
}
You can create custom exceptions within your application for this scenario like BagelNotFoundException and UnauthorizedException. Both these custom exception classes can extend Exception class or more specific classes from java exception hierarchy. You can annotate these custom exception classes with #ResponseStatus annotation to provide the http status code that should be sent in the response.
Next, you need to throw the objects of these exceptions within your controller.
Once this exception is thrown, an exception handler should be present within your application to take care of these exceptions. The same can be defined using #ControllerAdvice and #ExceptionHandler within your custom exception handler classes.
This way you'll be able to send appropriate response to the client, and the client application needs to redirect the user to error pages based on the response code received.
Hope this helps!

Access BindingResult of Spring WebDataBinder without putting in the argument list

I am trying to use Spring validation with a controller interface generated by swagger-codegen. The swagger code generation supplies an abstract class for a controller. Our controller implements the codegen class and provides the actual logic. I would like to access the BindingResult in my controller methods, but swagger-codegen does not generate that parameter in its interface. Is there any way to get ahold of the BindingResults object other than specifying it as a parameter?
To make this more concrete, the codegen makes the endpoint like this (noisy code removed):
#RequestMapping(value = "/api/repository/v1/datasets",
produces = { "application/json" },
consumes = { "application/json" },
method = RequestMethod.POST)
default ResponseEntity<JobModel> createDataset(#Valid #RequestBody DatasetRequestModel dataset) {
...
}
We implement a controller with the usual binder setup like:
#InitBinder
protected void initBinder(final WebDataBinder binder) {
binder.addValidators(requestValidator)
}
but within the endpoint, we have no way to get the BindingResult since it has to match the signature of the codegen entry:
public ResponseEntity<StudySummaryModel> createStudy(#Valid #RequestBody StudyRequestModel studyRequest) {
...
}
I think the most straightforward solution may be to skip using WebDataBinder. Instead, I can have each controller endpoint call validators directly.
I found another approach besides hand coding the validation; using an #ControllerAdvice class that extends ResponseEntityExceptionHandler.
There is a nice example here: Spring Validation Example
Here is my code based on that example that formats the error:
#ControllerAdvice
public class ApiValidationExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request
) {
BindingResult bindingResult = ex.getBindingResult();
List<String> errorDetails = bindingResult
.getFieldErrors()
.stream()
.map(err -> err.getCode() + " error on '" + err.getObjectName() + "': " + err.getDefaultMessage())
.collect(toList());
ErrorModel errorModel = new ErrorModel()
.message("Validation errors - see error details")
.errorDetail(errorDetails);
return new ResponseEntity<>(errorModel, HttpStatus.BAD_REQUEST);
}
}

Determine acceptable content types in ExceptionHandler

In a Spring application, I have an endpoint which normally returns an image (produces = MediaType.IMAGE_PNG_VALUE).
I also have #ExceptionHandler functions to handle various functions.
I'm trying to find a way to determine, from within the #ExceptionHandler, if the client will accept text/plain or text/json so in the event of an error I can return back one of those, or omit it if they are only expecting image/png.
How can I determine what acceptable content types I can return for a given request?
You can access the request to inspect headers and return an appropriate response. It is standard Content Negotiation.
Here's an example:
#ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {RuntimeException.class})
protected ResponseEntity<Object> handleMyException(RuntimeException ex, WebRequest request) {
List<String> acceptableMimeTypes = Arrays.asList(request.getHeaderValues(HttpHeaders.ACCEPT));
if (acceptableMimeTypes.contains(MediaType.TEXT_PLAIN_VALUE)) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.body("hello");
}
throw ex;
}
}
There are some arguments that spring-mvc can automagically inject into controller methods, and WebRequest (which is spring's representation of an http request) is one of those. If the client has sent an Accept : text/plain header with the request, the above example returns the string hello if there's a RuntimeException. If there's no exception, this logic won't get triggered at all, so the endpoint will just return whatever it normally returns. You can read more about #ControllerAdvice and #ExceptionHandler here.
Of course, be sure to think about the exact exception types you want to handle, and semantically appropriate status codes to return so that the clients know how to correctly interpret the response.
This is the answer I came up with. It's similar to YoungSpice's, but it is a little more flexible and uses MediaType directly (which means it'll handle wildcard types like text/* and the like):
private ResponseEntity<String> buildResponse(WebRequest request, HttpStatus status, String message) {
HttpHeaders httpHeader = new HttpHeaders();
List<MediaType> acceptHeader =
MediaType.parseMediaTypes(Arrays.asList(request.getHeaderValues(HttpHeaders.ACCEPT)));
if (acceptHeader.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.APPLICATION_JSON))) {
httpHeader.setContentType(MediaType.APPLICATION_JSON);
return new ResponseEntity<>("{ \"error\": \"" + message + "\" }", httpHeader, status);
} else if (acceptHeader.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.TEXT_PLAIN))) {
httpHeader.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>(message, httpHeader, status);
} else {
return ResponseEntity.status(status).body(null);
}
}
Basically, it uses MediaType.parseMediaTypes() to parse the Accept header, then I stream through them and use the mediaType.isCompatibleWith() function to check if my target is acceptable. This will let it handle if the header has something like application/* instead of application/json directly.
It also seems like if Accept isn't explicitly provided in the request, there is an implied */*, which seems to work as intended.

Spring MVC - how to return 404 on missing parameters

I want to trigger 404 page whenever I wasn't passed all of the parameters. Lets say I have the following URI:
/myapp/op?param1=1&param2=2#param3=3
In case on of the parameters wasn;t invoked I want to return 404 page. I tried doing:
#ResponseStatus(HttpStatus.NOT_FOUND)
#RequestMapping(value = "op", params = { "!param1" })
public void missingArg() {
}
but then I get an exception telling me there is ambiguity between methods that handle missing second and third parameter.
How can I accomplish this, then?
If you're using Spring 3.1 you can define an exception class like so:
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public final class ResourceNotFoundException extends RuntimeException {
// class definition
}
Now whenever you throw that exception, Spring will return the http status defined in your #ResponseStatus annotation. For example:
#RequestMapping(value = "/op")
public void methodWithRequestParams(#RequestParam(value = "param1", required = false) String param1,
#RequestParam(value = "param2", required = false) String param2) {
if (param1 == null || param2 == null) {
throw new ResourceNotFoundException();
}
}
will return a 404 whenever param1 or param2 is null.
You do not have to implement the missingArg() function. If there is no matching method for the incoming request, then Spring's HandlerExceptionResolver will handle it and return a response with an appropriate status code.
Spring will automatically convert the request parameters into method parameters if you use the #RequestParam annotation:
#RequestMapping(value = "/op")
public void methodWithRequestParams(#RequestParam("param1") String param1,
#RequestParam("param2") String param2,
#RequestParam("param3") String param3) {
// do something with params
}
By convention, the methodWithRequestParams() method will not be called if not all params are part of the request (unless the required attribute of the #RequestParam is set to false).
Also note that the parameters does not have to be Strings.
Echoing what matsev said in the comments of another answer, you should not be using #ResponseStatus(HttpStatus.NOT_FOUND) in this case, but rather #ResponseStatus(HttpStatus.BAD_REQUEST).
#ResponseStatus(HttpStatus.NOT_FOUND) should be used when the request was formed properly, but the resource isn't there.

Categories