11package 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 ;
36import java .time .Duration ;
7+ import java .util .Map ;
48import java .util .Objects ;
9+ import java .util .Optional ;
510import java .util .function .Consumer ;
6-
11+ import lombok .extern .slf4j .Slf4j ;
12+ import org .slf4j .MDC ;
713import org .springframework .beans .factory .annotation .Qualifier ;
814import org .springframework .http .HttpHeaders ;
915import org .springframework .http .HttpStatus ;
1016import org .springframework .http .ResponseEntity ;
1117import org .springframework .stereotype .Component ;
12- import org .springframework .web .client .HttpStatusCodeException ;
13- import org .springframework .web .client .UnknownContentTypeException ;
1418import org .springframework .web .reactive .function .client .WebClient ;
1519import org .springframework .web .reactive .function .client .WebClient .RequestBodySpec ;
1620import 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 ;
2321import reactor .core .publisher .Mono ;
2422import 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
3129public 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+ }
0 commit comments