Skip to content

Commit a04e805

Browse files
committed
Expose status handler at WebClient.Builder level
See gh-28533
1 parent 4ed581c commit a04e805

File tree

4 files changed

+100
-9
lines changed

4 files changed

+100
-9
lines changed

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.ZonedDateTime;
2222
import java.util.ArrayList;
2323
import java.util.Arrays;
24+
import java.util.Collections;
2425
import java.util.LinkedHashMap;
2526
import java.util.List;
2627
import java.util.Map;
@@ -29,6 +30,7 @@
2930
import java.util.function.IntPredicate;
3031
import java.util.function.Predicate;
3132
import java.util.function.Supplier;
33+
import java.util.stream.Collectors;
3234

3335
import org.reactivestreams.Publisher;
3436
import reactor.core.publisher.Flux;
@@ -84,21 +86,35 @@ class DefaultWebClient implements WebClient {
8486
@Nullable
8587
private final Consumer<RequestHeadersSpec<?>> defaultRequest;
8688

89+
private final List<DefaultResponseSpec.StatusHandler> defaultStatusHandlers;
90+
8791
private final DefaultWebClientBuilder builder;
8892

8993

9094
DefaultWebClient(ExchangeFunction exchangeFunction, UriBuilderFactory uriBuilderFactory,
9195
@Nullable HttpHeaders defaultHeaders, @Nullable MultiValueMap<String, String> defaultCookies,
92-
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest, DefaultWebClientBuilder builder) {
96+
@Nullable Consumer<RequestHeadersSpec<?>> defaultRequest,
97+
@Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlerMap,
98+
DefaultWebClientBuilder builder) {
9399

94100
this.exchangeFunction = exchangeFunction;
95101
this.uriBuilderFactory = uriBuilderFactory;
96102
this.defaultHeaders = defaultHeaders;
97103
this.defaultCookies = defaultCookies;
98104
this.defaultRequest = defaultRequest;
105+
this.defaultStatusHandlers = initStatusHandlers(statusHandlerMap);
99106
this.builder = builder;
100107
}
101108

109+
private static List<DefaultResponseSpec.StatusHandler> initStatusHandlers(
110+
@Nullable Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> handlerMap) {
111+
112+
return (CollectionUtils.isEmpty(handlerMap) ? Collections.emptyList() :
113+
handlerMap.entrySet().stream()
114+
.map(entry -> new DefaultResponseSpec.StatusHandler(entry.getKey(), entry.getValue()))
115+
.collect(Collectors.toList()));
116+
};
117+
102118

103119
@Override
104120
public RequestHeadersUriSpec<?> get() {
@@ -365,7 +381,8 @@ public RequestHeadersSpec<?> syncBody(Object body) {
365381

366382
@Override
367383
public ResponseSpec retrieve() {
368-
return new DefaultResponseSpec(exchange(), this::createRequest);
384+
return new DefaultResponseSpec(
385+
exchange(), this::createRequest, DefaultWebClient.this.defaultStatusHandlers);
369386
}
370387

371388
private HttpRequest createRequest() {
@@ -502,11 +519,18 @@ private static class DefaultResponseSpec implements ResponseSpec {
502519

503520
private final List<StatusHandler> statusHandlers = new ArrayList<>(1);
504521

522+
private final int defaultStatusHandlerCount;
523+
524+
525+
DefaultResponseSpec(
526+
Mono<ClientResponse> responseMono, Supplier<HttpRequest> requestSupplier,
527+
List<StatusHandler> defaultStatusHandlers) {
505528

506-
DefaultResponseSpec(Mono<ClientResponse> responseMono, Supplier<HttpRequest> requestSupplier) {
507529
this.responseMono = responseMono;
508530
this.requestSupplier = requestSupplier;
531+
this.statusHandlers.addAll(defaultStatusHandlers);
509532
this.statusHandlers.add(DEFAULT_STATUS_HANDLER);
533+
this.defaultStatusHandlerCount = this.statusHandlers.size();
510534
}
511535

512536

@@ -516,10 +540,9 @@ public ResponseSpec onStatus(Predicate<HttpStatusCode> statusCodePredicate,
516540

517541
Assert.notNull(statusCodePredicate, "StatusCodePredicate must not be null");
518542
Assert.notNull(exceptionFunction, "Function must not be null");
519-
int index = this.statusHandlers.size() - 1; // Default handler always last
543+
int index = this.statusHandlers.size() - this.defaultStatusHandlerCount; // Default handlers always last
520544
this.statusHandlers.add(index, new StatusHandler(statusCodePredicate, exceptionFunction));
521545
return this;
522-
523546
}
524547

525548
@Override

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,8 +22,13 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.function.Consumer;
25+
import java.util.function.Function;
26+
import java.util.function.Predicate;
27+
28+
import reactor.core.publisher.Mono;
2529

2630
import org.springframework.http.HttpHeaders;
31+
import org.springframework.http.HttpStatusCode;
2732
import org.springframework.http.client.reactive.ClientHttpConnector;
2833
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
2934
import org.springframework.http.client.reactive.JdkClientHttpConnector;
@@ -82,6 +87,9 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
8287
@Nullable
8388
private Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest;
8489

90+
@Nullable
91+
private Map<Predicate<HttpStatusCode>, Function<ClientResponse, Mono<? extends Throwable>>> statusHandlers;
92+
8593
@Nullable
8694
private List<ExchangeFilterFunction> filters;
8795

@@ -120,6 +128,7 @@ public DefaultWebClientBuilder(DefaultWebClientBuilder other) {
120128
this.defaultCookies = (other.defaultCookies != null ?
121129
new LinkedMultiValueMap<>(other.defaultCookies) : null);
122130
this.defaultRequest = other.defaultRequest;
131+
this.statusHandlers = (other.statusHandlers != null ? new LinkedHashMap<>(other.statusHandlers) : null);
123132
this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null);
124133

125134
this.connector = other.connector;
@@ -193,6 +202,15 @@ public WebClient.Builder defaultRequest(Consumer<WebClient.RequestHeadersSpec<?>
193202
return this;
194203
}
195204

205+
@Override
206+
public WebClient.Builder defaultStatusHandler(Predicate<HttpStatusCode> statusPredicate,
207+
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
208+
209+
this.statusHandlers = (this.statusHandlers != null ? this.statusHandlers : new LinkedHashMap<>());
210+
this.statusHandlers.put(statusPredicate, exceptionFunction);
211+
return this;
212+
}
213+
196214
@Override
197215
public WebClient.Builder filter(ExchangeFilterFunction filter) {
198216
Assert.notNull(filter, "ExchangeFilterFunction must not be null");
@@ -282,7 +300,9 @@ public WebClient build() {
282300
return new DefaultWebClient(filteredExchange, initUriBuilderFactory(),
283301
defaultHeaders,
284302
defaultCookies,
285-
this.defaultRequest, new DefaultWebClientBuilder(this));
303+
this.defaultRequest,
304+
this.statusHandlers,
305+
new DefaultWebClientBuilder(this));
286306
}
287307

288308
private ClientHttpConnector initConnector() {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -255,6 +255,20 @@ interface Builder {
255255
*/
256256
Builder defaultRequest(Consumer<RequestHeadersSpec<?>> defaultRequest);
257257

258+
/**
259+
* Register a default
260+
* {@link ResponseSpec#onStatus(Predicate, Function) status handler} to
261+
* apply to every response. Such default handlers are applied in the
262+
* order in which they are registered, and after any others that are
263+
* registered for a specific response.
264+
* @param statusPredicate to match responses with
265+
* @param exceptionFunction to map the response to an error signal
266+
* @return this builder
267+
* @since 6.0
268+
*/
269+
Builder defaultStatusHandler(Predicate<HttpStatusCode> statusPredicate,
270+
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction);
271+
258272
/**
259273
* Add the given filter to the end of the filter chain.
260274
* @param filter the filter to be added to the chain

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -423,6 +423,40 @@ public void onStatusHandlersOrderIsPreserved() {
423423
StepVerifier.create(result).expectErrorMessage("1").verify();
424424
}
425425

426+
@Test
427+
public void onStatusHandlerRegisteredGlobally() {
428+
429+
ClientResponse response = ClientResponse.create(HttpStatus.BAD_REQUEST).build();
430+
given(exchangeFunction.exchange(any())).willReturn(Mono.just(response));
431+
432+
Mono<Void> result = this.builder
433+
.defaultStatusHandler(HttpStatusCode::is4xxClientError, resp -> Mono.error(new IllegalStateException("1")))
434+
.defaultStatusHandler(HttpStatusCode::is4xxClientError, resp -> Mono.error(new IllegalStateException("2")))
435+
.build().get()
436+
.uri("/path")
437+
.retrieve()
438+
.bodyToMono(Void.class);
439+
440+
StepVerifier.create(result).expectErrorMessage("1").verify();
441+
}
442+
443+
@Test
444+
public void onStatusHandlerRegisteredGloballyHaveLowerPrecedence() {
445+
446+
ClientResponse response = ClientResponse.create(HttpStatus.BAD_REQUEST).build();
447+
given(exchangeFunction.exchange(any())).willReturn(Mono.just(response));
448+
449+
Mono<Void> result = this.builder
450+
.defaultStatusHandler(HttpStatusCode::is4xxClientError, resp -> Mono.error(new IllegalStateException("1")))
451+
.build().get()
452+
.uri("/path")
453+
.retrieve()
454+
.onStatus(HttpStatusCode::is4xxClientError, resp -> Mono.error(new IllegalStateException("2")))
455+
.bodyToMono(Void.class);
456+
457+
StepVerifier.create(result).expectErrorMessage("2").verify();
458+
}
459+
426460
@Test // gh-23880
427461
@SuppressWarnings("unchecked")
428462
public void onStatusHandlersDefaultHandlerIsLast() {

0 commit comments

Comments
 (0)