Skip to content

Commit c72f6d0

Browse files
Backport support for custom HTTP status in a backwards compatible way. (#1087)
1 parent 3953864 commit c72f6d0

File tree

7 files changed

+130
-14
lines changed

7 files changed

+130
-14
lines changed

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
|spring.cloud.loadbalancer.sticky-session | | Properties for LoadBalancer sticky-session.
5858
|spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie | `false` | Indicates whether a cookie with the newly selected instance should be added by LoadBalancer.
5959
|spring.cloud.loadbalancer.sticky-session.instance-id-cookie-name | `sc-lb-instance-id` | The name of the cookie holding the preferred instance id.
60+
|spring.cloud.loadbalancer.use-raw-status-code-in-response-data | `false` | Indicates that raw status codes should be used in {@link ResponseData}.
6061
|spring.cloud.loadbalancer.x-forwarded | | Enabling X-Forwarded Host and Proto Headers.
6162
|spring.cloud.loadbalancer.x-forwarded.enabled | `false` | To Enable X-Forwarded Headers.
6263
|spring.cloud.loadbalancer.zone | | Spring Cloud LoadBalancer zone.

docs/src/main/asciidoc/spring-cloud-commons.adoc

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,20 +1218,28 @@ One type of bean that it may be useful to register using <<custom-loadbalancer-c
12181218

12191219
The `LoadBalancerLifecycle` beans provide callback methods, named `onStart(Request<RC> request)`, `onStartRequest(Request<RC> request, Response<T> lbResponse)` and `onComplete(CompletionContext<RES, T, RC> completionContext)`, that you should implement to specify what actions should take place before and after load-balancing.
12201220

1221-
`onStart(Request<RC> request)` takes a `Request` object as a parameter. It contains data that is used to select an appropriate instance, including the downstream client request and <<spring-cloud-loadbalancer-hints,hint>>. `onStartRequest` also takes the `Request` object and, additionally, the `Response<T>` object as parameters. On the other hand, a `CompletionContext` object is provided to the `onComplete(CompletionContext<RES, T, RC> completionContext)` method. It contains the LoadBalancer `Response`, including the selected service instance, the `Status` of the request executed against that service instance and (if available) the response returned to the downstream client, and (if an exception has occurred) the corresponding `Throwable`.
1221+
`onStart(Request<RC> request)` takes a `Request` object as a parameter.
1222+
It contains data that is used to select an appropriate instance, including the downstream client request and <<spring-cloud-loadbalancer-hints,hint>>. `onStartRequest` also takes the `Request` object and, additionally, the `Response<T>` object as parameters.
1223+
On the other hand, a `CompletionContext` object is provided to the `onComplete(CompletionContext<RES, T, RC> completionContext)` method.
1224+
It contains the LoadBalancer `Response`, including the selected service instance, the `Status` of the request executed against that service instance and (if available) the response returned to the downstream client, and (if an exception has occurred) the corresponding `Throwable`.
12221225

12231226
The `supports(Class requestContextClass, Class responseClass,
1224-
Class serverTypeClass)` method can be used to determine whether the processor in question handles objects of provided types. If not overridden by the user, it returns `true`.
1227+
Class serverTypeClass)` method can be used to determine whether the processor in question handles objects of provided types.
1228+
If not overridden by the user, it returns `true`.
12251229

12261230
NOTE: In the preceding method calls, `RC` means `RequestContext` type, `RES` means client response type, and `T` means returned server type.
12271231

1232+
WARNING: If you are using custom HTTP status codes, you will be getting exceptions.
1233+
In order to prevent this, you can set the value of `spring.cloud.loadbalancer.use-raw-status-code-in-response-data`.
1234+
It will cause raw status codes to be used instead of `HttpStatus` enums.
1235+
The `httpStatus` field in `ResponseData` will then be used, but you'll be able to get the raw status code from the `rawHttpStatus` field.
1236+
12281237
[[loadbalancer-micrometer-stats-lifecycle]]
12291238
=== Spring Cloud LoadBalancer Statistics
12301239

12311240
We provide a `LoadBalancerLifecycle` bean called `MicrometerStatsLoadBalancerLifecycle`, which uses Micrometer to provide statistics for load-balanced calls.
12321241

1233-
In order to get this bean added to your application context,
1234-
set the value of the `spring.cloud.loadbalancer.stats.micrometer.enabled` to `true` and have a `MeterRegistry` available (for example, by adding https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html[Spring Boot Actuator] to your project).
1242+
In order to get this bean added to your application context, set the value of the `spring.cloud.loadbalancer.stats.micrometer.enabled` to `true` and have a `MeterRegistry` available (for example, by adding https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html[Spring Boot Actuator] to your project).
12351243

12361244
`MicrometerStatsLoadBalancerLifecycle` registers the following meters in `MeterRegistry`:
12371245

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public class LoadBalancerProperties {
6868
*/
6969
private StickySession stickySession = new StickySession();
7070

71+
/**
72+
* Indicates that raw status codes should be used in {@link ResponseData}.
73+
*/
74+
private boolean useRawStatusCodeInResponseData;
75+
7176
public HealthCheck getHealthCheck() {
7277
return healthCheck;
7378
}
@@ -121,6 +126,14 @@ public XForwarded getXForwarded() {
121126
return xForwarded;
122127
}
123128

129+
public boolean isUseRawStatusCodeInResponseData() {
130+
return useRawStatusCodeInResponseData;
131+
}
132+
133+
public void setUseRawStatusCodeInResponseData(boolean useRawStatusCodeInResponseData) {
134+
this.useRawStatusCodeInResponseData = useRawStatusCodeInResponseData;
135+
}
136+
124137
public static class StickySession {
125138

126139
/**

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/ResponseData.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,93 @@ public class ResponseData {
4848

4949
private final RequestData requestData;
5050

51+
private final Integer rawHttpStatus;
52+
53+
/**
54+
* @deprecated for removal; new constructors will be added in 4.x
55+
*/
56+
@Deprecated
57+
public ResponseData(HttpHeaders headers, MultiValueMap<String, ResponseCookie> cookies, RequestData requestData,
58+
Integer rawHttpStatus) {
59+
this.httpStatus = null;
60+
this.rawHttpStatus = rawHttpStatus;
61+
this.headers = headers;
62+
this.cookies = cookies;
63+
this.requestData = requestData;
64+
}
65+
66+
/**
67+
* @deprecated for removal; new constructors will be added in 4.x
68+
*/
69+
@Deprecated
5170
public ResponseData(HttpStatus httpStatus, HttpHeaders headers, MultiValueMap<String, ResponseCookie> cookies,
5271
RequestData requestData) {
5372
this.httpStatus = httpStatus;
73+
this.rawHttpStatus = httpStatus != null ? httpStatus.value() : null;
5474
this.headers = headers;
5575
this.cookies = cookies;
5676
this.requestData = requestData;
5777
}
5878

79+
/**
80+
* @deprecated for removal; new constructors will be added in 4.x
81+
*/
82+
@Deprecated
5983
public ResponseData(ClientResponse response, RequestData requestData) {
6084
this(response.statusCode(), response.headers().asHttpHeaders(), response.cookies(), requestData);
6185
}
6286

87+
// Done this way to maintain backwards compatibility while allowing switching to raw
88+
// HTTPStatus
89+
// Will be removed in `4.x`
90+
/**
91+
* @deprecated for removal; new constructors will be added in 4.x
92+
*/
93+
@Deprecated
94+
public ResponseData(RequestData requestData, ClientResponse response) {
95+
this(response.headers().asHttpHeaders(), response.cookies(), requestData, response.rawStatusCode());
96+
}
97+
98+
/**
99+
* @deprecated for removal; new constructors will be added in 4.x
100+
*/
101+
@Deprecated
63102
public ResponseData(ServerHttpResponse response, RequestData requestData) {
64103
this(response.getStatusCode(), response.getHeaders(), response.getCookies(), requestData);
65104
}
66105

106+
// Done this way to maintain backwards compatibility while allowing switching to raw
107+
// HTTPStatus
108+
// Will be removed in `4.x`
109+
/**
110+
* @deprecated for removal; new constructors will be added in 4.x
111+
*/
112+
@Deprecated
113+
public ResponseData(RequestData requestData, ServerHttpResponse response) {
114+
this(response.getHeaders(), response.getCookies(), requestData, response.getRawStatusCode());
115+
}
116+
117+
/**
118+
* @deprecated for removal; new constructors will be added in 4.x
119+
*/
120+
@Deprecated
67121
public ResponseData(ClientHttpResponse clientHttpResponse, RequestData requestData) throws IOException {
68122
this(clientHttpResponse.getStatusCode(), clientHttpResponse.getHeaders(),
69123
buildCookiesFromHeaders(clientHttpResponse.getHeaders()), requestData);
70124
}
71125

126+
// Done this way to maintain backwards compatibility while allowing switching to raw
127+
// HTTPStatus
128+
// Will be removed in `4.x`
129+
/**
130+
* @deprecated for removal; new constructors will be added in 4.x
131+
*/
132+
@Deprecated
133+
public ResponseData(RequestData requestData, ClientHttpResponse clientHttpResponse) throws IOException {
134+
this(clientHttpResponse.getHeaders(), buildCookiesFromHeaders(clientHttpResponse.getHeaders()), requestData,
135+
clientHttpResponse.getRawStatusCode());
136+
}
137+
72138
public HttpStatus getHttpStatus() {
73139
return httpStatus;
74140
}
@@ -85,6 +151,10 @@ public RequestData getRequestData() {
85151
return requestData;
86152
}
87153

154+
public Integer getRawHttpStatus() {
155+
return rawHttpStatus;
156+
}
157+
88158
@Override
89159
public String toString() {
90160
ToStringCreator to = new ToStringCreator(this);
@@ -113,7 +183,7 @@ static MultiValueMap<String, ResponseCookie> buildCookiesFromHeaders(HttpHeaders
113183

114184
@Override
115185
public int hashCode() {
116-
return Objects.hash(httpStatus, headers, cookies, requestData);
186+
return Objects.hash(httpStatus, headers, cookies, requestData, rawHttpStatus);
117187
}
118188

119189
@Override
@@ -126,7 +196,8 @@ public boolean equals(Object o) {
126196
}
127197
ResponseData that = (ResponseData) o;
128198
return httpStatus == that.httpStatus && Objects.equals(headers, that.headers)
129-
&& Objects.equals(cookies, that.cookies) && Objects.equals(requestData, that.requestData);
199+
&& Objects.equals(cookies, that.cookies) && Objects.equals(requestData, that.requestData)
200+
&& Objects.equals(rawHttpStatus, that.rawHttpStatus);
130201
}
131202

132203
}

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerExchangeFilterFunction.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction
128128
LOG.debug(String.format("LoadBalancer has retrieved the instance for service %s: %s", serviceId,
129129
instance.getUri()));
130130
}
131-
LoadBalancerProperties.StickySession stickySessionProperties = loadBalancerFactory.getProperties(serviceId)
132-
.getStickySession();
131+
LoadBalancerProperties properties = loadBalancerFactory.getProperties(serviceId);
132+
LoadBalancerProperties.StickySession stickySessionProperties = properties.getStickySession();
133133
ClientRequest newRequest = buildClientRequest(clientRequest, instance,
134134
stickySessionProperties.getInstanceIdCookieName(),
135135
stickySessionProperties.isAddServiceInstanceCookie(), transformers);
@@ -140,10 +140,19 @@ public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction
140140
CompletionContext.Status.FAILED, throwable, lbRequest, lbResponse))))
141141
.doOnSuccess(clientResponse -> supportedLifecycleProcessors.forEach(
142142
lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
143-
lbRequest, lbResponse, new ResponseData(clientResponse, requestData)))));
143+
lbRequest, lbResponse, buildResponseData(requestData, clientResponse,
144+
properties.isUseRawStatusCodeInResponseData())))));
144145
});
145146
}
146147

148+
private ResponseData buildResponseData(RequestData requestData, ClientResponse clientResponse,
149+
boolean useRawStatusCodes) {
150+
if (useRawStatusCodes) {
151+
return new ResponseData(requestData, clientResponse);
152+
}
153+
return new ResponseData(clientResponse, requestData);
154+
}
155+
147156
protected Mono<Response<ServiceInstance>> choose(String serviceId, Request<RequestDataContext> request) {
148157
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
149158
if (loadBalancer == null) {

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunction.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction
164164
.doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
165165
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>(
166166
CompletionContext.Status.FAILED, throwable, lbRequest, lbResponse))))
167-
.doOnSuccess(clientResponse -> supportedLifecycleProcessors.forEach(
168-
lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
169-
lbRequest, lbResponse, new ResponseData(clientResponse, requestData)))))
167+
.doOnSuccess(
168+
clientResponse -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
169+
new CompletionContext<>(CompletionContext.Status.SUCCESS, lbRequest, lbResponse,
170+
buildResponseData(requestData, clientResponse,
171+
properties.isUseRawStatusCodeInResponseData())))))
170172
.map(clientResponse -> {
171173
loadBalancerRetryContext.setClientResponse(clientResponse);
172174
if (shouldRetrySameServiceInstance(retryPolicy, loadBalancerRetryContext)) {
@@ -192,6 +194,14 @@ lbRequest, lbResponse, new ResponseData(clientResponse, requestData)))))
192194
}).retryWhen(exchangeRetry)).retryWhen(filterRetry);
193195
}
194196

197+
private ResponseData buildResponseData(RequestData requestData, ClientResponse clientResponse,
198+
boolean useRawStatusCodes) {
199+
if (useRawStatusCodes) {
200+
return new ResponseData(requestData, clientResponse);
201+
}
202+
return new ResponseData(clientResponse, requestData);
203+
}
204+
195205
private Retry buildRetrySpec(int max, boolean transientErrors, LoadBalancerProperties.Retry retry) {
196206
if (!retry.isEnabled()) {
197207
return Retry.max(0).filter(this::isRetryException).transientErrors(transientErrors);

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBala
9696
.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, new DefaultResponse(serviceInstance)));
9797
try {
9898
T response = request.apply(serviceInstance);
99-
Object clientResponse = getClientResponse(response);
99+
LoadBalancerProperties properties = loadBalancerClientFactory.getProperties(serviceId);
100+
Object clientResponse = getClientResponse(response, properties.isUseRawStatusCodeInResponseData());
100101
supportedLifecycleProcessors
101102
.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
102103
lbRequest, defaultResponse, clientResponse)));
@@ -115,13 +116,16 @@ public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBala
115116
return null;
116117
}
117118

118-
private <T> Object getClientResponse(T response) {
119+
private <T> Object getClientResponse(T response, boolean useRawStatusCodes) {
119120
ClientHttpResponse clientHttpResponse = null;
120121
if (response instanceof ClientHttpResponse) {
121122
clientHttpResponse = (ClientHttpResponse) response;
122123
}
123124
if (clientHttpResponse != null) {
124125
try {
126+
if (useRawStatusCodes) {
127+
return new ResponseData(null, clientHttpResponse);
128+
}
125129
return new ResponseData(clientHttpResponse, null);
126130
}
127131
catch (IOException ignored) {

0 commit comments

Comments
 (0)