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.
Related
I have a Spring-Boot controller application that will be called by the front-end. The Spring-boot #PostMapping would accept the XML and JSON. I want to call different methods based on the Content-Type.
Is there a way to check what is the incoming content type?
#CrossOrigin(origins = "*")
#RestController
#RequestMapping("/api")
public class MyController {
#PostMapping(value = "/generator", consumes = {"application/json", "application/xml"}, produces = "application/json")
public String generate(#RequestBody String input) {
try {
System.out.println("INPUT CONTENT TYPE : ");
if(contentType == "application/xml")
{
//Call Method-1
}else if(contentType == "application/json"){
//Call Method-2
}
} catch (Exception exception) {
System.out.println(exception.getMessage());
}
}
}
As we can see the RestController method accepts XML and JSON. I want to check whats the incoming Content-type is based on its need to make different decisions. Can someone please explain to me how to do it?
Please Note:
I am aware that I can create different methods to handle XML and JSON but I would like to do it in a single method so it would be easy and efficient.
Add RequestHeader with its name Content-type:
public String generate(#RequestBody String input, #RequestHeader("Content-type") String contentType)
Annotation which indicates that a method parameter should be bound to a web request header.
You can use
#RequestHeader Map<String, String> headers
inside param of your generate() methode for get all Header come from the client.
After that, just check the
Content-Type
value
A question about how to write a contract for a method annotated with #RequestBody taking a Collection of Strings as a parameter.
I have the following method:
#PostMapping(path = "/some/uri", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
#ApiOperation("GET with body")
public Response<Boolean> someMethod(#RequestParam(value = "key") final String key,
#RequestBody final Collection<String> numbers){
return some logic;
}
and I have written the following contract for testing purposes:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return true"
request {
method POST()
url("/some/uri?key=NEW_KEY")
body'''["12345",
"00143"]'''
}
response {
status 200
headers {header 'Content-Type': 'application/json;charset=UTF-8'}
body '''true'''
}
I keep getting 415, the test cannot find my method, I guess my mistake might be in the way I send the collection of strings, I have tried some other options but did not succed.
I tried the suggestions above but unfortunately they both did not solve my issue. The reason I got 415 was that when I added a body to my request, the check was also made behind the scenes on the content type of the body, so I had to explicitly specify that the body was in json format also in the request:
request {
method POST()
url("/some/uri?key=NEW_KEY")
headers {header 'Content-Type': 'application/json;charset=UTF-8'}
body'''["12345",
"00143"]'''
}
I have a Spring-Boot (v2.0.2) application with a RestController with 2 methods which only differ by the Accept header. A simplified version of the code is this:
#RestController
#RequestMapping("/myapp")
public class FooController {
#GetMapping(value = "/foo/{id}", headers = "Accept=application/json", produces = "application/json;charset=UTF-8")
public ResponseEntity<String> fooJson(#PathVariable id) {
return foo(pageId, true);
}
#GetMapping(value = "/foo/{id}", headers = "Accept=application/ld+json", produces = "application/ld+json;charset=UTF-8")
public ResponseEntity<String> fooJsonLd(#PathVariable id) {
return foo(pageId, false);
}
private ResponseEntity<String> foo(String id, boolean isJson) {
String result = generateBasicResponse(id);
if (isJson) {
return result
}
return addJsonLdContext(result);
}
This works fine. If we sent a request with accept header such as application/json;q=0.5,application/ld+json;q=0.6 for example it will return a json-ld response as it should.
My problem is that if we sent a request with no accept header, an empty accept header or a wildcard */* then it will by default always return a json response whereas I want the default response to be json-ld.
I've tried various things to make the json-ld request mapping take priority over the json one:
Reversing the order in which the mappings are declared.
Adding an #Order annotation to both methods (with value 1 for json-ld and value 2 for the json method)
Creating different classes and putting the #Order annotation at class-level
Adding Accept=*/* as a second accept header to the json-ld mapping does work in giving it preference but has the unwanted side-affect that all accept headers are accepted, even unsupported types as application/xml for example.
The only solution I can think of is creating one request-mapping method that accepts both headers and then processing the accept header ourselves, but I don't really like that solution. Is there a better, easier way to give preference to json-ld?
After some more searching this question on configuring custom MediaTypes pointed me in the right direction.
The WebMvcConfigurerAdapter (Spring 3 or 4) or WebMvcConfigurer (Spring 5) allows you to set a default mediatype like this:
public static final String MEDIA_TYPE_JSONLD = "application/ld+json";
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.valueOf(MEDIA_TYPE_JSONLD));
}
}
This works great for requests with no or an empty accept header, as well as accept: */*. However when you combine an unsupported type with the wildcard, for example accept: */*,text/plain it will return json instead of json-ld!? I suspect this is a bug in Spring.
I solved the issue using the consumes in the #GetMapping annotation. According to the official documentation:
The format is a single media type or a sequence of media types, with a request only mapped if the Content-Type matches one of these media types. Expressions can be negated by using the "!" operator, as in "!text/plain", which matches all requests with a Content-Type other than "text/plain".
In the solution bellow, note that I've added the consumes array to the normal json request mapping, making the client only be able to use the json endpoint if it have the correct Content-Type. Other requests go to the ld+json endpoint.
#GetMapping(value = "/json", headers = "Accept=application/json", consumes = {"application/json"})
#ResponseBody
public String testJson() {
return "{\"type\":\"json\"}";
}
#GetMapping(value = "/json", headers = "Accept=application/ld+json")
#ResponseBody
public String textLDJson() {
return "{\"type\":\"ld\"}";
}
How can I validate the accept headers in spring and return a custom message if the accept header is not application/json. Currently I am doing it this way(below) but I am wondering if there is another way of doing this?
I know I can make custom exceptions and throw the exception based on what went wrong but is there a different way of doing this?
#GetMapping(value = "/get", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
public ResponseEntity<String> get(final HttpEntity<String> httpEntity) {
HttpHeaders responseHeaders = new HttpHeaders();
String acceptString = httpEntity.getHeaders().getFirst("Accept");
if (acceptString == null || acceptString.isEmpty()) {
return new ResponseEntity<String>("empty accept header", HttpStatus.BAD_REQUEST);
}
if ((MediaType.APPLICATION_JSON_VALUE).equalsIgnoreCase(acceptString)) {
responseHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
}
else {
return new ResponseEntity<String>("Not valid accept value", HttpStatus.BAD_REQUEST);
}
....
Within your #Getmapping you can add headers="Accept=application/json" this way your method will only handle calls with that header type.
You can then add a method which processes all the calls that are ignored by your specific AcceptHeader method. This way you only need one method to validate the headers.
You can also write a custom validator and use anotation to add this to your method like here:
conditional validation
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());
}