Skip to content

Commit 9ded949

Browse files
feat: Added asynchronous methods and context propagation (#41)
* updated version * added asynchronous methods and context propagation * updated linting * updated linting * removed comments * testing in PR Build * Revert "testing in PR Build" This reverts commit ad66b57. * updated few log levels to debug * Added Exception in the ClientHttpResponse * Added Exception in the ClientHttpResponse --------- Co-authored-by: ssayyaparaj <sainikhitha_sayyaparaju@intuit.com>
1 parent 029108b commit 9ded949

File tree

4 files changed

+685
-197
lines changed

4 files changed

+685
-197
lines changed

pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>com.intuit.rwebpulse</groupId>
55
<artifactId>rwebpulse</artifactId>
6-
<version>1.0.3</version>
6+
<version>1.0.4</version>
77
<packaging>jar</packaging>
88
<name>Spring Web Client</name>
99
<url>https://github.com/intuit/rwebpulse</url>
@@ -73,6 +73,11 @@
7373
<artifactId>spring-boot-starter-webflux</artifactId>
7474
<version>${spring-webflux.version}</version>
7575
</dependency>
76+
<dependency>
77+
<groupId>io.projectreactor</groupId>
78+
<artifactId>reactor-test</artifactId>
79+
<scope>test</scope>
80+
</dependency>
7681

7782

7883
</dependencies>
Lines changed: 170 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,201 @@
11
package com.intuit.springwebclient.client;
22

3+
import com.intuit.springwebclient.entity.ClientHttpRequest;
4+
import com.intuit.springwebclient.entity.ClientHttpResponse;
5+
import com.intuit.springwebclient.retryHandler.RetryHandlerFactory;
36
import java.time.Duration;
7+
import java.util.Map;
48
import java.util.Objects;
9+
import java.util.Optional;
510
import java.util.function.Consumer;
6-
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.slf4j.MDC;
713
import org.springframework.beans.factory.annotation.Qualifier;
814
import org.springframework.http.HttpHeaders;
915
import org.springframework.http.HttpStatus;
1016
import org.springframework.http.ResponseEntity;
1117
import org.springframework.stereotype.Component;
12-
import org.springframework.web.client.HttpStatusCodeException;
13-
import org.springframework.web.client.UnknownContentTypeException;
1418
import org.springframework.web.reactive.function.client.WebClient;
1519
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
1620
import org.springframework.web.reactive.function.client.WebClientResponseException;
17-
18-
import com.intuit.springwebclient.entity.ClientHttpRequest;
19-
import com.intuit.springwebclient.entity.ClientHttpResponse;
20-
import com.intuit.springwebclient.retryHandler.RetryHandlerFactory;
21-
22-
import lombok.extern.slf4j.Slf4j;
2321
import reactor.core.publisher.Mono;
2422
import reactor.util.retry.Retry;
2523

2624
/**
27-
* Spring 5 Web Client Method executor.
25+
* Spring 5 Web Client Method executor with MDC propagation for retries.
2826
*/
2927
@Slf4j
3028
@Component
3129
public class CommonSpringWebClient {
32-
private final WebClient webClient;
33-
34-
public CommonSpringWebClient(@Qualifier("RWebPulseClient") WebClient webClient) {
35-
this.webClient = webClient;
36-
}
3730

38-
/**
39-
* Execute Blocking http request.
40-
* @param httpRequest
41-
* @return
42-
* @param <REQUEST>
43-
* @param <RESPONSE>
44-
*/
45-
public <REQUEST, RESPONSE> ClientHttpResponse<RESPONSE> syncHttpResponse(ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
46-
try {
47-
log.info("Executing http request for request={}, method={}", httpRequest.getRequest(),
48-
httpRequest.getHttpMethod());
49-
return generateResponseSpec(httpRequest).toEntity(httpRequest.getResponseType())
50-
.map(this::generateResponse).retryWhen(generateRetrySpec(httpRequest)).block();
51-
} catch (final WebClientResponseException ex) {
52-
final String errorMessage = String.format("Error in making rest call. Error=%s Headers=%s statusCode=%s",
53-
ex.getResponseBodyAsString(), ex.getHeaders(), ex.getStatusCode());
54-
return handleException(ex, errorMessage, ex.getResponseBodyAsString(),
55-
HttpStatus.valueOf(ex.getStatusCode().value()), httpRequest);
56-
} catch (final HttpStatusCodeException ex) {
57-
final String errorMessage = String.format("Error in making rest call. Error=%s Headers=%s statusCode=%s",
58-
ex.getResponseBodyAsString(), ex.getResponseHeaders(), ex.getStatusCode());
59-
return handleException(ex, errorMessage, ex.getResponseBodyAsString(),
60-
HttpStatus.valueOf(ex.getStatusCode().value()), httpRequest);
61-
} catch (final UnknownContentTypeException ex) {
62-
// It was observed that this exception was thrown whenever there was a HTTP 5XX error
63-
// returned in the REST call. The handle went into `RestClientException` which is the parent
64-
// class of `UnknownContentTypeException` and hence some contextual information was lost
65-
final String errorMessage = String.format("Error in making rest call. Error=%s Headers=%s",
66-
ex.getResponseBodyAsString(), ex.getResponseHeaders());
67-
return handleException(ex, errorMessage, ex.getResponseBodyAsString(),
68-
HttpStatus.valueOf(ex.getRawStatusCode()), httpRequest);
69-
} catch (final Exception ex) {
70-
final String errorMessage = String
71-
.format("Error in making rest call. Error=%s", ex.getMessage());
72-
return handleException(ex, errorMessage, null, HttpStatus.INTERNAL_SERVER_ERROR, httpRequest);
73-
}
74-
}
31+
private final WebClient webClient;
7532

76-
/**
77-
* Generate Web Client Response spec from http request.
78-
*
79-
* @param httpRequest
80-
* @return
81-
*/
82-
private <REQUEST, RESPONSE> WebClient.ResponseSpec generateResponseSpec(
83-
ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
33+
// --- Constant for MDC Context Key ---
34+
// This key is used to store and retrieve the MDC map from Reactor's Context
35+
private static final String MDC_CONTEXT_KEY = "mdcContextMap";
8436

85-
Consumer<HttpHeaders> httpHeadersConsumer = (httpHeaders -> httpHeaders
86-
.putAll(httpRequest.getRequestHeaders()));
87-
RequestBodySpec webClientBuilder = webClient.method(httpRequest.getHttpMethod()).uri(httpRequest.getUrl())
88-
.headers(httpHeadersConsumer);
37+
public CommonSpringWebClient(@Qualifier("RWebPulseClient") WebClient webClient) {
38+
this.webClient = webClient;
39+
}
8940

90-
// set only when provided
91-
if (Objects.nonNull(httpRequest.getRequest()) && Objects.nonNull(httpRequest.getRequestType())) {
92-
webClientBuilder.body(Mono.just(httpRequest.getRequest()), httpRequest.getRequestType());
93-
}
41+
/**
42+
* Executes a blocking HTTP request with WebClient, supporting retries and MDC propagation across
43+
* thread changes.
44+
*
45+
* @param httpRequest The client HTTP request details.
46+
* @param <REQUEST> Type of the request body.
47+
* @param <RESPONSE> Type of the response body.
48+
* @return ClientHttpResponse containing the response or error details.
49+
*/
50+
public <REQUEST, RESPONSE> ClientHttpResponse<RESPONSE> syncHttpResponse(
51+
ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
52+
return asyncHttpResponse(httpRequest).block();
53+
}
9454

95-
return webClientBuilder.retrieve();
55+
/**
56+
* Executes a non-blocking HTTP request with WebClient, supporting retries and MDC propagation
57+
* across thread changes. Returns a Mono for reactive programming.
58+
*
59+
* @param httpRequest The client HTTP request details.
60+
* @param <REQUEST> Type of the request body.
61+
* @param <RESPONSE> Type of the response body.
62+
* @return Mono<ClientHttpResponse < RESPONSE>> containing the response or error details.
63+
*/
64+
public <REQUEST, RESPONSE> Mono<ClientHttpResponse<RESPONSE>> asyncHttpResponse(
65+
ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
66+
final Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
67+
log.debug("asyncHttpResponse initiated. Captured MDC from calling thread: {}", mdcContextMap);
9668

97-
}
69+
return generateResponseSpec(httpRequest)
70+
.toEntity(httpRequest.getResponseType())
71+
.map(this::generateResponse)
72+
.retryWhen(generateRetrySpec(httpRequest))
73+
.contextWrite(ctx -> {
74+
if (mdcContextMap != null) {
75+
return ctx.put(MDC_CONTEXT_KEY, mdcContextMap);
76+
}
77+
return ctx;
78+
})
79+
.doOnEach(signal -> {
80+
Optional<Map<String, String>> contextFromReactor = signal.getContextView()
81+
.getOrEmpty(MDC_CONTEXT_KEY);
82+
contextFromReactor.ifPresent(MDC::setContextMap);
83+
})
84+
.onErrorResume(WebClientResponseException.class, ex -> {
85+
final String errorMessage = String.format(
86+
"Error in WebClient call (ResponseException). Error=%s Headers=%s statusCode=%s",
87+
ex.getResponseBodyAsString(), ex.getHeaders(), ex.getStatusCode());
88+
return Mono.just(handleExceptionInternal(ex, errorMessage, ex.getResponseBodyAsString(),
89+
HttpStatus.valueOf(ex.getStatusCode().value()), httpRequest));
90+
})
91+
.onErrorResume(org.springframework.web.client.HttpStatusCodeException.class, ex -> {
92+
final String errorMessage = String.format(
93+
"Error in WebClient call (HttpStatusCodeException). Error=%s Headers=%s statusCode=%s",
94+
ex.getResponseBodyAsString(), ex.getResponseHeaders(), ex.getStatusCode());
95+
return Mono.just(handleExceptionInternal(ex, errorMessage, ex.getResponseBodyAsString(),
96+
HttpStatus.valueOf(ex.getStatusCode().value()), httpRequest));
97+
})
98+
.onErrorResume(org.springframework.web.client.UnknownContentTypeException.class, ex -> {
99+
final String errorMessage = String.format(
100+
"Error in WebClient call (UnknownContentTypeException). Error=%s Headers=%s",
101+
ex.getResponseBodyAsString(), ex.getResponseHeaders());
102+
return Mono.just(handleExceptionInternal(ex, errorMessage, ex.getResponseBodyAsString(),
103+
HttpStatus.valueOf(ex.getRawStatusCode()), httpRequest));
104+
})
105+
.onErrorResume(Exception.class, ex -> { // Catch any other unexpected exceptions
106+
final String errorMessage = String.format(
107+
"Unhandled exception in WebClient call. Error=%s Cause=%s", ex.getMessage(),
108+
ex.getCause());
109+
return Mono.just(
110+
handleExceptionInternal(ex, errorMessage, null, HttpStatus.INTERNAL_SERVER_ERROR,
111+
httpRequest));
112+
})
113+
.doFinally(signalType -> {
114+
MDC.clear();
115+
log.debug("MDC cleared after reactive chain completion (signal type: {}).", signalType);
116+
});
117+
}
98118

99-
/**
100-
* Generates retry spec for the request based on config provided.
101-
* @param httpRequest
102-
* @return
103-
*/
104-
private <REQUEST, RESPONSE> Retry generateRetrySpec(ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
105-
return Retry
106-
.fixedDelay(httpRequest.getClientRetryConfig().getMaxAttempts(),
107-
Duration.ofSeconds(httpRequest.getClientRetryConfig().getBackOff()))
108-
.doBeforeRetry(signal -> log.info("Retrying for requestUrl={}, retryCount={}", httpRequest.getUrl(),
109-
signal.totalRetries()))
110-
.filter(httpRequest.getClientRetryConfig().getRetryFilter());
111-
}
119+
/**
120+
* Generates WebClient ResponseSpec from the ClientHttpRequest.
121+
*
122+
* @param httpRequest The client HTTP request details.
123+
* @return WebClient.ResponseSpec ready for retrieval.
124+
*/
125+
private <REQUEST, RESPONSE> WebClient.ResponseSpec generateResponseSpec(
126+
ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
112127

113-
/**
114-
* Handle Success response.
115-
*
116-
* @param response
117-
* @return
118-
* @param <RESPONSE>
119-
*/
120-
private <RESPONSE> ClientHttpResponse<RESPONSE> generateResponse(ResponseEntity<RESPONSE> response) {
121-
return ClientHttpResponse.<RESPONSE>builder().response(response.getBody()).status(response.getStatusCode())
122-
.isSuccess2xx(response.getStatusCode().is2xxSuccessful()).build();
123-
}
128+
Consumer<HttpHeaders> httpHeadersConsumer = (httpHeaders -> httpHeaders
129+
.putAll(httpRequest.getRequestHeaders()));
130+
RequestBodySpec webClientBuilder = webClient.method(httpRequest.getHttpMethod())
131+
.uri(httpRequest.getUrl())
132+
.headers(httpHeadersConsumer);
124133

125-
/**
126-
* Handle Exception and send back response.
127-
* @param exception
128-
* @param errorMessage
129-
* @param httpStatus
130-
* @param httpRequest
131-
* @return
132-
* @param <RESPONSE>
133-
*/
134-
private <REQUEST, RESPONSE> ClientHttpResponse<RESPONSE> handleException(
135-
final Exception exception,
136-
final String errorMessage,
137-
final String responseBody,
138-
final HttpStatus httpStatus,
139-
final ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
140-
log.error("Exception while executing http request for requestUrl={}, status={}, errorMessage={}", httpRequest.getUrl(), httpStatus, errorMessage);
141-
httpRequest.getRetryHandlers()
142-
.forEach(handlerId -> RetryHandlerFactory.getHandler(handlerId.toString()).checkAndThrowRetriableException(exception));
143-
return ClientHttpResponse.<RESPONSE>builder().error(responseBody).status(httpStatus).build();
134+
if (Objects.nonNull(httpRequest.getRequest()) && Objects.nonNull(
135+
httpRequest.getRequestType())) {
136+
webClientBuilder.body(Mono.just(httpRequest.getRequest()), httpRequest.getRequestType());
144137
}
145-
}
138+
139+
return webClientBuilder.retrieve();
140+
}
141+
142+
/**
143+
* Generates retry specification for the request based on config provided.
144+
*
145+
* @param httpRequest The client HTTP request details including retry configuration.
146+
* @return Reactor Retry specification.
147+
*/
148+
private <REQUEST, RESPONSE> Retry generateRetrySpec(
149+
ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
150+
return Retry
151+
.fixedDelay(httpRequest.getClientRetryConfig().getMaxAttempts(),
152+
Duration.ofSeconds(httpRequest.getClientRetryConfig().getBackOff()))
153+
.doBeforeRetry(signal -> {
154+
log.info("Retrying for requestUrl={}, retryCount={}",
155+
httpRequest.getUrl(), signal.totalRetries());
156+
})
157+
.filter(httpRequest.getClientRetryConfig().getRetryFilter());
158+
}
159+
160+
/**
161+
* Handles a successful HTTP response, transforming it into ClientHttpResponse.
162+
*
163+
* @param response The ResponseEntity from WebClient.
164+
* @param <RESPONSE> Type of the response body.
165+
* @return ClientHttpResponse indicating success.
166+
*/
167+
private <RESPONSE> ClientHttpResponse<RESPONSE> generateResponse(
168+
ResponseEntity<RESPONSE> response) {
169+
return ClientHttpResponse.<RESPONSE>builder().response(response.getBody())
170+
.status(response.getStatusCode())
171+
.isSuccess2xx(response.getStatusCode().is2xxSuccessful()).build();
172+
}
173+
174+
/**
175+
* Internal method to handle exceptions and build an error ClientHttpResponse. This is now called
176+
* from within the `onErrorResume` operators in the reactive chain.
177+
*
178+
* @param exception The exception that occurred.
179+
* @param errorMessage Formatted error message.
180+
* @param responseBody Raw response body if available.
181+
* @param httpStatus HTTP status of the error.
182+
* @param httpRequest The original HTTP request.
183+
* @param <RESPONSE> Type of the response body.
184+
* @return ClientHttpResponse with error details.
185+
*/
186+
private <REQUEST, RESPONSE> ClientHttpResponse<RESPONSE> handleExceptionInternal(
187+
final Exception exception,
188+
final String errorMessage,
189+
final String responseBody,
190+
final HttpStatus httpStatus,
191+
final ClientHttpRequest<REQUEST, RESPONSE> httpRequest) {
192+
log.error(
193+
"Exception while executing http request for requestUrl={}, status={}, errorMessage={}",
194+
httpRequest.getUrl(), httpStatus, errorMessage,
195+
exception); // Include 'exception' for stack trace
196+
httpRequest.getRetryHandlers()
197+
.forEach(handlerId -> RetryHandlerFactory.getHandler(handlerId.toString())
198+
.checkAndThrowRetriableException(exception));
199+
return ClientHttpResponse.<RESPONSE>builder().error(responseBody).exception(exception).status(httpStatus).build();
200+
}
201+
}

src/main/java/com/intuit/springwebclient/entity/ClientHttpResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public final class ClientHttpResponse<T>{
1010

1111
private final T response;
1212
private final String error;
13+
private final Throwable exception;
1314
private final HttpStatusCode status;
1415
private final boolean isSuccess2xx;
1516

0 commit comments

Comments
 (0)