Skip to content

Commit a9b3d95

Browse files
committed
Interpret empty mono from status handler as normal response
Prior to this commit, returning an empty mono from an exception handler registered through ResponseSpec::onStatus would result in memory leaks (since the response was not read) and in an empty response from bodyTo* methods of the webclient. As of this commit, that same empty mono is now interpreted to return the body (and not an exception), offering a way to override the default status handlers and return a normal response for 4xx and 5xx status codes.
1 parent b2b79ae commit a9b3d95

File tree

3 files changed

+111
-54
lines changed

3 files changed

+111
-54
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,6 @@ public HttpHeaders getHeaders() {
415415

416416
private static class DefaultResponseSpec implements ResponseSpec {
417417

418-
private static final StatusHandler DEFAULT_STATUS_HANDLER =
419-
new StatusHandler(HttpStatus::isError, ClientResponse::createException);
420-
421418
private final Mono<ClientResponse> responseMono;
422419

423420
private final Supplier<HttpRequest> requestSupplier;
@@ -427,72 +424,95 @@ private static class DefaultResponseSpec implements ResponseSpec {
427424
DefaultResponseSpec(Mono<ClientResponse> responseMono, Supplier<HttpRequest> requestSupplier) {
428425
this.responseMono = responseMono;
429426
this.requestSupplier = requestSupplier;
430-
this.statusHandlers.add(DEFAULT_STATUS_HANDLER);
427+
this.statusHandlers.add(new StatusHandler(HttpStatus::isError, ClientResponse::createException));
431428
}
432429

433430
@Override
434431
public ResponseSpec onStatus(Predicate<HttpStatus> statusPredicate,
435432
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
436433

437-
if (this.statusHandlers.size() == 1 && this.statusHandlers.get(0) == DEFAULT_STATUS_HANDLER) {
438-
this.statusHandlers.clear();
439-
}
440-
this.statusHandlers.add(new StatusHandler(statusPredicate, exceptionFunction));
434+
Assert.notNull(statusPredicate, "StatusPredicate must not be null");
435+
Assert.notNull(exceptionFunction, "Function must not be null");
441436

437+
this.statusHandlers.add(0, new StatusHandler(statusPredicate, exceptionFunction));
442438
return this;
443439
}
444440

445441
@Override
446442
public <T> Mono<T> bodyToMono(Class<T> elementClass) {
447-
return this.responseMono.flatMap(response -> handleBody(response,
448-
response.bodyToMono(elementClass), mono -> mono.flatMap(Mono::error)));
443+
Assert.notNull(elementClass, "ElementClass must not be null");
444+
return this.responseMono.flatMap(response -> handleBodyMono(response, response.bodyToMono(elementClass)));
449445
}
450446

451447
@Override
452448
public <T> Mono<T> bodyToMono(ParameterizedTypeReference<T> elementTypeRef) {
453-
return this.responseMono.flatMap(response ->
454-
handleBody(response, response.bodyToMono(elementTypeRef), mono -> mono.flatMap(Mono::error)));
449+
Assert.notNull(elementTypeRef, "ElementTypeRef must not be null");
450+
return this.responseMono.flatMap(response -> handleBodyMono(response, response.bodyToMono(elementTypeRef)));
451+
}
452+
453+
private <T> Mono<T> handleBodyMono(ClientResponse response, Mono<T> bodyPublisher) {
454+
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
455+
Mono<T> result = statusHandlers(response);
456+
if (result != null) {
457+
return result.switchIfEmpty(bodyPublisher);
458+
}
459+
else {
460+
return bodyPublisher;
461+
}
462+
}
463+
else {
464+
return response.createException().flatMap(Mono::error);
465+
}
455466
}
456467

457468
@Override
458469
public <T> Flux<T> bodyToFlux(Class<T> elementClass) {
459-
return this.responseMono.flatMapMany(response ->
460-
handleBody(response, response.bodyToFlux(elementClass), mono -> mono.handle((t, sink) -> sink.error(t))));
470+
Assert.notNull(elementClass, "ElementClass must not be null");
471+
return this.responseMono.flatMapMany(response -> handleBodyFlux(response, response.bodyToFlux(elementClass)));
461472
}
462473

463474
@Override
464475
public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementTypeRef) {
465-
return this.responseMono.flatMapMany(response ->
466-
handleBody(response, response.bodyToFlux(elementTypeRef), mono -> mono.handle((t, sink) -> sink.error(t))));
476+
Assert.notNull(elementTypeRef, "ElementTypeRef must not be null");
477+
return this.responseMono.flatMapMany(response -> handleBodyFlux(response, response.bodyToFlux(elementTypeRef)));
467478
}
468479

469-
private <T extends Publisher<?>> T handleBody(ClientResponse response,
470-
T bodyPublisher, Function<Mono<? extends Throwable>, T> errorFunction) {
471-
480+
private <T> Publisher<T> handleBodyFlux(ClientResponse response, Flux<T> bodyPublisher) {
472481
if (HttpStatus.resolve(response.rawStatusCode()) != null) {
473-
for (StatusHandler handler : this.statusHandlers) {
474-
if (handler.test(response.statusCode())) {
475-
Mono<? extends Throwable> exMono;
476-
try {
477-
exMono = handler.apply(response);
478-
exMono = exMono.flatMap(ex -> drainBody(response, ex));
479-
exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
480-
}
481-
catch (Throwable ex2) {
482-
exMono = drainBody(response, ex2);
483-
}
484-
T result = errorFunction.apply(exMono);
485-
HttpRequest request = this.requestSupplier.get();
486-
return insertCheckpoint(result, response.statusCode(), request);
487-
}
482+
Mono<T> result = statusHandlers(response);
483+
if (result != null) {
484+
return result.flux().switchIfEmpty(bodyPublisher);
485+
}
486+
else {
487+
return bodyPublisher;
488488
}
489-
return bodyPublisher;
490489
}
491490
else {
492-
return errorFunction.apply(response.createException());
491+
return response.createException().flatMap(Mono::error);
493492
}
494493
}
495494

495+
@Nullable
496+
private <T> Mono<T> statusHandlers(ClientResponse response) {
497+
for (StatusHandler handler : this.statusHandlers) {
498+
if (handler.test(response.statusCode())) {
499+
Mono<? extends Throwable> exMono;
500+
try {
501+
exMono = handler.apply(response);
502+
exMono = exMono.flatMap(ex -> drainBody(response, ex));
503+
exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
504+
}
505+
catch (Throwable ex2) {
506+
exMono = drainBody(response, ex2);
507+
}
508+
Mono<T> result = exMono.flatMap(Mono::error);
509+
HttpRequest request = this.requestSupplier.get();
510+
return insertCheckpoint(result, response.statusCode(), request);
511+
}
512+
}
513+
return null;
514+
}
515+
496516
@SuppressWarnings("unchecked")
497517
private <T> Mono<T> drainBody(ClientResponse response, Throwable ex) {
498518
// Ensure the body is drained, even if the StatusHandler didn't consume it,
@@ -501,20 +521,11 @@ private <T> Mono<T> drainBody(ClientResponse response, Throwable ex) {
501521
.onErrorResume(ex2 -> Mono.empty()).thenReturn(ex);
502522
}
503523

504-
@SuppressWarnings("unchecked")
505-
private <T extends Publisher<?>> T insertCheckpoint(T result, HttpStatus status, HttpRequest request) {
524+
private <T> Mono<T> insertCheckpoint(Mono<T> result, HttpStatus status, HttpRequest request) {
506525
String httpMethod = request.getMethodValue();
507526
URI uri = request.getURI();
508527
String description = status + " from " + httpMethod + " " + uri + " [DefaultWebClient]";
509-
if (result instanceof Mono) {
510-
return (T) ((Mono<?>) result).checkpoint(description);
511-
}
512-
else if (result instanceof Flux) {
513-
return (T) ((Flux<?>) result).checkpoint(description);
514-
}
515-
else {
516-
return result;
517-
}
528+
return result.checkpoint(description);
518529
}
519530

520531

@@ -527,8 +538,6 @@ private static class StatusHandler {
527538
public StatusHandler(Predicate<HttpStatus> predicate,
528539
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
529540

530-
Assert.notNull(predicate, "Predicate must not be null");
531-
Assert.notNull(exceptionFunction, "Function must not be null");
532541
this.predicate = predicate;
533542
this.exceptionFunction = exceptionFunction;
534543
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -671,17 +671,21 @@ interface ResponseSpec {
671671

672672
/**
673673
* Register a custom error function that gets invoked when the given {@link HttpStatus}
674-
* predicate applies. The exception returned from the function will be returned from
675-
* {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}.
676-
* <p>By default, an error handler is register that throws a
674+
* predicate applies. Whatever exception is returned from the function (possibly using
675+
* {@link ClientResponse#createException()}) will also be returned as error signal
676+
* from {@link #bodyToMono(Class)} and {@link #bodyToFlux(Class)}.
677+
* <p>By default, an error handler is registered that returns a
677678
* {@link WebClientResponseException} when the response status code is 4xx or 5xx.
678-
* @param statusPredicate a predicate that indicates whether {@code exceptionFunction}
679-
* applies
679+
* To override this default (and return a non-error response from {@code bodyOn*}), register
680+
* an exception function that returns an {@linkplain Mono#empty() empty} mono.
680681
* <p><strong>NOTE:</strong> if the response is expected to have content,
681682
* the exceptionFunction should consume it. If not, the content will be
682683
* automatically drained to ensure resources are released.
684+
* @param statusPredicate a predicate that indicates whether {@code exceptionFunction}
685+
* applies
683686
* @param exceptionFunction the function that returns the exception
684687
* @return this builder
688+
* @see ClientResponse#createException()
685689
*/
686690
ResponseSpec onStatus(Predicate<HttpStatus> statusPredicate,
687691
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction);

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,50 @@ public void shouldApplyCustomStatusHandlerParameterizedTypeReference() {
626626
});
627627
}
628628

629+
@Test
630+
public void emptyStatusHandlerShouldReturnBody() {
631+
prepareResponse(response -> response.setResponseCode(500)
632+
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
633+
634+
Mono<String> result = this.webClient.get()
635+
.uri("/greeting?name=Spring")
636+
.retrieve()
637+
.onStatus(HttpStatus::is5xxServerError, response -> Mono.empty())
638+
.bodyToMono(String.class);
639+
640+
StepVerifier.create(result)
641+
.expectNext("Internal Server error")
642+
.verifyComplete();
643+
644+
expectRequestCount(1);
645+
expectRequest(request -> {
646+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("*/*");
647+
assertThat(request.getPath()).isEqualTo("/greeting?name=Spring");
648+
});
649+
}
650+
651+
@Test
652+
public void emptyStatusHandlerShouldReturnBodyFlux() {
653+
prepareResponse(response -> response.setResponseCode(500)
654+
.setHeader("Content-Type", "text/plain").setBody("Internal Server error"));
655+
656+
Flux<String> result = this.webClient.get()
657+
.uri("/greeting?name=Spring")
658+
.retrieve()
659+
.onStatus(HttpStatus::is5xxServerError, response -> Mono.empty())
660+
.bodyToFlux(String.class);
661+
662+
StepVerifier.create(result)
663+
.expectNext("Internal Server error")
664+
.verifyComplete();
665+
666+
expectRequestCount(1);
667+
expectRequest(request -> {
668+
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("*/*");
669+
assertThat(request.getPath()).isEqualTo("/greeting?name=Spring");
670+
});
671+
}
672+
629673
@Test
630674
public void shouldReceiveNotFoundEntity() {
631675
prepareResponse(response -> response.setResponseCode(404)

0 commit comments

Comments
 (0)