RestOperation exchange rejects cookie - java

I have a class which checks the health status of a REST-Interface. It works nice but my log files are full of these warnings:
2017-08-23 03:59:58.707 WARN 1849 --- [io-13811-exec-5] o.a.h.c.protocol.ResponseProcessCookies : Cookie rejected [JSESSIONID="14747303A2F23D4BE6DBAE0F282DEA94", version:0, domain:dealersearch.....com, path:/DCRMBroker/, expiry:null] Illegal 'path' attribute "/DCRMBroker/". Path of origin: "/system/healthcheck.jsp"
It seems the requests do not accept any cookies. I do not understand what is going on and how to accept them to get around the masses of log messages.
My health check class:
package com......commons.health;
import com.....commons.health.HealthResult.Health;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestOperations;
public abstract class RestHealthCheck extends HealthCheck {
private static final Logger LOGGER = LoggerFactory.getLogger(RestHealthCheck.class);
public RestHealthCheck() {}
public abstract String getUrl();
public abstract RestOperations getRestOperations();
public HealthResult getHealthResult() {
HealthResult result = new HealthResult();
result.setName(this.getName());
result.setHealth(Health.HEALTHY);
result.setMessage((String)null);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
HttpEntity entity = new HttpEntity(headers);
try {
this.getRestOperations().exchange(this.getUrl(), HttpMethod.HEAD, entity, String.class, new Object[0]);
} catch (Exception var5) {
LOGGER.trace("Health check failed", var5);
result.setHealth(Health.UNHEALTHY);
result.setMessage(var5.getMessage());
}
return result;
}
}

An URL http://server.example.com/system/healthcheck.jsp is requested and the response contained a cookie that is defined for the URL http://server.example.com/DCRMBroker/.
The URLs are different and a client worth its name should reject that which happens here leading to the logs. The information you gave is not enough to say for sure who the culprit is. It can be the JSP-page setting a wrong cookie, it might be a forwarding rule on the server, so the JSP-page is internally forwarded to a different resource without changing the path within the response. It might also be something on your client where some kind of redirection takes place leading to this effect.
Maybe my explanations help you to track the reason down. If it's something on your side, you can fix that. If it's something on the other side, you might get in contact with them in order to let that fix that. Alternatively you can check your client if you can switch off the warning for this particular case so your log becomes calm again.

Related

Spring Cloud Gateway - modify response body in global Post filter

I have spent the last 2 days trying every possible way of modifying the response body of a request before it hits the client, and nothing seems to work for me. So far I have tried the implementations mentioned here, here, here, here, here and a few others that I can't find right now, but nothing has worked. It doesn't matter if I define the filter as pre, post, global, gateway or route-specific - the actual response modification doesn't seem to work for me.
My situation is the following:
I have a YAML-configured API gateway running and have configured one of its routes to lead to an ADF service in the background. The issue I have with this ADF application is that the response it returns to the client is in the form of an HTML template that is automatically generated by its backend. In this template, some of the URLs are hardcoded and point to the address of the application itself. To justify the use of an API Gateway in this case, I want to replace those ADF URLs with those of the API Gateway.
For simplicity's sake, let's say the IP address of my ADF service is 1.2.3.4:1234, and the IP address of my API Gateway is localhost:8080. When I hit the ADF route in my gateway, the response contains some auto-generated javascript inserts, such as this one:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://1.2.3.4:1234/entry/dynamic/index.jspx");
As you can see, it contains a hardcoded URL. I want to access the response body and find all those hardcoded URLs and replace them with the gateway URL, so the above example becomes:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://localhost:8080/entry/dynamic/index.jspx");
To do this, it seems sensible to me to have a global POST filter that kicks in only when the request matches the route for my ADF application, so that's what I've settled on doing.
Here is my post filter so far:
#Bean
public GlobalFilter globalADFUrlReplacementFilter() {
return (exchange, chain) -> chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (requestIsTowardsADF(request)) {
logger.info("EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT");
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
#Override
#SuppressWarnings("unchecked")
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
logger.info("OVERRIDING writeWith METHOD TO MODIFY THE BODY");
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(buffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(buffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
bodyStr = bodyStr.replace(ADF_URL, API_GATEWAY_URL);
getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
return bufferFactory().wrap(bodyStr.getBytes());
}));
}
};
logger.info("ADF URL REPLACEMENT FILTER DONE");
return chain.filter(serverWebExchange.mutate().request(request).response(responseDecorator).build());
}
return serverWebExchange;
})
.then();
}
And the config:
spring:
cloud:
gateway:
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
You can see that I'm using a org.slf4j.Logger object to log messages in the console. When I run my API Gateway and hit the ADF route, I can see the following:
EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT
ADF URL REPLACEMENT FILTER DONE
And when I check the response I got back from the API Gateway, I can see that the response body is still identical and the ADF URLs have not been replaced at all. I tried debugging the application and as soon as it reaches ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) { it skips over the entire anonymous class implementation within those curly braces. A testament to that is the absence of the OVERRIDING writeWith METHOD TO MODIFY THE BODY log in the console - it never got executed!
It seems that for some reason the actual body modification doesn't get executed and I can't figure out why. I tried several different implementations of this filter, as mentioned in the above links, and neither of them worked.
Can someone please share with me a working POST filter that modifies the response body, or point out the flaw in my solution?
Thanks a bunch in advance!
Thanks for sharing this sample filter cdan. I provided the most straightforward solution to my issue using it as a template. Here's how it looks:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
#Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
final Logger logger = LoggerFactory.getLogger(TestFilter2.class);
public static class Config {
private String param1;
public Config() {
}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
}
#Override
public List<String> shortcutFieldOrder() {
return List.of("param1");
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public TestFilter2() {
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
#Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
I have added this filter to my route definition like so:
spring:
cloud:
gateway:
httpclient:
wiretap: true
httpserver:
wiretap: true
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
filters:
- TestFilter2
I'm simply trying to modify the response body and replace the ADF URL in it with the AG URL, but whenever I try to hit the ADF route I get the below exception:
2022-05-08 17:35:19.492 ERROR 87216 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [284b180d-1] 500 Server Error for HTTP GET "/entry/dynamic/index.jspx"
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String
at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders$12(BodyExtractors.java:201) ~[spring-webflux-5.3.18.jar:5.3.18]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? Body from UNKNOWN [DefaultClientResponse]
*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ? HTTP GET "/entry/dynamic/index.jspx" [ExceptionHandlingWebHandler]
I searched the web for some time but wasn't able to find any clear answer on why this UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String exception gets thrown when I try to work with the bodyAsString field that is supposed to contain the response body as String. Debugging the entire filter didn't work either, as the exception seems to be thrown immediately after I hit the route and I can't even get in the body of that class. Am I missing something obvious?
UPDATE (09.05.2022):
After looking into this further, I refactored the filter structure a bit by removing the unnecessary parameter in the config, and Autowiring the dependency towards ModifyResponseBodyGatewayFilterFactory, and now it seems the filter works properly and does the replacement I needed it to do. I will test it a bit longer to make sure it does indeed work as expected, and if it does, I'll mark this as the solution. Thanks for all of your input cdan!
Here's the entire filter:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
#Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
#Autowired
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public static class Config {
public Config() {
}
}
public TestFilter2(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
super(Config.class);
this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
}
#Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
Try with the built-in ModifyResponseBody Filter with Java DSL. If you still need more advanced response processing, your next option is to extend the ModifyResponseBodyGatewayFilterFactory class.
(Update 2022-05-08)
For example, using the Delegation design pattern (wrapping the built-in ModifyResponseBodyFilter in a new custom filter taking one custom parameter):
package test;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.*;
#Component
public class MyFilterFactory extends AbstractGatewayFilterFactory<MyFilterFactory.Config>
{
public static class Config
{
private String param1;
// Add other parameters if necessary
public Config() {}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
// Add getters and setters for other parameters if any
}
#Override
public List<String> shortcutFieldOrder()
{
return Arrays.asList("param1" /*, other parameters */ );
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public MyFilterFactory()
{
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
#Override
public GatewayFilter apply(Config config)
{
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> {
final String output;
/*
Do whatever transformation of bodyAsString (response body as String) and assign the result to output...
*/
return Mono.just(output);
});
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}

Javax validation does not include field name

We have built an API in which in our model We have a field
#Min(1) #Max(16)
private Long demoField;
When we provide 17 to demoField
it will throw us an error on the client-side
"must be less than or equal to 16"
But when we see the violation message it includes the field name and the message looks like
"demoField: must be less than or equal to 16"
So the question of why we are not getting field name in the client-side error message.
Am I missing something?
API built on spring boot
It's not passed by default. You could implement your own error handler to customize the message passed back, by using #ControllerAdvice for example.
One way is to just specify the message:
#Min(value = 5, message="Age must be at least 5")
In which case in the #ControllerAdvice, you would just need to read getDefaultMessage()
If you don't want to manually add default messages, the approach would be to implement something along the lines of (with appropriate null checks etc):
package com.example.demo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleBindException(
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(
ex.getFieldError().getField() + ": " + ex.getFieldError().getDefaultMessage(),
headers,
status);
}
}

OffsetDateTime yielding "No injection source found for a parameter of type public javax.ws.rs.core.response" in GET method

I have the following GET REST method:
import java.time.OffsetDateTime;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import com.product.rest.api.TransactionsApi;
import com.product.rest.model.Transaction;
#Path("/transactions")
#Api(description = "the transactions API")
#Consumes({ "application/json" })
#Produces({ "application/json" })
public class TransactionsApiImpl extends TransactionsApi {
#GET
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "", notes = "Get all transactions", response = Transaction.class, responseContainer = "List", tags = {})
#ApiResponses(
value = { #ApiResponse(code = 200, message = "OK", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 400, message = "Bad Request", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 404, message = "Not Found", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 500, message = "Internal Server Error", response = Transaction.class, responseContainer = "List") })
#Override
public Response transactionsGet(
#HeaderParam("tok") String tok,
#QueryParam("param1") Integer param1,
#QueryParam("param2") String param2,
#QueryParam("param3") OffsetDateTime param3,
#QueryParam("param4") OffsetDateTime param4,
#QueryParam("param5") Integer param5,
#QueryParam("param6") Integer param6,
#QueryParam("param7") String param7) {
return Response.ok().entity("Success!").build();
}
The TransactionsApi is a generated implementation using Swagger Codegen, as is the Transaction model class. I have several other functions in this class, but whenever I leave the GET /transactions function uncommented, I receive the following error:
WARN [Thread-1] (ContextHandler.java:2175) - unavailable
org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public javax.ws.rs.core.Response
com.product.rest.impl.v1.TransactionsApiImpl.transactionsGet(java.lang.String,java.lang.Integer,java.lang.String,java.time.OffsetDateTime,java.time.OffsetDateTime,java.lang.Integer,java.lang.Integer,java.lang.String) at index 3.; source='ResourceMethod{httpMethod=GET, consumedTypes=[application/json], producedTypes=[application/json], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.product.rest.impl.v1.TransactionsApiImpl, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor#7df78e88]}, definitionMethod=public javax.ws.rs.core.Response
All other similar questions I have found had to do with MultiPart Data and file uploading, whereas I am making a simple GET request. Other functions that also use the javax.ws.rs.code.Response class do not have this issue and the server starts normally.
I have noticed that the problem happens whenever the OffsetDateTime class is in the parameters (i.e. param3 and param4), but I have been unable to find out why. Moreover, OffsetDateTime was chosen by Swagger Codegen and I am reluctant to change it seeing how I will have to change every derived file afterwards whenever I regenerate my sources.
Has anyone had this issue before with REST services and OffsetDateTime?
All other similar questions I have found had to do with MultiPart Data and file uploading
It's related. The error is a general error you get when Jersey can't validate the resource model. Part of the resource model is the method parameters. Jersey has a system for knowing which parameters it will be able to process and which ones it won't. In your case, it doesn't know how to process the OffsetDateTime.
There are a set of rules that you need to follow in order to able to use non basic types as #QueryParams (and all other #XxxParams such as #PathParam and #FormParam, etc.):
Be a primitive type
Have a constructor that accepts a single String argument
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
So in this case of OffsetDateTime, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf or fromString
So basically, the only option left is to implement a ParamConverter/ParamConverterProvider for it. The basic set up looks like
#Provider
public class OffsetDateTimeProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (clazz.getName().equals(OffsetDateTime.class.getName())) {
return new ParamConverter<T>() {
#SuppressWarnings("unchecked")
#Override
public T fromString(String value) {
OffsetDateTime time = ...
return (T) time;
}
#Override
public String toString(T time) {
return ...;
}
};
}
return null;
}
}
Jersey will pass you the String value of the query parameter, and it's your job to to create it and return it.
Then just register the OffsetDateTimeProvider with the application. If you're using package scanning, it should be picked up and registered automatically from the #Provider annotation.
I don't use Swagger, so I don't know if they already provide something like this already implemented, but it seems odd that they would generate this for you, and not have a way to make it work. I know Jersey 3 will have Java 8 support out the box, but who know when the heck that's gonna be released.

Handle Embedded Tomcat Exception in Spring Boot

We have an issue where embedded Tomcat is throwing IllegalArgumentException from the LegacyCookieProcessor. It throws a 500 HTTP response code.
We need to handle the exception and do something with it (specifically, send it as a 400 instead).
The typical #ExceptionHandler(IllegalArgumentException.class) doesn't seem to get triggered and Google only seems to give results for dealing with Spring Boot specific exceptions.
Example:
Here is an example to reproduce the behavior. You can execute the example by downloading the initial project including spring-web (https://start.spring.io/) in version 2.1.5.RELEASE. Then add the following two classes to your project.
DemoControllerAdvice.java
package com.example.demo;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
#RestControllerAdvice
public class DemoControllerAdvice {
#ExceptionHandler(IllegalArgumentException.class)
#ResponseStatus(HttpStatus.FORBIDDEN)
public Map<String, String> forbiddenHandler() {
Map<String, String> map = new HashMap<>();
map.put("error", "An error occurred.");
map.put("status", HttpStatus.FORBIDDEN.value() + " " + HttpStatus.FORBIDDEN.name());
return map;
}
}
DemoRestController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DemoRestController {
#GetMapping(value = "/working")
public void working() {
throw new java.lang.IllegalArgumentException();
}
#GetMapping(value = "/not-working")
public String notWorking(#RequestParam String demo) {
return "You need to pass e.g. the character ^ as a request param to test this.";
}
}
Then, start the server and request the following URLs in the browser:
http://localhost:8080/working An IllegalArgumentException is thrown manually in the controller. It is then caught by the ControllerAdvice and will therefore produce a JSON string containing the information defined in the DemoControllerAdvice
http://localhost:8080/not-working?demo=test^123 An IllegalArgumentException is thrown by the Tomcat, because the request param cannot be parsed (because of the invalid character ^). The exception however is not caught by the ControllerAdvice. It shows the default HTML page provided by Tomcat. It also provides a different error code than defined in the DemoControllerAdvice.
In the logs the following message is shown:
o.apache.coyote.http11.Http11Processor : Error parsing HTTP request header
Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:467) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
This is a feature of Tomcat itself as mentioned in this answer.
However, you can do something like this by allowing the special characters that you are expecting as part of your request and handle them yourself.
First, you need to allow the special characters that you would need to handle by setting up the relaxedQueryChars like this.
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
#Component
public class TomcatCustomizer implements
WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers((connector) -> {
connector.setAttribute("relaxedQueryChars", "^");
});
}
}
and later handle the special characters in each of your requests or create an interceptor and handle it in a common place.
To handle it in the request individually you can do something like this.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DemoRestController {
#GetMapping(value = "/working")
public void working() {
throw new java.lang.IllegalArgumentException();
}
#GetMapping(value = "/not-working")
public String notWorking(#RequestParam String demo) {
if (demo.contains("^")) {
throw new java.lang.IllegalArgumentException("^");
}
return "You need to pass e.g. the character ^ as a request param to test this.";
}
}
You might also want to refer this answer to decide if you really need this fix.
Try to catch the IllegalArgumentException in your filter, then call HttpServletResponse.sendError(int sc, String msg);. This may catch the IllegalArgumentExceptions that do not come from Tomcat though. But I suppose you already handle them properly.

Custom content negociation handling extension AND "Accept" header with Jersey

I have this resource (simplified):
#Path("/cars{extension:(\\.(xml|json))?}")
public class Cars {
#GET
#Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response searchCars(#PathParam("extension") String extension) {
System.out.println("extension: " + extension);
//...
return Response.status(200).entity(output).build();
}
}
And I want, for instance (but the business logic can change), to return an xml response if the extension is xml, or Json if the extension is json, whatever the header "Accept" is.
The thing is by default I want to use the Accept header but let's say some dummy guy wants to access my web service with Ajax and doesn't know much about headers, I want to make things easy for him by just adding the appropriate extension.
With that snippet I am able to get the extension (if there is one) but I don't know how to change the strategy accordingly.
Thanks!
EDIT:
So I found something, I can use .type() from Response.ResponseBuilder like:
Response.ResponseBuilder responseBuilder = Response.status(200).entity(output);
if ([some test about extention or header])
responseBuilder.type(MediaType.APPLICATION_XML);
// other tests
I don't know if this is the correct way to do, but that would mean I need to handle it for all the paths...
I would use a ContainerResponseFilter for this, so you don't have todo it for each path.
First check for extension - aka the MediaType the dummy guy loves to get.
Than check if requested MediaType is acceptable for your service. If not, I would say the dummy guy has hard luck ;)
Example code [jersey 2.x]:
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.List;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;
#Provider
public class EntityResponseFilter implements ContainerResponseFilter {
#Override
public void filter( ContainerRequestContext reqc , ContainerResponseContext resc ) throws IOException {
MediaType mediaType = this.getMediaTypeFromExtentionOrHeader(); // TODO
List<MediaType> mediaTypes = reqc.getAcceptableMediaTypes();
if( mediaTypes.contains(mediaType) ) {
resc.setEntity( resc.getEntity(), new Annotation[0], mediaType );
}
// ...
}
}
Hope this was helpful somehow :)

Categories