In the below code snippet, I am trying to match my request against a custom predicate. Upon the predicate being evaluated as false, I would like to send back a custom status code (403 Forbidden in the below snippet) instead of the default 404 that is being sent on predicate failure. Here's what I've tried.
RouteLocator
#Bean
public RouteLocator customRoutesLocator(RouteLocatorBuilder builder
AuthenticationRoutePredicateFactory arpf) {
return builder.routes()
.route("id1", r ->r.path("/app1/**")
.uri("lb://id1")
.predicate(arpf.apply(new Config()))).build();
}
AuthenticationRoutePredicateFactory
public class AuthenticationRoutePredicateFactory
extends AbstractRoutePredicateFactory<AuthenticationRoutePredicateFactory.Config> {
public AuthenticationRoutePredicateFactory() {
super(Config.class);
}
#Override
public Predicate<ServerWebExchange> apply(Config config) {
return (ServerWebExchange t) -> {
try {
Boolean isRequestAuthenticated = checkAuthenticated();
return isRequestAuthenticated;
}
} catch (HttpClientErrorException e) {
//This status code does not carried forward and 404 is displayed instead.
t.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return false;
}
};
}
#Validated
public static class Config {
public Config() {
}
}
private Boolean checkAuthenticated() {
// Some sample logic that makes a REST call and returns TRUE/FALSE/HttpClientErrorException
//Not shown here for simplicity.
return true;
}
}
When the predicate returns as true, the request is forwarded to the URI. However, on false evaluation 404 is displayed, I require 403 to be displayed (On HttpClientErrorException ). Is this the right way to expect a response with custom status code?. Additionally, I also read on implementing custom webfilters for a given route that can possibly modify the response object before forwarding the request. Is there a way to invoke a filter on predicate failure in that case?
As a newbie to spring cloud gateway, I chose the wrong direction towards approaching this problem.
Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a route, it is sent to the Gateway Web Handler. Predicates aid the Gateway Handler Mapping in determining if that request matches the route and can only return either a true or false value.
Once the request matches the route, The handler runs the request through a filter chain that is specific to the request. This is where "pre" or "post" requests can be applied before forwarding a request.
Therefore In order to send a custom response status code before conditionally forwarding the request, One must write a custom "pre" filter and this can be achieved as below. (Setting 403 status code)
AuthenticationGatewayFilterFactory
#Component
public class AuthenticationGatewayFilterFactory
extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
public AuthenticationGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
try {
if(isRequestAuthenticated) {
return chain.filter(exchange);
}
else {
exchange.getResponse().setStatusCode(403); // Any status code can be set here.
return exchange.getResponse().complete();
}
} catch (HttpClientErrorException e) {
exchange.getResponse().setStatusCode(403); // Any status code can be set here.
return exchange.getResponse().complete();
}
};
}
public static class Config {
}
private Boolean isRequestAuthenticated(String authToken) {
// Some sample logic that makes a REST call and returns TRUE/FALSE/HttpClientErrorException
//Not shown here for simplicity.
return true;
}
RouteLocator
#Bean
public RouteLocator customRoutesLocator(RouteLocatorBuilder builder
AuthenticationGatewayFilterFactory agff) {
return builder.routes()
.route("id1", r ->r.path("/app1/**")
.uri("lb://id1")
.filter(agff.apply(new Config()))).build();
}
Related
In the following code in Spring Webflux application, I am calling an endpoint "myfunction" which internally calls another endpoint. If the list contains 3 values, I will hit the "cancel" endpoint 3 times. Here is the question. I want to hit the endpoint one by one which means once I get response for 1st value in list then only I want to hit for second value and so on. I know it is reactive framework, still do we have any way to do without using delayElements.
#RestController
#RequestMapping("test")
#Slf4j
public class MyRestController {
private final WebClient webClient;
public MyRestController(WebClient webClient) {
this.webClient = webClient.mutate().baseUrl("http://localhost:7076/test/").build();
}
#GetMapping("/myfunction")
public void callTest() {
Flux.fromIterable(List.of("e1", "e2", "e3"))
//.delayElements(Duration.ofMillis(1000))
.flatMap(event -> {
log.info(event);
return sendCancelRequest(event);
}).subscribe(log::info);
}
public Mono<String> sendCancelRequest(String event) {
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("cancel").queryParam("event", event).build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class);
}
#GetMapping("/cancel")
public Mono<String> callMe(#RequestParam String event) {
//try{Thread.sleep(5000);}catch (Exception e){}
return Mono.just(event + " cancelled");
}
}
For example:
Once I get response for "e1" then only I wanna to call "e2" as sequence and response matters for subsequent values in the list. Please assist here guys!
I'm working on a POC i need to use zuul as a sever to route 2 routes first will run normally but it has a custom post filter which will send another request to other api using some data of the response of the first requet,
so need to extract the response body of the first request into my custom post filter and get some specific attributes but i can not find the response as it always be null but the status code is 200.
how can i wait and get a value of specific attribute from the response and get the actual status code not just 200 as default value.
i tried to make this implementation using cloud gateway but i reached the same point of disability of extracting the response.
also i tried to make a response decorator but it failed too.
#Component
public class AddResponseHeaderFilter extends ZuulFilter {
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
System.out.println("this is my filter");
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = new HttpServletRequestWrapper(context.getRequest());
System.out.println(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
HttpServletResponse servletResponse = context.getResponse();
// return an address only
System.out.println(context.getResponseBody().toString());
servletResponse.addHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}
RequestContext.getCurrentContext().getResponseDataStream() works fine for me, I am also able to manipulate the response.
import java.nio.charset.Charset;
import org.springframework.util.StreamUtils;
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestLog = StreamUtils.copyToString(request.getInputStream(),
Charset.forName("UTF-8"));
I have a filter (coded in Java) that processes every HTTP request made to my app. I want this filter to look at the value of a specific header, and based on that value, drop (i.e. eventually return a 404 error) this current HTTP request.
Is it possible to do this in Play?
Thanks,
This is fairly straight-forward. First of all, read and understand the section on filters in the documentation.
The implementation of your filter then looks like this:
import akka.stream.Materializer;
import play.mvc.Filter;
import play.mvc.Http;
public class HeaderFilter extends Filter {
#Inject
public HeaderFilter(final Materializer mat) {
super(mat);
}
#Override
public CompletionStage<Result> apply(final Function<Http.RequestHeader, CompletionStage<Result>> next,
final Http.RequestHeader requestHeader) {
final String header = requestHeader.getHeader("header name");
if (some test) {
// allow the request to continue
return next.apply(requestHeader);
} else {
// block the request
return CompletableFuture.completedFuture(Results.notFound());
}
}
}
I trying to write custom exception mapper for CompletionException and to show image in case of this excption. This is my mapper:
#Provider
public class CustomCompleteExceptionManager implements ExceptionMapper<CompletionException> {
#Override
public Response toResponse(CompletionException e) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity(Paths.get("error.png").toFile())
.type("image/img")
.build();
}
Method toResponse is called when my code throws CompletionException, but browser doesn't displays error.png. I get an error:
This site can’t be reached
The webpage at http://localhost:8080/cf/point might be temporarily down or it may have moved permanently to a new web address.
When I write just String message to Response it works fine:
#Provider
public class CustomCompleteExceptionManager implements ExceptionMapper<CompletionException> {
#Override
public Response toResponse(CompletionException e) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity(e.getMessage())
.build();
}
What could be the problem?
The media type set in response is wrong.
It should be .type("image/png") not image/img.
This is the code when I tested in my local.
return Response
.status(Response.Status.BAD_REQUEST)
.entity(Paths.get("/home","kishore","Pictures","Meme.png").toFile())
.type("image/png")
.build();
I have an Spring + CXF application which consumes a Transmission API: Transmission RPC running in another server.
According to Transmission docs, you need to send a token which is generated on the first request. The server then responds with a 409 http code along with a header containing the token. This token should be sent on all subsequent calls:
2.3.1. CSRF Protection Most Transmission RPC servers require a X-Transmission-Session-Id header to be sent with requests, to prevent
CSRF attacks. When your request has the wrong id -- such as when you
send your first request, or when the server expires the CSRF token --
the Transmission RPC server will return an HTTP 409 error with the
right X-Transmission-Session-Id in its own headers. So, the correct
way to handle a 409 response is to update your
X-Transmission-Session-Id and to resend the previous request.
I was looking for solution either using a CXF filter or interceptor, that basically will handle the 409 response and retry the initial request adding the token header. I'm thinking that clients can persist this token and send it in future calls.
I'm not very familiar with cxf so I was wondering if this can be accomplish and how. Any hint would be helpful.
Thanks!
Here spring-retry can be utilized which is now an independent project and no longer part of spring-batch.
As explained here retry callback will help make another call updated with the token header.
Pseudo code / logic in this case would look something like below
RetryTemplate template = new RetryTemplate();
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
/*
* 1. Check if RetryContext contains the token via hasAttribute. If available set the header else proceed
* 2. Call the transmission API
* 3.a. If API responds with 409, read the token
* 3.a.1. Store the token in RetryContext via setAttribute method
* 3.a.2. Throw a custom exception so that retry kicks in
* 3.b. If API response is non 409 handle according to business logic
* 4. Return result
*/
}
});
Make sure to configure the RetryTemplate with reasonable retry & backoff policies so as to avoid any resource contention / surprises.
Let know in comments in case of any queries / roadblock.
N.B.: RetryContext's implementation RetryContextSupport has the hasAttribute & setAttribute method inherited from Spring core AttributeAccessor
Assuming you are using Apache CXF JAX RS Client it is easy to do by just creating a custom Runtime Exception and ResponseExceptionMapper for it. So the idea is to manually convert 409 outcomes to some exception and then handle them correctly (in your case retry the service call).
See following code snipped for fully working example.
#SpringBootApplication
#EnableJaxRsProxyClient
public class SpringBootClientApplication {
// This can e stored somewhere in db or elsewhere
private static String lastToken = "";
public static void main(String[] args) {
SpringApplication.run(SpringBootClientApplication.class, args);
}
#Bean
CommandLineRunner initWebClientRunner(final TransmissionService service) {
return new CommandLineRunner() {
#Override
public void run(String... runArgs) throws Exception {
try {
System.out.println(service.sayHello(1, lastToken));
// catch the TokenExpiredException get the new token and retry
} catch (TokenExpiredException ex) {
lastToken = ex.getNewToken();
System.out.println(service.sayHello(1, lastToken));
}
}
};
}
public static class TokenExpiredException extends RuntimeException {
private String newToken;
public TokenExpiredException(String token) {
newToken = token;
}
public String getNewToken() {
return newToken;
}
}
/**
* This is where the magic is done !!!!
*/
#Provider
public static class TokenExpiredExceptionMapper implements ResponseExceptionMapper<TokenExpiredException> {
#Override
public TokenExpiredException fromResponse(Response r) {
if (r.getStatus() == 409) {
return new TokenExpiredException(r.getHeaderString("X-Transmission-Session-Id"));
}
return null;
}
}
#Path("/post")
public interface TransmissionService {
#GET
#Path("/{a}")
#Produces(MediaType.APPLICATION_JSON_VALUE)
String sayHello(#PathParam("a") Integer a, #HeaderParam("X-Transmission-Session-Id") String sessionId)
throws TokenExpiredException;
}
}