Setting a default content-type header while using #RequestBody - spring

When using #Requestbody, I am not able to run the API without sending a content-type header with a value of application/json. I want a method to set this header by default whenever the controller is called.
I don't want to use HttpRequest. I have tried to set the produces and consumes parameter in #RequestMapping, but in vain. I noticed that without the header the user is not able to hit the API.
#ResponseBody
#PostMapping(value = "/1.0/{productType}/itinerary/create)
protected ResponseEntity<String> createIternary(#PathVariable final String productType,
#RequestParam(name = "product-id", required = false, defaultValue = "-1") String productIdStr,
#RequestParam Map<String, String> searchRequest)
throws Exception {}
{
"timestamp": 1568117108917,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain' not supported",
"path": "/local/1.0/ttd/itinerary/create"
}
//This is the error in Postman
{
"timestamp":1568116888056,
"status":415,
"error":"Unsupported Media Type",
"exception":
"org.springframework.web.HttpMediaTypeNotSupportedException",
"message":"Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported",
"path":"/local/1.0/ttd/itinerary/create"
}
//This is the error in terminal

Spring dependends on Content-Type header to correctly handle the request.
If you want to customize it, you need to do it yourself, by writing some sort of request wrapper and / or a filter.
UPDATE
You'll need to create a class which extends from HttpServletRequestWrapper and a filter.
The HttpServletRequestWrapper can be used exactly for that. Sometimes you want to adjust the original request. With this wrapper, you can wrap the original request and override some methods to modify it's behaviour.
ContentTypeRequestWrapper.java
public class ContentTypeRequestWrapper extends HttpServletRequestWrapper {
public ContentTypeRequestWrapper(HttpServletRequest request) {
super(request);
}
#Override
public String getContentType() {
return "application/json";
}
#Override
public String getHeader(String name) {
if (name.equalsIgnoreCase("content-type")) {
return "application/json";
}
return super.getHeader(name);
}
#Override
public Enumeration <String> getHeaderNames() {
List <String> headerNames = Collections.list(super.getHeaderNames());
if (!headerNames.contains("content-type")) {
headerNames.add("content-type");
return Collections.enumeration(headerNames);
}
return super.getHeaderNames();
}
#Override
public Enumeration <String> getHeaders(String name) {
if (name.equalsIgnoreCase("content-type")) {
return Collections.enumeration(Collections.singletonList("application/json"));
}
return super.getHeaders(name);
}
}
ForcedApplicationJsonContentTypeFilter.java
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ForcedApplicationJsonContentTypeFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException,
ServletException {
chain.doFilter(new ContentTypeRequestWrapper((HttpServletRequest) req), resp);
}
}
Of course this is not the best way to do so. The way I wrote it, you're assuming that 100% of the incoming request to your API are in JSON format.
You'll probably need to adapt the ContentTypeRequestWrapper to do some validations like 'is Content-Type already set? Then don't override'.

The #PostRequest annotation has both a consumes and produces property. In this case you would want to use the produces property:
#PostMapping(value = "/1.0/{productType}/itinerary/create", produces = "application/json")

Related

How to rewrite URLs with Spring (Boot) via REST Controllers?

Let's say I have the following controller with its parent class:
#RestController
public class BusinessController extends RootController {
#GetMapping(value = "users", produces = {"application/json"})
#ResponseBody
public String users() {
return "{ \"users\": [] }"
}
#GetMapping(value = "companies", produces = {"application/json"})
#ResponseBody
public String companies() {
return "{ \"companies\": [] }"
}
}
#RestController
#RequestMapping(path = "api")
public class RootController {
}
Data is retrieved by calling such URL's:
http://app.company.com/api/users
http://app.company.com/api/companies
Now let's say I want to rename the /api path to /rest but keep it "available" by returning a 301 HTTP status code alongside the new URI's
e.g. client request:
GET /api/users HTTP/1.1
Host: app.company.com
server request:
HTTP/1.1 301 Moved Permanently
Location: http://app.company.com/rest/users
So I plan to change from "api" to "rest" in my parent controller:
#RestController
#RequestMapping(path = "rest")
public class RootController {
}
then introduce a "legacy" controller:
#RestController
#RequestMapping(path = "api")
public class LegacyRootController {
}
but now how to make it "rewrite" the "legacy" URI's?
That's what I'm struggling with, I can't find anything Spring-related on the matter, whether on StackOverflow or elsewhere.
Also I have many controllers AND many methods-endpoints so I can not do this manually (i.e. by editing every #RequestMapping/#GetMapping annotations).
And project I'm working on is based on Spring Boot 2.1
Edit: I removed the /business path because actually inheritance doesn't work "by default" (see questions & answers like Spring MVC #RequestMapping Inheritance or Modifying #RequestMappings on startup ) - sorry for that.
I finally found a way to implement this, both as a javax.servlet.Filter AND a org.springframework.web.server.WebFilter implementation.
In fact, I introduced the Adapter pattern in order to transform both:
org.springframework.http.server.ServletServerHttpResponse (non-reactive) and
org.springframework.http.server.reactive.ServerHttpResponse (reactive)
because on the contrary of the Spring's HTTP requests' wrappers which share org.springframework.http.HttpRequest (letting me access both URI and HttpHeaders), the responses's wrappers do not share a common interface that does it, so I had to emulate one (here purposely named in a similar fashion, HttpResponse).
#Component
public class RestRedirectWebFilter implements Filter, WebFilter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
ServletServerHttpRequest request = new ServletServerHttpRequest((HttpServletRequest) servletRequest);
ServletServerHttpResponse response = new ServletServerHttpResponse((HttpServletResponse) servletResponse);
if (actualFilter(request, adapt(response))) {
chain.doFilter(servletRequest, servletResponse);
}
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (actualFilter(exchange.getRequest(), adapt(exchange.getResponse()))) {
return chain.filter(exchange);
} else {
return Mono.empty();
}
}
/**
* Actual filtering.
*
* #param request
* #param response
* #return boolean flag specifying if filter chaining should continue.
*/
private boolean actualFilter(HttpRequest request, HttpResponse response) {
URI uri = request.getURI();
String path = uri.getPath();
if (path.startsWith("/api/")) {
String newPath = path.replaceFirst("/api/", "/rest/");
URI location = UriComponentsBuilder.fromUri(uri).replacePath(newPath).build().toUri();
response.getHeaders().setLocation(location);
response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
response.flush();
return false;
}
return true;
}
interface HttpResponse extends HttpMessage {
void setStatusCode(HttpStatus status);
void flush();
}
private HttpResponse adapt(ServletServerHttpResponse response) {
return new HttpResponse() {
public HttpHeaders getHeaders() {
return response.getHeaders();
}
public void setStatusCode(HttpStatus status) {
response.setStatusCode(status);
}
public void flush() {
response.close();
}
};
}
private HttpResponse adapt(org.springframework.http.server.reactive.ServerHttpResponse response) {
return new HttpResponse() {
public HttpHeaders getHeaders() {
return response.getHeaders();
}
public void setStatusCode(HttpStatus status) {
response.setStatusCode(status);
}
public void flush() {
response.setComplete();
}
};
}
}
Since it looks like you want to preserve the 301 but also have it return a response, you do have the option to wire in your RootController into your LegacyRootController
That way you can provide reuse the logic you have in the RootController but return different response codes and serve different paths on your LegacyRootController
#RestController
#RequestMapping(path = "api")
public class LegacyRootController {
private final RootController rootController;
public LegacyRootController(RootController rootController) {
this.rootController = rootController;
}
#GetMapping(value = "users", produces = {"application/json"})
#ResponseStatus(HttpStatus.MOVED_PERMANENTLY) // Respond 301
#ResponseBody
public String users() {
return rootController.users(); // Use rootController to provide appropriate response.
}
#GetMapping(value = "companies", produces = {"application/json"})
#ResponseStatus(HttpStatus.MOVED_PERMANENTLY)
#ResponseBody
public String companies() {
return rootController.companies();
}
}
This will allow you to serve /api/users to serve up a response with a 301, while also allowing you to serve /rest/users with your standard response.
If you would like to add the Location headers, you can have your LegacyRootController return a ResponseEntity to provide the body, code and header values.
#GetMapping(value = "users", produces = {"application/json"})
public ResponseEntity<String> users() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation("...");
return new ResponseEntity<String>(rootController.users(), responseHeaders, HttpStatus.MOVED_PERMANENTLY);
}
If you want to serve multiple endpoints that does not serve different status codes, you can simply provide multiple paths
#RequestMapping(path = {"api", "rest"})

How to by default execute the latest version of endpoint in Spring Boot REST?

I'm developing Spring Boot v2.2.2.RELEASE and API versioning using Custom Header. By default I want latest version of endpoint to be executed.
Here in below code its X-API-VERSION=2. Before executing the endpoint I need to check mapping and then set the latest version in for the Custom Header and then execute it.
#GetMapping(value = "/student/header", headers = {"X-API-VERSION=1"})
public StudentV1 headerV1() {
return serviceImpl.headerV1();
}
#GetMapping(value = "/student/header", headers = {"X-API-VERSION=2"})
public StudentV1 headerV2() {
return serviceImpl.headerV2();
}
If I got your question right, you want that the request sent without X-API-VERSION header will automatically be routed to headerV2() method
This can be done:
Conceptually you'll need a Web Filter that gets executed before the Spring MVC Dispatch Servlet and you'll have to check whether the request contains a header and if it doesn't add a header of the version that you think should be the latest one (from configuration or something).
One caveat here is that the HttpServletRequest doesn't allow adding Headers so you'll have to create a HttpServletRequestWrapper and implement the logic of header addition there.
Here is an example (I've borrowed the implementation from this question):
// The web filter
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
#Component
#Order(1)
public class LatestVersionFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if(req.getHeader("X-API-VERSION")== null) {
HeaderMapRequestWrapper wrappedRequest = new HeaderMapRequestWrapper(req);
wrappedRequest.addHeader("X-API-VERSION", resolveLastVersion());
filterChain.doFilter(wrappedRequest, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
private String resolveLastVersion() {
// read from configuration or something
return "2";
}
}
Note that the request is wrapped to add the header as I've explained:
class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
/**
* construct a wrapper for this request
*
* #param request
*/
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
private Map<String, String> headerMap = new HashMap<String, String>();
/**
* add a header with given name and value
*
* #param name
* #param value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
#Override
public String getHeader(String name) {
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
/**
* get the Header names
*/
#Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
#Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
values.add(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
With this implementation (make sure you put the servlet in the package next to controller or something so that the spring boot will recognize it as a component) you'll see:
GET with header X-API-VERSION=1 will be routed to headerV1()
GET with header X-API-VERSION=2 will be routed to headerV2()
GET without X-API-VERSION header will be routed to headerV2()
You can add default method without header, like that:
#GetMapping(value = "/student/header")
public StudentV1 headerDefault() {
return headerV2();
}
Or just remove headers = {"X-API-VERSION=2"} from method that should be current.

How to get Request URL in Spring Boot

I need to submit request URL as a String parameter to a method
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void testItt(#RequestParam String requestParameter, #RequestURL String requestUrl) {
// Do something with requestUrl
}
How to submit Request URL correctly?
I tried request.getRequestURL().toString()
But I feel there must be a better way.
Never just grab the URL from the request. This is too easy! programming is supposed to be hard and when it's not hard, you MAKE it hard! :)
But you can retrieve the URL the way you show up above
So lets start off with an annotation that represents the value you want to retrieve
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface RequestURL {
}
This will work as a way to inject the value you already have access to.
Next we need to create a class that can build the URL string
public class RequestUrlArgumentResolver
implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(RequestURL.class) != null;
}
#Override
public Object resolveArgument(
MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request
= (HttpServletRequest) nativeWebRequest.getNativeRequest();
//Nice and cozy at home surrounded by safety not obfuscation
return request.getRequestURL().toString();
}
}
Next thing we need to do is get the framework to recognize the handler for this annotation.
add the method below to your configuration (If your config does not implement WebMvcConfigurer you may need to implement this class or create a new config which does and include the new config)
...
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestUrlArgumentResolver());
}
...
Then finally we are back to your original request mapping and it should work as originally written
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void testItt(#RequestParam String requestParameter,
#RequestURL String requestUrl) {
// Do something with requestUrl
}
Credits - https://www.baeldung.com/spring-mvc-custom-data-binder

Jackson failed to convert request element

I'm using Spring to craft a REST API which exposes a POST endpoint. I'm able to reach the endpoint, but I'm having trouble reading the request body.
The following code works: payload contains the object sent.
#RestController
public class RestController {
#RequestMapping(value = "/endpoint")
public ResponseEntity endpoint(#RequestParam("payload") String str) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
Payload payload = objectMapper.readValue(str, Payload.class);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
However, the following code DOES NOT works: it throws an exception.
#RestController
public class RestController {
#RequestMapping(value = "/endpoint")
public ResponseEntity endpoint(#RequestParam("payload") Payload payload) throws IOException {
return ResponseEntity.status(HttpStatus.OK).build();
}
}
The exception:
Failed to convert request element: org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'beans.Payload'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'beans.Payload': no matching editors or conversion strategy found
Why the latter doesn't work? I thought Spring decode request parameters in the same way...
UPDATE: my Payload class:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Payload {
#JsonProperty("type")
private String type;
#JsonProperty("callback_id")
private String callbackId;
#JsonProperty("message_ts")
private String message_ts;
#JsonProperty("response_url")
private String responseUrl;
protected Payload() {}
public String getType() {
return type;
}
public String getCallbackId() {
return callbackId;
}
public String getMessage_ts() {
return message_ts;
}
public String getResponseUrl() {
return responseUrl;
}
}
UPDATE: I'm testing the endpoint with Postman. This is what I'm sending:
KEY VALUE
payload { "type": "test" }
and this is the error I got:
{
"timestamp": "2018-08-28T10:38:33.133+0000",
"status": 415,
"error": "Unsupported Media Type",
"message": "Content type 'multipart/form-data;boundary=--------------------------586093407866564427326096;charset=UTF-8' not supported",
"path": "/endpoint"
}
Payload constructor should not be protected. It should be public.
Change it to
Public PayLoad(){}
Otherwise, controllers cant create a payload object when mapping is done.
Use #RequestBody instead of #RequestParam. Your Payload is in post body and #RequestBody annotation will deserialize it to payload.
Just get rid of protected Payload() {} . As you don't have any parameterized constructer you are fine, Java compiler will take care of adding the default constructer to the compiled byte code.
And you need to change this
Controller method
#RestController
public class RestController {
#RequestMapping(value = "/endpoint")
public ResponseEntity endpoint(#RequestParam("payload") Payload payload) throws IOException {
return ResponseEntity.status(HttpStatus.OK).build();
}
}
Changes
a. Change HTTP request method to POST it, instead of GET . (method = RequestMethod.POST) .
b. Change Payload to a message body insteda of request param (#RequestParam("payload") Payload payload --> #RequestBody Payload payload ).
Change it as
#RequestMapping(value = "/endpoint", method = RequestMethod.POST)
public ResponseEntity endpoint(#RequestBody Payload payload) throws IOException {
return ResponseEntity.status(HttpStatus.OK).build();
}
Your URL patterns are mal-configured. Try,
#RestController
#RequestMapping(value = "/")
public class PayLoadController {
#RequestMapping(value = "endpoint/",method = RequestMethod.POST)
public ResponseEntity endpoint(#RequestBody Payload payload) throws IOException {
return ResponseEntity.status(HttpStatus.OK).build();
}}
Just copy paste this code and rename controller file name.
Then post your data to,
localhost:8080/endpoint/

Handling empty request body (protobuf3 encoded)

I have a Spring Boot application running. Requests/responses are sent protobuf (Protobuf3) encoded.
My (simplified) REST controller:
#RestController
public class ServiceController {
#RequestMapping(value = "/foo/{userId}", method = RequestMethod.POST)
public void doStuff(#PathVariable int userId, #RequestBody(required = false) Stuff.Request pbRequest) {
// Do stuff
}
}
My (simplified) protobuf3 schema:
syntax = "proto3";
message Request {
int32 data = 1;
}
My configuration to have content negotiation available:
#Configuration
public class ProtobufConfig {
#Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
}
Everything is working like a charm as long as the request body has some bytes set. BUT protobuf does not write any bytes if just default values are sent. As soon I have a request message which contains data = 0 the generated bytes are just empty. On the app side the request body is null and won't be converted to a protobuf message (it even throws an exception if request body is set to required = true). The HTTP input message isn't processed by the ProtobufHttpMessageConverter at all. Is there a way to handle that?
I found a way of handling it. But it uses reflection which is really something I don't want to have:
#ControllerAdvice
public class RequestBodyAdviceChain implements RequestBodyAdvice {
#Override
public boolean supports(MethodParameter methodParameter, Type type,
Class< ? extends HttpMessageConverter< ? >> aClass) {
return true;
}
#Override
public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter,
Type type, Class< ? extends HttpMessageConverter< ? >> aClass) {
try {
Class<?> cls = Class.forName(type.getTypeName());
Method m = cls.getMethod("getDefaultInstance");
return m.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter,
Type type, Class< ? extends HttpMessageConverter< ? >> aClass) throws IOException {
return httpInputMessage;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type,
Class< ? extends HttpMessageConverter< ? >> aClass) {
return body;
}
}
So in the case of an empty body I create a default instance of the protobuf message object.

Resources