2 ways streaming using spring webflux - spring

I'm wondering if it's possible to achieve 2 ways streaming using Spring Webflux?
Basically, I'm looking to make the client to send a flux of data that the server receives maps them to String then return the result, all fluently without having to collect data.
I did it using RSocket but I'm wondering if I can get the same result using http 2.0 (with Spring and Project-Reactor).
Tried doing like this:
1- Client:
public Mono<Void> stream() {
var input = Flux.range(1, 10).delayElements(Duration.ofMillis(500));
return stockWebClient.post()
.uri("/stream")
.body(BodyInserters.fromPublisher(input, Integer.class))
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class)
.log()
.then();
}
2- Server:
#PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(#RequestBody Integer i) {
return Flux.range(i, i+10).map(n -> String.valueOf(i)).log();
}
Or:
public Flux<String> stream(#RequestBody Flux<Integer> i) {
return i.map(n -> String.valueOf(i)).log();
}
Or:
#PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(#RequestBody List<Integer> i) {
return Flux.fromIterable(i).map(n -> String.valueOf(i)).log();
}
None worked correctly.

If you want use Server Sent Event you need to return a Flux<ServerSentEvent<String>>.
So your server merthod should be:
#PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> stream(#RequestBody Integer i) {
return Flux.range(i, i + 10).map(n -> ServerSentEvent.builder(String.valueOf(n)).build());
}
But in this case the body is only an Integer and your client code becomes:
input.flatMap(i ->
stockWebClient
.post()
.uri("/stream")
.bodyValue(i)
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<String>>() {})
.mapNotNull(ServerSentEvent::data)
.log())
.blockLast();
You can also do the same with functional endpoint.
If you want to be able to stream data from the client to the server and back you won't be able to use SSE but you can achieve this with websocket.
You will need a HandlerMapping and a WebSocketHandler
public class TestWebSocketHandler implements WebSocketHandler {
#Override
public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive()
.map(WebSocketMessage::getPayloadAsText)
.map(Integer::parseInt)
.concatMap(i -> Flux.range(i, i + 10).map(String::valueOf))
.map(session::textMessage);
return session.send(output);
}
}
The configuration with the handler :
#Bean
public TestWebSocketHandler myHandler() {
return new TestWebSocketHandler();
}
#Bean
public HandlerMapping handlerMapping(final TestWebSocketHandler myHandler) {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/streamSocket", myHandler);
int order = -1; // before annotated controllers
return new SimpleUrlHandlerMapping(map, order);
}
On the client side:
var input2 = Flux.range(1, 10).delayElements(Duration.ofMillis(500));
WebSocketClient client = new ReactorNettyWebSocketClient();
client.execute(URI.create("http://localhost:8080/streamSocket"), session ->
session.send(input2.map(i -> session.textMessage("" + i))).then(session.receive().map(WebSocketMessage::getPayloadAsText).log().then())
).block();

Related

API call not returning response with HttpRequestExecutingMessageHandler

I'm facing an issue where which ever API I call first using
HttpRequestExecutingMessageHandler
will return response back and second API called again using
HttpRequestExecutingMessageHandler
just hangs and return 504 timeout response back even though the request gets accepted for second API at server and processing is also done. Both the APIs call methods are listed in two different classes with separate output queue channels.
If I now restart the server and call second API first, this time, will return 200 response back but now first API start failing to respond back the 200 response.
#Configuration
class OutgoingHttpChannelAdapterConfig {
#Bean
#Qualifier("responseChannel1")
fun fromResponseChannel1(): QueueChannel = MessageChannels.queue().get()
#Bean
#Qualifier("customRestTemplateOut")
fun customRestTemplateOut(): RestTemplate {
return RestTemplate()
}
#Bean
#ServiceActivator(inputChannel = "forwardDataChannel")
#Throws(MessageHandlingException::class)
fun forwardRequestMethod(
#Qualifier("customRestTemplateOut") restTemplate: RestTemplate
): MessageHandler {
val headerMapper = DefaultHttpHeaderMapper()
headerMapper.setOutboundHeaderNames("Authorization","key")
val msgHandler = HttpRequestExecutingMessageHandler(url)
msgHandler.setHeaderMapper(headerMapper)
msgHandler.setHttpMethod(HttpMethod.POST)
msgHandler.isExpectReply = true
msgHandler.outputChannel = fromResponseChannel1()
msgHandler.setExpectedResponseType(DataResponse::class.java)
return msgHandler
}
}
#Configuration
class IncomingHttpChannelAdapterConfig{
#Bean
#Qualifier("responseChannel2")
fun fromResponseChannel2(): QueueChannel = MessageChannels.queue().get()
#Bean
#Qualifier("customRestTemplate")
fun customRestTemplate(): RestTemplate {
return RestTemplate()
}
#Bean
#ServiceActivator(inputChannel = "acceptRequestChannel")
#Throws(MessageHandlingException::class)
fun acceptRequestMethod(
#Qualifier("customRestTemplate") restTemplate: RestTemplate
): MessageHandler {
val parser = SpelExpressionParser()
val map = mapOf<String, Expression>(
"id" to parser.parseRaw("payload.id")
)
val msgHandler = HttpRequestExecutingMessageHandler(url, restTemplate)
msgHandler.setHeaderMapper(headerMapper)
msgHandler.setHttpMethod(HttpMethod.PUT)
msgHandler.outputChannel = fromResponseChannel2()
msgHandler.setUriVariableExpressions(map)
return msgHandler
}
}
#MessagingGateway(
defaultRequestChannel = "forwardDataChannel", errorChannel = "newErrorChannel",
defaultReplyChannel = "replyChannel1"
)
interface ForwardRequest {
fun forwardRequest(msg: Message<MessageNotification>): Message<*>
}
#MessagingGateway(
defaultRequestChannel = "acceptRequestChannel", errorChannel = "newErrorChannel",
defaultReplyChannel = "replyChannel2"
)
interface AcceptRequest {
fun acceptRequest(msg: Message<MessageNotification>): Message<*>
}
Right now we are not doing anything with queue channel. Its just used for placeholder purpose.

Configuring AWS Signing in Reactive Elasticsearch Configuration

In one of our service I tried to configure AWS signing in Spring data Reactive Elasticsearch configuration.
Spring provides the configuring the webclient through webclientClientConfigurer
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.usingSsl()
.withWebClientConfigurer(
webClient -> {
return webClient.mutate().filter(new AwsSigningInterceptor()).build();
})
. // ... other options to configure if required
.build();
through which we can configure to sign the requests but however AWS signing it requires url, queryparams, headers and request body(in case of POST,POST) to generate the signed headers.
Using this I created a simple exchange filter function to sign the request but in this function I was not able to access the request body and use it.
Below is the Filter function i was trying to use
#Component
public class AwsSigningInterceptor implements ExchangeFilterFunction
{
private final AwsHeaderSigner awsHeaderSigner;
public AwsSigningInterceptor(AwsHeaderSigner awsHeaderSigner)
{
this.awsHeaderSigner = awsHeaderSigner;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
{
Map<String, List<String>> signingHeaders = awsHeaderSigner.createSigningHeaders(request, new byte[]{}, "es", "us-west-2"); // should pass request body bytes in place of new byte[]{}
ClientRequest.Builder requestBuilder = ClientRequest.from(request);
signingHeaders.forEach((key, value) -> requestBuilder.header(key, value.toArray(new String[0])));
return next.exchange(requestBuilder.build());
}
}
I also tried to access the request body inside ExchangeFilterFunction using below approach but once i get the request body using below approach.
ClientRequest.from(newRequest.build())
.body(
(outputMessage, context) -> {
ClientHttpRequestDecorator loggingOutputMessage =
new ClientHttpRequestDecorator(outputMessage) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("Inside write with method");
body =
DataBufferUtils.join(body)
.map(
content -> {
// Log request body using
// 'content.toString(StandardCharsets.UTF_8)'
String requestBody =
content.toString(StandardCharsets.UTF_8);
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of(
requestBody.getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return content;
});
log.info("Before returning the body");
return super.writeWith(body);
}
#Override
public Mono<Void>
setComplete() { // This is for requests with no body (e.g. GET).
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of("".getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return super.setComplete();
}
};
return originalBodyInserter.insert(loggingOutputMessage, context);
})
.build();
But with above approach I was not able to change the request headers as adding headers throws UnsupportedOperationException inside writewith method.
Has anyone used the spring data reactive elastic search and configured to sign with AWS signed headers?
Any help would be highly appreciated.

How to properly get InputStreamResource to ResponseEntity in Webflux?

I have a method that fetches a PDF from another web service, but we have to fetch a cross reference ID before we can make the call:
PdfService.groovy
#Service
class PdfService {
#Autowired
WebClient webClient
#Autowired
CrossRefService crossRefService
InputStreamResource getPdf(String userId, String pdfId) {
def pdf = return crossRefService
.getCrossRefId(userId)
.flatMapMany(crossRefResponse -> {
return webClient
.get()
.uri("https://some-url/${pdfId}.pdf", {
it.queryParam("crossRefId", crossRefResponse.id)
it.build(pdfId)
})
.accept(MediaType.APPLICATION_PDF)
.retrieve()
.bodyToFlux(DataBuffer)
})
convertDataBufferToInputStreamResource(pdf)
}
// https://manhtai.github.io/posts/flux-databuffer-to-inputstream/
InputStreamResource getInputStreamFromFluxDataBuffer(Flux<DataBuffer> data) throws IOException {
PipedOutputStream osPipe = new PipedOutputStream();
PipedInputStream isPipe = new PipedInputStream(osPipe);
DataBufferUtils.write(data, osPipe)
.subscribeOn(Schedulers.elastic())
.doOnComplete(() -> {
try {
osPipe.close();
} catch (IOException ignored) {
}
})
.subscribe(DataBufferUtils.releaseConsumer());
new InputStreamResource(isPipe);
}
}
PdfController.groovy
#RestController
#RequestMapping("/pdf/{pdfId}.pdf")
class PdfController {
#Autowired
PdfService service
#GetMapping
Mono<ResponseEntity<Resource>> getPdf(#AuthenticationPrincipal Jwt jwt, #PathVariable String pdfId) {
def pdf = service.getPdf(jwt.claims.userId, pdfId)
Mono.just(ResponseEntity.ok().body(pdf))
}
}
When I run my service class as an integration test, everything work fine, and the InputStreamResource has a content length of 240,000. However, when I try to make this same call from the controller, it seems as if the internal cross reference call is never made.
What is the correct way to place an InputStreamResource into a Publisher? Or is it even needed?

spring boot web client and writing pact contracts

So im trying to figure out how to write consumer contracts for the following class. I have written junit tests fine using mockwebserver.
However for pact testing im struggling and cant seem to see how you get the weblient to use the response from server, all the examples tend to be for resttemplate.
public class OrdersGateway {
public static final String PATH = "/orders";
private final WebClient webClient;
#Autowired
public OrdersGateway(String baseURL) {
this.webClient = WebClient.builder()
.baseUrl(baseURL)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.ALL_VALUE)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
#Override
public Orderresponse findOrders() {
return this.webClient
.post()
.uri(PATH)
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(4));
})
.exchangeToMono(response())
.block();
}
private Function<ClientResponse, Mono<OrderResponse>> response() {
return result -> {
if (result.statusCode().equals(HttpStatus.OK)) {
return result.bodyToMono(OrderResponse.class);
} else {
String exception = String.format("error", result.statusCode());
return Mono.error(new IllegalStateException(exception));
}
};
}
}
Its the #test method for verification, im not sure how to create that. I cant see how the pact-mock-server can intercept the webcleint call.
There might be an assumption that Pact automatically intercepts requests - this is not the case.
So when you write a Pact unit test, you need to explicitly configure your API client to communicate to the Pact mock service, not the real thing.
Using this example as a basis, your test might look like this:
#ExtendWith(PactConsumerTestExt.class)
#PactTestFor(providerName = "orders-gateway")
public class OrdersPactTest {
#Pact(consumer="orders-provider")
public RequestResponsePact findOrders(PactDslWithProvider builder) {
PactDslJsonBody body = PactDslJsonArray.arrayEachLike()
.uuid("id", "5cc989d0-d800-434c-b4bb-b1268499e850")
.stringType("status", "STATUS")
.decimalType("amount", 100.0)
.closeObject();
return builder
.given("orders exist")
.uponReceiving("a request to find orders")
.path("/orders")
.method("GET")
.willRespondWith()
.status(200)
.body(body)
.toPact();
}
#PactTestFor(pactMethod = "findOrders")
#Test
public void findOrders(MockServer mockServer) throws IOException {
OrdersGateway orders = new OrdersGateway(mockServer.getUrl()).findOrders();
// do some assertions
}
}

How to handle exceptions thrown by the webclient?

I'm trying to figure out how to log exceptions from the webclient, whatever the error status code that is returned from the api that gets called.
I've seen the following implementation:
.onStatus(status -> status.value() != HttpStatus.OK.value(),
rs -> rs.bodyToMono(String.class).map(body -> new IOException(String.format(
"Response HTTP code is different from 200: %s, body: '%s'", rs.statusCode(), body))))
Another example I've seen uses a filter. I guess this filter could be used to log errors as well, aside from requests like in this example:
public MyClient(WebClient.Builder webClientBuilder) {
webClient = webClientBuilder // you can also just use WebClient.builder()
.baseUrl("https://httpbin.org")
.filter(logRequest()) // here is the magic
.build();
}
But are we serious that there is no dedicated exception handler to this thing?
Found it.
bodyToMono throws a WebClientException if the status code is 4xx (client error) or 5xx (Server error).
Full implementation of the service:
#Service
public class FacebookService {
private static final Logger LOG = LoggerFactory.getLogger(FacebookService.class);
private static final String URL_DEBUG = "https://graph.facebook.com/debug_token";
private WebClient webClient;
public FacebookService() {
webClient = WebClient.builder()
.filter(logRequest())
.build();
}
public Mono<DebugTokenResponse> verifyFbAccessToken(String fbAccessToken, String fbAppToken) {
LOG.info("verifyFacebookToken for " + String.format("fbAccessToken: %s and fbAppToken: %s", fbAccessToken, fbAppToken));
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(URL_DEBUG)
.queryParam("input_token", fbAccessToken)
.queryParam("access_token", fbAppToken);
return this.webClient.get()
.uri(builder.toUriString())
.retrieve()
.bodyToMono(DebugTokenResponse.class);
}
private static ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
LOG.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> LOG.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
LOG.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
}

Resources