While playing around with Spring Webflux and WebClient, I noted a behavior (demonstrated by the code below) when returning a ServerResponse containing a Flux<String>
. Unless the String elements are terminated with a newline character, returning the Flux<String>
via a ServerResponse appears to concatenate all Flux<String>
elements into a single String. Can someone explain to me why I'm seeing this behavior, and what I am doing to cause it?
When each String element is terminated with a newline character, the Flux<String>
is "returned as expected" via ServerResponse, and subscribing to the returned Flux<String>
produces expected results. However, if viewed as simple JSON (via Postman), this also results in an extra, empty, String element being returned in the JSON body.
Console output showing described behavior...
Flux<String>
containing 10 String elements, where the second occurrence of each String value is terminated with a newline character, resulting in a blank line being output.The second listing of String elements occurs in StringClient.getAll(), and demonstrates how the original String elements that were not terminated with a newline character have been concatenated with the following element.
2019-10-10 10:13:37.225 INFO 8748 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080 2019-10-10 10:13:37.228 INFO 8748 --- [ main] c.example.fluxtest.FluxTestApplication : Started FluxTestApplication in 1.271 seconds (JVM running for 1.796)
***** "Get" issued on http:/localhost:8080/StringClient/String StringClientHandler.getAll( ServerRequest ) StringClient.getAll() StringProducerHandler.getAll( ServerRequest ) ListElement-0 ListElement-0
ListElement-1 ListElement-1
ListElement-2 ListElement-2
ListElement-3 ListElement-3
ListElement-4 ListElement-4
ListElement-0ListElement-0 @ 1570727628948 ListElement-1ListElement-1 @ 1570727628948 ListElement-2ListElement-2 @ 1570727628948 ListElement-3ListElement-3 @ 1570727628948 ListElement-4ListElement-4 @ 1570727628949
Code to replicate this behavior is provided below...
@SpringBootApplication
public class FluxTestApplication {
public static void main(String[] args) {
SpringApplication.run(FluxTestApplication.class, args);
}
}
@Configuration
public class StringClientRouter {
@Bean
public RouterFunction<ServerResponse> clientRoutes(StringClientHandler requestHandler) {
return nest(path("/StringClient"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.GET("/String"), requestHandler::getAll)));
}
}
@Component
public class StringClientHandler {
@Autowired
StringClient stringClient;
public Mono<ServerResponse> getAll(ServerRequest request) {
System.out.println("StringClientHandler.getAll( ServerRequest )");
Mono<Void> signal = stringClient.getAll();
return ServerResponse.ok().build();
}
}
@Component
public class StringClient {
private final WebClient client;
public StringClient() {
client = WebClient.create();
}
public Mono<Void> getAll() {
System.out.println("StringClient.getAll()");
// break chain to explicitly obtain the ClientResponse
Mono<ClientResponse> monoCR = client.get().uri("http://localhost:8080/StringProducer/String")
.accept(MediaType.APPLICATION_JSON)
.exchange();
// extract the Flux<String> and print to console
Flux<String> fluxString = monoCR.flatMapMany(response -> response.bodyToFlux(String.class));
// this statement iterates over the Flux<String> and outputs each element
fluxString.subscribe(strVal -> System.out.println(strVal + " @ " + System.currentTimeMillis()));
return Mono.empty();
}
}
@Configuration
public class StringProducerRouter {
@Bean
public RouterFunction<ServerResponse> demoPOJORoute(StringProducerHandler requestHandler) {
return nest(path("/StringProducer"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.GET("/String"), requestHandler::getAll)));
}
}
@Component
public class StringProducerHandler {
public Mono<ServerResponse> getAll(ServerRequest request) {
System.out.println("StringProducerHandler.getAll( ServerRequest )");
int listSize = 5;
List<String> strList = new ArrayList<String>();
for (int i=0; i<listSize; i++) {
strList.add("ListElement-" + i); // add String value without newline termination
strList.add("ListElement-" + i + "\n"); // add String value with newline termination
}
// this statement produces the expected console output of String values
Flux.fromIterable(strList).subscribe(System.out::println);
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Flux.fromIterable(strList), String.class);
}
}
This is due to how org.springframework.core.codec.StringDecoder
works.
When you call response.bodyToFlux(String.class)
the response body is transformed into a Flux
of String
s. The org.springframework.core.codec.StringDecoder
does the heavy lifting and has the opinion that it should split on default delimiters.
List<byte[]> delimiterBytes = getDelimiterBytes(mimeType);
Flux<DataBuffer> inputFlux = Flux.from(input)
.flatMapIterable(buffer -> splitOnDelimiter(buffer, delimiterBytes))
.bufferUntil(buffer -> buffer == END_FRAME)
.map(StringDecoder::joinUntilEndFrame)
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
return super.decode(inputFlux, elementType, mimeType, hints);
The default delimiters are public static final List<String> DEFAULT_DELIMITERS = Arrays.asList("\r\n", "\n");
Therefore you get:
ListElement-0ListElement-0 @ 1570732000374
ListElement-1ListElement-1 @ 1570732000375
...
instead of
ListElement-0 @ 1570732055461
ListElement-0 @ 1570732055461
ListElement-1 @ 1570732055462
ListElement-1 @ 1570732055462
...