spring webflux (netty) handler can't parse ServerRequest containing json larger than 750 bytes - spring-boot

We recently started experimenting with spring boot 2.0.
Having the following handler code:
#Component
public class DataStreamHandler {
public Mono<ServerResponse> pipeEvent(ServerRequest request) {
Mono<String> reqBody = request.bodyToMono(String.class);
String body = reqBody.block();
System.out.println(body);
return ServerResponse.ok().body(fromObject("OK"));
}
}
#Configuration
public class RouterConfig {
#Bean
public RouterFunction<ServerResponse> monoRouterFunction(DataStreamHandler dataStreamHandler) {
return route(POST("/pipeEvent"), dataStreamHandler::pipeEvent);
}
}
It seems that the handler can't parse request containing json larger than 750 bytes.
when i searched how to configure max-http-post-size i found solutions only for tomcat,jetty and undertow.
How can i configure it for underlying netty?

This is a known issue in reactor Betty - look at this issue comment for more on this.
You're not supposed to do blocking operations within a Handler.
You should change your code to this:
#Component
public class DataStreamHandler {
public Mono<ServerResponse> pipeEvent(ServerRequest request) {
return request.bodyToMono(String.class)
.doOnNext(System.out::println)
.then(ServerResponse.ok().body(fromObject("OK")));
}
}

Related

AutoConfigure RestController Spring Boot

I have tried to find documentation on how to manually configure a RestController (i.e in a Configuation class). That means without using the RestController annotation. Considering all the other annotations, like mapping, pathvariables etc. is at all possible?
A controller is essentially a component with a request mapping. See RequestMappingHandlerMapping.
#Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
If you want to instantiate a "rest controller" through configuration, you can probably do so via the following:
#Configuration
public class MyConfiguration {
#Bean
public MyController() {
return new MyController();
}
}
#ResponseBody
public class MyController {
#RequestMapping("/test")
public String someEndpoint() {
return "some payload";
}
}
But I don't think you'll be able to configure the request mappings (path variables, etc) in the configuration though; at least I haven't seen an example nor figured out how.

Spring Boot RSocketRequester deal with server restart

I have a question about Springs RSocketRequester. I have a rsocket server and client. Client connects to this server and requests #MessageMapping endpoint. It works as expected.
But what if I restart the server. How to do automatic reconnect to rsocket server from client? Thanks
Server:
#Controller
class RSC {
#MessageMapping("pong")
public Mono<String> pong(String m) {
return Mono.just("PONG " + m);
}
}
Client:
#Bean
public RSocketRequester rSocketRequester() {
return RSocketRequester
.builder()
.connectTcp("localhost", 7000)
.block();
}
#RestController
class RST {
#Autowired
private RSocketRequester requester;
#GetMapping(path = "/ping")
public Mono<String> ping(){
return this.requester
.route("pong")
.data("TEST")
.retrieveMono(String.class)
.doOnNext(System.out::println);
}
}
Updated for Spring Framework 5.2.6+
You could achieve it with io.rsocket.core.RSocketConnector#reconnect.
#Bean
Mono<RSocketRequester> rSocketRequester(RSocketRequester.Builder rSocketRequesterBuilder) {
return rSocketRequesterBuilder
.rsocketConnector(connector -> connector
.reconnect(Retry.fixedDelay(Integer.MAX_VALUE, Duration.ofSeconds(1))))
.connectTcp("localhost", 7000);
}
#RestController
public class RST {
#Autowired
private Mono<RSocketRequester> rSocketRequesterMono;
#GetMapping(path = "/ping")
public Mono<String> ping() {
return rSocketRequesterMono.flatMap(rSocketRequester ->
rSocketRequester.route("pong")
.data("TEST")
.retrieveMono(String.class)
.doOnNext(System.out::println));
}
}
I don't think I would create a RSocketRequester bean in an application. Unlike WebClient (which has a pool of reusable connections), the RSocket requester wraps a single RSocket, i.e. a single network connection.
I think it's best to store a Mono<RSocketRequester> and subscribe to that to get an actual requester when needed. Because you don't want to create a new connection for each call, you can cache the result. Thanks to Mono retryXYZ operators, there are many ways you can refine the reconnection behavior.
You could try something like the following:
#Service
public class RSocketPingService {
private final Mono<RSocketRequester> requesterMono;
// Spring Boot is creating an auto-configured RSocketRequester.Builder bean
public RSocketPingService(RSocketRequester.Builder builder) {
this.requesterMono = builder
.dataMimeType(MediaType.APPLICATION_CBOR)
.connectTcp("localhost", 7000).retry(5).cache();
}
public Mono<String> ping() {
return this.requesterMono.flatMap(requester -> requester.route("pong")
.data("TEST")
.retrieveMono(String.class));
}
}
the answer here https://stackoverflow.com/a/58890649/2852528 is the right one. The only thing I would like to add is that reactor.util.retry.Retry has many options for configuring the logic of your retry including even logging.
So I would slightly improve the original answer, so we'd be increasing the time between the retry till riching the max value (16 sec) and before each retry log the failure - so we could monitor the activity of the connector:
#Bean
Mono<RSocketRequester> rSocketRequester(RSocketRequester.Builder builder) {
return builder.rsocketConnector(connector -> connector.reconnect(Retry.backoff(Integer.MAX_VALUE, Duration.ofSeconds(1L))
.maxBackoff(Duration.ofSeconds(16L))
.jitter(1.0D)
.doBeforeRetry((signal) -> log.error("connection error", signal.failure()))))
.connectTcp("localhost", 7000);
}

FeignClient is passing on headers

I have about 10 microservices all built with Spring boot 2 using Eureka and FeignClients. My microservices use certain header values to keep track of data so when a FeignClient is used it needs to pass on certain values that are in the incoming request. So if Microservice 1 does a call to Microservice 2 it must pass on the headers from the incoming request onto microservice 2. I haven't been able to find out how I can do that. I understand their is #Header however if you have 20 FeignClients then you don't want to have to manually add the #header to all the FeignClients. Can you indicate that FeignClients must read a certain header from the incoming request and pass it on in the FeignClient?
You can use request interceptor in Feign.
Example Implementation:
Request Interceptor:
#Component
public class MyRequestInterceptor implements RequestInterceptor {
#Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String authorization = requestAttributes.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
if(null != authorization) {
template.header(HttpHeaders.AUTHORIZATION, authorization);
}
}
}
Bean Configuration:
#Configuration
public class CustomFeignConfig {
#Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
#Bean
public MyRequestInterceptor basicAuthRequestInterceptor() {
return new MyRequestInterceptor();
}
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
}

Persisting Spring Cloud Gateway Routes in Database

I am currently using the spring cloud gateway project to build simple api gateway, the plan was to persist the route in mongodb, then refresh, so that the new route can be available. I have done something simple like this to get my route from mongo.
#Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder){
List<CreateAPIRequest> apiRequestList = repository.findAll();
RouteLocatorBuilder.Builder routeLocator = builder.routes();
for (CreateAPIRequest request: apiRequestList) {
routeLocator
.route(r-> {
r.path("/"+request.getProxy().getListenPath())
.filters(f->f.stripPrefix(1))
.uri(request.getProxy().getTargetUrl())
});
}
return routeLocator.build();
}
I was able to create new route in the db, but I am unable to refresh on the fly.
I need to understand how to refresh the routes on the fly.
Thanks
Whenever you wish to update the routes dynamically send a RefreshRoutesEvent. The following component implements the event sending functionality.
#Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
ApplicationEventPublisher publisher;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
publisher = applicationEventPublisher;
}
public void refreshRoutes() {
publisher.publishEvent(new RefreshRoutesEvent(this));
}
}
Here is a sample showing how to use the component above:
#Autowired
GatewayRoutesRefresher gatewayRoutesRefresher;
...
public void buildRoutes() {
// build your routes basing on your db entries then refresh the routes in gateway
...
gatewayRoutesRefresher.refreshRoutes();
}
You can find a more complete picture of the concept by looking into the following project code: https://github.com/botorabi/HomieCenter
SCG(Spring Cloud Gateway) has been provided RouteDefinitionRepository, you can write your own RouteDefinitionRepository, and implements RouteDefinitionRepository to override getRouteDefinitions method.
You can refer to this class: InMemoryRouteDefinitionRepository
For example:
#Service
public class MongodbDefinitionRepository implements RouteDefinitionRepository {
#Autowired
private RouteConfigDao routeConfigDao;
#Override
public Flux<RouteDefinition> getRouteDefinitions() {
// todo
List<RouteDefinition> routeConfigs = routeConfigDao.findAll();
return Flux.fromIterable(routeConfigs);
}
#Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
// todo
return Mono.empty();
});
}
#Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
// todo
int delete = routeConfigDao.delete(routeId);
if (delete > 0) {
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new Exception("delete route definition error, routeId:" + routeId)));
});
}
}
How to refresh the routes on the fly
Enable actuator
place this in your application.yml
management:
endpoints:
web:
exposure:
include: gateway
POST http://ip:port/actuator/gateway/refresh
Publish RefreshRoutesEvent
#Service
public class MyPublishBiz implements ApplicationEventPublisherAware {
protected ApplicationEventPublisher publisher;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public Mono<Void> refresh() {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return Mono.empty();
}
}
I went quickly to the repo and the open issues.
And it seems that at the moment the only way to refresh the routes is from Actuator via:
/actuator/gateway/refresh
You can check the discussion here: https://github.com/spring-cloud/spring-cloud-gateway/issues/43
Can you use Consul for persisting your route definitions instead of mongo. Then a simple POST call to the actuator's refresh will reload your route definitions on the fly.

Spring cloud gateway: How to create a filter

I'm new to spring cloud gateway.
I've been watching some of the youtube videos from the SpringDeveloper channel and am working on the following example:
#Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/get")
.addRequestHeader("X-SpringOne", "Awesome")
.uri("http://httpbin.org:80"))
.build();
}
Prior to looking at spring cloud gateway, i've also looked at Spring Netflix Zuul. I understand that in Netflix Zuul, you can create filters by creating a class that extends ZuulFilter and define it as a pre, post, route, etc.
However I was wondering how one can create a PRE/ POST filter using Spring cloud gateway?
Any help/ advice is much appreciated.
Thanks.
For a pre filter here is AddRequestHeader (code is executed before chain.filter() call):
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
#Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
For a 'post' filter, here is SetStatus (code is run in lambda in chain.filter(exchange).then()):
public class SetStatusGatewayFilterFactory extends AbstractGatewayFilterFactory<SetStatusGatewayFilterFactory.Config> {
#Override
public GatewayFilter apply(Config config) {
final HttpStatus status = ServerWebExchangeUtils.parse(config.status);
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// check not really needed, since it is guarded in setStatusCode,
// but it's a good example
if (!exchange.getResponse().isCommitted()) {
setResponseStatus(exchange, status);
}
}));
};
}
}
Here is a simple example in Kotlin: the URI http://.../customers is mapped to the URI obtained from the discovery service (lb = load balanced) for the service named customer and appended with "/". Furthermore, the forwarded request is enhanced with an additional header entry. Hope this helps.
#SpringBootApplication
class Application {
#Bean
fun routes(builder: RouteLocatorBuilder) = builder.routes {
route {
path("/customers")
filters {
setPath("/")
addRequestHeader("aKey", "aValue")
}
uri("lb://customer")
}
}
}
I am not sure this is the correct way to do it because I am also trying to achieve this behavior, I am thinking if this is something that needs to be done:
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CustomFilter implements GatewayFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//code for PRE filter
Mono<Void> v = chain.filter(exchange);
//code for POST filter
return v;
}
}
Let me know if that works for you or if you found another solution.

Resources