Spring manage http status of custom DTO class without ResponseEntity - spring

public class ApiResponse {
long timestamp;
int status;
String error;
String message;
String path;
public ApiResponse(HttpStatus status, String message) {
super();
this.status = status.value();
this.message = message;
}
}
I have this class and what i want to do is set response status based on status value of ApiResponse object
return new ApiResponse(HttpStatus.CREATED, "Success");
I want response status to be 201.
Is something like this possible?
I know there is ResponseEntity class but i want to handle it without using this class.

Use the ResponseBodyAdvice interface to process the response uniformly.
#RestControllerAdvice
public class TransformStatusBodyAdvice implements ResponseBodyAdvice<ApiResponse> {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
// check return type is ApiResponse
return returnType.getParameterType().isAssignableFrom(ApiResponse.class);
}
#Override
public ApiResponse beforeBodyWrite(ApiResponse body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body != null) {
// change something if you want
// set the response status code
response.setStatusCode(HttpStatus.valueOf(body.getStatus()));
}
return body;
}
}
Test Controller
#RestController
#RequestMapping("/test")
public class TestController {
#PostMapping
public ApiResponse create() {
return new ApiResponse(HttpStatus.CREATED, "Success");
}
}
Use curl to test result. Now the http response code is 201. If you change your HttpStatus in the ApiResponse, the http status response code will be change.
$ curl -v -X POST http://localhost:8080/test
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /test HTTP/1.1
> ...
>
< HTTP/1.1 201
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Mon, 05 Jul 2021 03:45:00 GMT
<
{"timestamp":0,"status":201,"error":null,"message":"Success","path":null}* Connection #0 to host localhost left intact

Related

How to modify response status and body after committed spring boot

I implemented a rate limiter with Filter.class. However, we encountered that we should not limit successful requests. So, I needed status code of the response. When I get status code in Filter chain, it always returns 200. That means request not processed. When I trigger chain.doFilter status is set but response is in the committed state means read-only. However, I need to return 429 response for the rate limit responses
I tried OncePerRequestFilter.class, lots of wrappers that I forget. I expect to set response body via response status
I came upon lots of answers. However it was like that.
So I removed Filter usage. I use ResponseBodyAdvice globally.
#ControllerAdvice
public class RequestLimitAdvice implements ResponseBodyAdvice<Object> {
private final LoadingCache<String, Integer> requestCountsPerClient;
private static final List<Integer> statusCodes = Arrays.asList(HttpStatus.BAD_REQUEST.value(), HttpStatus.UNAUTHORIZED.value(),
HttpStatus.FORBIDDEN.value(), HttpStatus.NOT_FOUND.value(), HttpStatus.METHOD_NOT_ALLOWED.value(),
HttpStatus.NOT_ACCEPTABLE.value(), HttpStatus.REQUEST_TIMEOUT.value(), HttpStatus.CONFLICT.value(),
HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.NOT_IMPLEMENTED.value(), HttpStatus.SERVICE_UNAVAILABLE.value(),
HttpStatus.GATEWAY_TIMEOUT.value(), HttpStatus.HTTP_VERSION_NOT_SUPPORTED.value());
public RequestLimitAdvice() {
requestCountsPerClient = Caffeine.newBuilder().
expireAfterWrite(1, TimeUnit.MINUTES).build(key -> 0);
}
#Override
public boolean supports(#NotNull MethodParameter returnType, #NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, #NotNull MethodParameter returnType, #NotNull MediaType selectedContentType,
#NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
#NotNull ServerHttpRequest request, #NotNull ServerHttpResponse response) {
if (response instanceof ServletServerHttpResponse) {
ServletServerHttpResponse httpServletResponse = (ServletServerHttpResponse) (response);
if (statusCodes.contains(httpServletResponse.getServletResponse().getStatus())) {
String client = getClient(((ServletServerHttpRequest) request).getServletRequest());
if (isMaximumRequestsPerSecondExceeded(client)) {
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return Response.notOk(Translator.toLocale("http.status.too_many_requests"), EErrorCode.TOO_MANY_REQUESTS).getError();
}
}
}
return body;
}
So in ResponseBodyAdvice, thanks to set of response status code and modify enable of response body. I could make what I wanted.
From the answers of
https://stackoverflow.com/a/54501013/16263216 (ResponseBodyAdvice usage)
https://stackoverflow.com/a/65015720/16263216 (String responses ClassCastException issue)

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"})

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.

Overwrite Zuul's connection header Keep-Alive

I have following applications:
Gateway-Service
Api-Service
Gateway-Serivce is Zuul proxy and Api-Service is basically rest + broker + websocket + other thechnologies.
I'm using sockjs + stomp to communicate with broker through zuul. For doing protocol upgrade socket handler need to have connection header set to Upgrade. When I request this on gateway this connection header is set to Upgrade but after the zuul processing header is set to Keep-Alive.
So first question is if it's ok to replace connection header with original request header and if it's ok then how to do that? Using some Zuul filter?
Thanks
for someone interested this could be the solution...all credits goes to #spencergibb
#Component
public static class WebSocketHeaderFilter extends ZuulFilter {
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
RequestWrapper wrapper = new RequestWrapper(ctx.getRequest());
String upgradeHeader = wrapper.getHeader("Upgrade");
if (null == upgradeHeader) {
upgradeHeader = wrapper.getHeader("upgrade");
}
if (null != upgradeHeader && "websocket".equalsIgnoreCase(upgradeHeader)) {
wrapper.addHeader("connection", "Upgrade");
ctx.addZuulRequestHeader("connection", "Upgrade");
ctx.setRequest(wrapper);
}
return null;
}
}

Server returned HTTP response code: 400 using Spring-Security AuthenticationProvider

I'm building a RESTful authentication service and I'm having trouble connecting to it. I get the dreaded "Server returned HTTP response code: 400" when trying to retrieve the response. This seems very odd. I would think I'd get this error when sending the request.
I'm using this service as part of a Spring-Security AuthenticationProvider. I'm currently using a simulator instead of the actual service for testing. It won't connect to either the simulator or the service.
Here is the calling method:
public <T> T invoke(String service, Object request, Class<T> responseType) throws IOException {
ObjectMapper mapper = new ObjectMapper();
URL url = new URL("http://localhost:8888/simulator/rest" + service);
HttpURLConnection uc = (HttpURLConnection) url.openConnection();
uc.setRequestMethod("POST");
uc.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
uc.setDoOutput(true);
uc.setDoInput(true);
uc.connect();
OutputStream out = uc.getOutputStream();
mapper.writeValue(out, request);
out.close();
return mapper.readValue(uc.getInputStream(), responseType);
}
Here is the code that calls this method:
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken("thomas", "thomas");
UsernamePasswordAuthenticationToken response =
invoke("/authenticate", token, UsernamePasswordAuthenticationToken.class);
Here is the simulator method that gets called:
#RequestMapping(value = "/authenticate", method = RequestMethod.POST)
#ResponseBody
public UsernamePasswordAuthenticationToken authenticate(
#RequestBody UsernamePasswordAuthenticationToken userNameAndPassword) {
String userName = (String) userNameAndPassword.getPrincipal();
String password = (String) userNameAndPassword.getCredentials();
if (userName.equalsIgnoreCase("thomas")) {
if (userName.equals(password)) {
UsernamePasswordAuthenticationToken response =
new UsernamePasswordAuthenticationToken(
userName,
password,
new ArrayList<GrantedAuthority>());
return response;
}
}
return new UsernamePasswordAuthenticationToken(userName, password);
}
The line that causes the error is the :
mapper.readValue(uc.getInputStream(), responseType);
If can't see any issues with this code. Must have been looking at it too long. Need new eyes on the problem.
BTW, this REST service and simulator has been used successfully with other operations.
Additional Information:
The error occurs in the uc.getInputStream() call. The HttpURLConnection.inputStream = null.
Also, the headers for the request are as follows:
If this helps, here are the headers for this request:
[WARN] 400 - POST /simulator/rest/authenticate (127.0.0.1) 1417 bytes
Request headers
Content-Type: application/json;charset=UTF-8
X-Tenant: 1
Authorization: 0000013770b132a1dfcbfe0a694542b244534e0e406cfa857660c904daa89af91d0ac769
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.6.0_26
Host: localhost:8888
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 112
Response headers
Set-Cookie: JSESSIONID=1r02p7yvm8mzs;Path=/
X-UA-Compatible: IE=9
Content-Type: text/html; charset=iso-8859-1
Content-Length: 1417
Here is the my Token code:
import java.util.List;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
public class SerializedAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 2783395505630241326L;
private Object principal;
private Object credentials;
/**
* no-arg constructor to satisfy Serializable.
*/
public SerializedAuthenticationToken() {
super(null, null);
}
/**
* constructor.
*/
public SerializedAuthenticationToken(Object principal, Object credentials) {
super(null, null);
setPrincipal(principal);
setCredentials(credentials);
}
/**
* constructor with List<GrantedAuthorities>.
*/
public SerializedAuthenticationToken(Object principal, Object credentials, List<GrantedAuthority> authorities) {
super(null, null, authorities);
setPrincipal(principal);
setCredentials(credentials);
}
public Object getPrincipal() {
return principal;
}
public void setPrincipal(Object principal) {
this.principal = principal;
}
public Object getCredentials() {
return credentials;
}
public void setCredentials(Object credentials) {
this.credentials = credentials;
}
public void setName(Object name) {
}
}
I also now am getting a new stack trace:
org.codehaus.jackson.map.JsonMappingException: Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead (through reference chain: com.mckesson.shared.util.SerializedAuthenticationToken["authenticated"])
You need to create a Dummy token to do that. Here is my test to verify it:
public class JacksonTest {
#Test
public void test() throws Exception {
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("thomas", "thomas");
String tokenStr = mapper.writeValueAsString(token);
/* WON'T WORK
UsernamePasswordAuthenticationToken auth = mapper.readValue(tokenStr, UsernamePasswordAuthenticationToken.class);
*/
MyToken auth = mapper.readValue(tokenStr, MyToken.class);
String authStr = mapper.writeValueAsString(auth);
Assert.assertThat(tokenStr.equals(authStr), is(true));
}
private static class MyToken extends UsernamePasswordAuthenticationToken {
private Object principal;
private Object credentials;
private static final long serialVersionUID = -5045038656629236029L;
public MyToken() {
super(null, null);
}
public MyToken(Object principal, Object credentials) {
super(null, null);
this.principal = principal;
this.credentials = credentials;
}
/**
* #return the principal
*/
public Object getPrincipal() {
return principal;
}
/**
* #param principal the principal to set
*/
public void setPrincipal(Object principal) {
this.principal = principal;
}
/**
* #return the credentials
*/
public Object getCredentials() {
return credentials;
}
/**
* #param credentials the credentials to set
*/
public void setCredentials(Object credentials) {
this.credentials = credentials;
}
public void setName(Object name) {
}
}
}
Can't spot the exact issue, but I think a contributor to the problem is that fact that you're unnecessarily dropping down to such a low-level API to accomplish something for which some very reasonable abstractions exist. Have a look at Spring's RestTemplate to see a more elegant means or writing client code against a RESTful service. Whatever you're doing wrong, chances are RestTemplate will get it right.

Resources