Change body of ServerWebExchange response in WebFilter - spring

I am trying to make an application in spring webflux. But there is a lot of stuff that I can't find in the documentation.
I have a webfilter where I want to add a wrapper around the response. Example:
Reponse before:
{
"id": 1,
"name": "asdf"
}
Response after:
{
"status": "success",
"data": {
"id": 1,
"name": "asdf"
}
}
This is the WebFilter at the moment:
#Component
public class ResponseWrapperFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
// Add wrapper to the response content...
return webFilterChain.filter(serverWebExchange);
}
}
But I can't find a way to get or edit the body of the response.
How can I change the response output in a WebFilter?

As stated in the javadoc, WebFilter is made for application-agnostic, cross-cutting concerns. WebFilters usually mutate the request headers, add attributes to the exchange or handle the response altogether without delegating to the rest of the chain.
I don't think that what you're trying to achieve is easily doable or even something that should be achieved this way. At the raw exchange level, you're dealing with a Flux<DataBuffer>, which is the response body split randomly in groups of bytes.
You could somehow wrap the response in a filter and use Flux.concat to prepend and append data to the actual response written by the handler. But there are a lot of questions regarding encoding, infinite streams, etc.
Maybe this is a concern that should be achieved at the Encoder level, since there you could restrict that behavior to a particular media type.

Related

Error Response body changed after Boot 3 upgrade

I have the following Controller endpoint in my project:
#GetMapping(value = "/{id}")
public FooDto findOne(#PathVariable Long id) {
Foo model = fooService.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return toDto(model);
}
My application retrieved the following response when it couldn't find a Foo with the provided id:
{
"timestamp": "2023-01-06T08:43:12.161+00:00",
"status": 404,
"error": "Not Found",
"path": "/foo/99"
}
However, after upgrading to Boot 3, the response body changed to:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"instance": "/foo/99"
}
I couldn't find any related information in the Spring Boot 3.0 Migration Guide nor in the Upgrading to Spring Framework 6.x one.
Spring Web 6 introduced support for the "Problem Details for HTTP APIs" specification, RFC 7807.
With this, the ResponseStatusException now implements the ErrorResponse interface and extends the ErrorResponseException class.
Having a quick look at the javadocs, we can see that all these are backed by the RFC 7807 formatted ProblemDetail body, which, as you can imagine, has the fields of the new response you're getting, and also uses the application/problem+json media type in the response.
Here is a reference to how Spring now treats Error Responses, which naturally goes in the direction of using Problem Details spec across the board.
Now, normally, if you were simply relying on Boot's Error Handling mechanism without any further change, you would still see the same response as before. My guess is that you are using a #ControllerAdvice extending ResponseEntityExceptionHandler. With that, you are enabling RFC 7807 (as per the Spring docs here)
So, that is why your ResponseStatusException has changed its body content.
Configuring the Problem Detail response body to include previous fields
If you need to stick to the pre-existing fields (at least until you fully migrate to the Problem Detail based approach) or if you simply want to add custom fields to the error response, you can override the createResponseEntity method in the #ControlAdvice class extending ResponseEntityExceptionHandler as follows:
#ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> createResponseEntity(#Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (body instanceof ProblemDetail) {
ProblemDetail problemDetail = ((ProblemDetail) body);
problemDetail.setProperty("error", problemDetail.getTitle());
problemDetail.setProperty("timestamp", new Date());
if (request instanceof ServletWebRequest) {
problemDetail.setProperty("path", ((ServletWebRequest) request).getRequest()
.getRequestURI());
}
}
return new ResponseEntity<>(body, headers, statusCode);
}
}
Note: I'm using new Date() instead of Java Time simply because that's what Boot's DefaultErrorAttributes class uses. Also, I'm not using Boot's ErrorAttributes for simplicity.
Note that defining the path field is a little bit tricky because problemDetail.getInstance() returns null at this stage; the framework sets it up later in the HttpEntityMethodProcessor.
Of course, this solution is suitable for a servlet stack, but it should help figure out how to proceed in a reactive stack as well.
With this, the response will look as follows:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"instance": "/foo/99",
"error": "Not Found",
"path": "/foo/99",
"timestamp": "2023-01-06T10:00:20.509+00:00"
}
Of course, it has duplicated fields. You can completely replace the response body in the method if you prefer.
Configuring Boot to also use the Problem Detail spec
Now, to be consistent across the board in your application, note that Boot now provides the spring.mvc.problemdetails.enabled property to use the Problem Details in its Error Handling mechanism (which is disabled by default to avoid breaking changes, as its associated issue):
spring.mvc.problemdetails.enabled=true

Spring Webflux has huge performance drop from webFilter to controller

spring webflux version: 2.7.2
I tested a simple webflux project (although I tested with SSL/TLS, I believe the problem would still exist without them), and I found problems with titles like: "HTTP request returns immediately after entering webFilter" and "HTTP returns as soon as the request enters the controller", there is a huge performance difference between the two cases.
TPS tested with Apache Bench in the case of 4-cores CPU:
"Return as soon as the HTTP request enters the webFilter" = 67000
"The HTTP request will return immediately after entering the controller" = 20000 ~ 30000
Some additional conditions are added:
In the webFilter, the only thing I do is add the keep-alive response header to the response, and then return the response. (code below)
There is no code content that I added after the webFilter. The next piece of code I added is the controller
no injection with #Autowired in the controller.
The controller method uses #RequestBody to get parameters. The length of the input parameters is very small. Take the following code as an example: message 16 bytes (code below)
Code:
MyFilter
public class MyFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getResponse().getHeaders().add("keep-alive", "true");
// when i need to finish request
return exchange.getResponse().writeWith(Mono.just(DefaultDataBufferFactory.sharedInstance.wrap("hello".getBytes())));
// when i don't need to finish request
return chain.filter(exchange);
}
}
MyController:
#RestController
public class MyController {
#PostMapping("/hello")
public Mono<HelloBody> hello(#RequestBody HelloBody helloBody) {
return Mono.just(helloBody);
}
public static class HelloBody {
private String message;
// get/set
}
}
As can be seen from the above code, such a test does not involve any business.
What I want to know is why there is such a big difference in performance returned by these two scenarios. More importantly, how can I reduce the performance loss of this part, so as to strive for more room for improvement in the performance of my program.

How can I implement one "PostFilter" in SpringCloudGateway?

Forgive my poor English, please.
When using Spring Cloud Gateway, you can usually define global filters by implementing GlobalFilter. After doing some verification work in it, execute chain.filter(exchange). But if I want to do something with the response returned by the microservice in the filter, such as viewing the returned cookie, what should I do.
#Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// do something about the request
chain.filter(exchange);
// do something about the response
// does it works like above? I tried but failed, please help me .
}

Not able to access request payload in Spring filter when request type is www-form-urlencoded

A pretty simple use case:
class MyFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
String body = IOUtils.toString(req.getInputStream());
// body is a always empty
}
}
When I send a request with the content-type application/json, I get the actual payload. But when I send it as x-www-form-urlencoded, I don't happen to get any data. I am assuming one of the spring filters is already consuming the data and leaving my filter down the line with nothing.
Do note that this issue is NOT related to the fact that we can read a stream only once. That part I have already figured out and I am creating another HttpServletRequest with a buffered input stream for other filters. The problem is that something else in Spring Boot is reading all of my data leaving me with nothing.
I just want a way to get my hands on the payload. It doesn't have to come from the InputStream, if spring could just give me the payload as a byte[], it would serve my purpose.
I am using Spring Boot 1.3.5.
you can disable this via application.properties
spring.mvc.hiddenmethod.filter.enabled=false
I guess the problem here is HiddenHttpMethodFilter which consumes the stream.
Please try to disable the filter by adding below code and then check if it works
#Bean
public FilterRegistrationBean registration(HiddenHttpMethodFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}

How to make a #RestController POST method ignore Content-Type header and only use request body?

I'm using latest Spring Boot (1.2.1) and whatever Spring MVC version comes with it.
I have a controller method with implicit JSON conversions for both incoming and outgoing data:
#RestController
public class LoginController {
#RequestMapping(value = "/login", method = POST, produces = "application/json")
ResponseEntity<LoginResponse> login(#RequestBody LoginRequest loginRequest) {
// ...
}
}
This works fine, but only if request Content-Type is set to application/json. In all other cases, it responds with 415, regardless of the request body:
{
"timestamp": 1423844498998,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/login/"
}
Thing is, I'd like to make my API more lenient; I want Spring to only use the POST request body and completely ignore Content-Type header. (If request body is not valid JSON or cannot be parsed into LoginRequest instance, Spring already responds with 400 Bad Request which is fine.) Is this possible while continuing to use the implicit JSON conversions (via Jackson)?
I've tried consumes="*", and other variants like consumes = {"text/*", "application/*"} but it has no effect: the API keeps giving 415 if Content-Type is not JSON.
Edit
It looks like this behaviour is caused by MappingJackson2HttpMessageConverter whose documentation says:
By default, this converter supports application/json and
application/*+json. This can be overridden by setting the supportedMediaTypes property.
I'm still missing how exactly do I customise that, for example in a
custom Jackson2ObjectMapperBuilder...
I assume that you are using default MappingJackson2HttpMessageConverter provided by Spring.
If you would like to have the same behavior in all requests, one solution would be to write custom converter which will not look for Content-Type, in a header (instead will parse to JSON alwayse) and then configure Spring to use your custom one. Again this will affect all requests, so might not fit all needs.
public class CustomerJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper mapper = new ObjectMapper();
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public CustomerJsonHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
#Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return mapper.readValue(inputMessage.getBody(), clazz);
}
#Override
protected boolean supports(Class<?> clazz) {
return true;
}
#Override
protected void writeInternal(Object value, HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
String json = mapper.writeValueAsString(value);
outputMessage.getBody().write(json.getBytes());
}
}
To have custom media type,
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
Arrays.asList(
new MediaType("text", "plain"),
new MediaType("text", "html")
));
For anyone else who is curious about this;
It is possible to customize the used MappingJackson2HttpMessageConverter by overridding WebMvcConfigurerAdapter.extendMessageConverters to allow for multiple mime types.
However, it does not work as expected because application/x-www-form-urlencoded is hardcoded in ServletServerHttpRequest.getBody to modify the body to be url encoded (even if the post data is JSON) before passing it to MappingJackson2HttpMessageConverter.
If you really needed this to work then I think the only way is to put a Filter that modifies the request content-type header before handling (not to imply this is a good idea, just if the situation arises where this is necessary).
Update: watch out if you use this
(This was probably a stupid idea anyway.)
This has the side effect that server sets response Content-Type to whatever the first value in the request's Accept header is! (E.g. text/plain instead of the correct application/json.)
After noticing that, I got rid of this customisation and settled went with Spring's default behaviour (respond with 415 error if request does not have correct Content-Type).
Original answer:
MappingJackson2HttpMessageConverter javadocs state that:
By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.
...which pointed me towards a pretty simple solution that seems to work. In main Application class:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter(new CustomObjectMapper());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
return converter;
}
(CustomObjectMapper is related to other Jackson customisations I have; that contructor parameter is optional.)
This does affect all requests, but so far I don't see a problem with that in my app. If this became a problem, I'd probably just switch the #RequestBody parameter into String, and deserialise it manually.

Resources