1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.exception.feign;
18
19 import static feign.Util.RETRY_AFTER;
20 import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
21 import static java.util.Objects.isNull;
22 import static java.util.Objects.nonNull;
23 import static java.util.concurrent.TimeUnit.SECONDS;
24
25 import feign.Request.HttpMethod;
26 import feign.Response;
27 import feign.RetryableException;
28 import feign.codec.ErrorDecoder;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.time.ZonedDateTime;
32 import java.util.Collection;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Objects;
36 import java.util.Optional;
37 import lombok.extern.slf4j.Slf4j;
38 import org.bremersee.exception.RestApiExceptionParser;
39 import org.bremersee.exception.RestApiExceptionParserImpl;
40 import org.bremersee.exception.model.RestApiException;
41 import org.springframework.http.HttpHeaders;
42 import org.springframework.http.HttpStatusCode;
43 import org.springframework.util.FileCopyUtils;
44
45
46
47
48
49
50
51 @Slf4j
52 public class FeignClientExceptionErrorDecoder implements ErrorDecoder {
53
54 private final RestApiExceptionParser parser;
55
56
57
58
59 public FeignClientExceptionErrorDecoder() {
60 this(null);
61 }
62
63
64
65
66
67
68 public FeignClientExceptionErrorDecoder(RestApiExceptionParser parser) {
69 this.parser = nonNull(parser) ? parser : new RestApiExceptionParserImpl();
70 }
71
72 @Override
73 public Exception decode(String methodKey, Response response) {
74
75 if (log.isDebugEnabled()) {
76 log.debug("Decoding feign exception at {}", methodKey);
77 }
78 Map<String, Collection<String>> headers = Objects
79 .requireNonNullElseGet(response.headers(), Map::of);
80 HttpHeaders httpHeaders = headers.entrySet()
81 .stream()
82 .collect(
83 HttpHeaders::new,
84 (a, b) -> a.addAll(b.getKey(), List.copyOf(b.getValue())),
85 HttpHeaders::putAll);
86 byte[] body = getResponseBody(response);
87 RestApiException restApiException = parser.parseException(
88 body,
89 HttpStatusCode.valueOf(response.status()),
90 httpHeaders);
91 FeignClientException feignClientException = new FeignClientException(
92 response.status(),
93 String.format("Status %s reading %s", response.status(), methodKey),
94 response.request(),
95 headers,
96 body,
97 restApiException);
98 return determineRetryAfter(httpHeaders.getFirst(RETRY_AFTER))
99 .map(retryAfter -> (Exception) new RetryableException(
100 response.status(),
101 feignClientException.getMessage(),
102 getHttpMethod(response),
103 feignClientException,
104 retryAfter,
105 response.request()))
106 .orElse(feignClientException);
107 }
108
109
110
111
112
113
114
115 protected byte[] getResponseBody(Response response) {
116 byte[] body;
117 if (isNull(response.body())) {
118 body = new byte[0];
119 } else {
120 try (InputStream in = response.body().asInputStream()) {
121 body = FileCopyUtils.copyToByteArray(in);
122 } catch (IOException e) {
123 body = new byte[0];
124 }
125 }
126 return body;
127 }
128
129
130
131
132
133
134
135 protected HttpMethod getHttpMethod(Response response) {
136 if (isNull(response) || isNull(response.request())) {
137 return null;
138 }
139 return response.request().httpMethod();
140 }
141
142
143
144
145
146
147
148 protected Optional<Long> determineRetryAfter(String retryAfter) {
149 try {
150 return Optional.ofNullable(retryAfter)
151 .filter(retryAfterValue -> retryAfterValue.matches("^[0-9]+\\.?0*$"))
152 .map(retryAfterValue -> retryAfterValue.replaceAll("\\.0*$", ""))
153 .map(retryAfterValue -> SECONDS.toMillis(Long.parseLong(retryAfterValue)))
154 .map(deltaMillis -> currentTimeMillis() + deltaMillis)
155 .or(() -> Optional.ofNullable(retryAfter)
156 .map(retryAfterValue -> ZonedDateTime
157 .parse(retryAfterValue, RFC_1123_DATE_TIME).toInstant().toEpochMilli()));
158 } catch (Exception e) {
159 log.warn("Parsing retry after date for feigns RetryableException failed.", e);
160 return Optional.empty();
161 }
162 }
163
164
165
166
167
168
169 protected long currentTimeMillis() {
170 return System.currentTimeMillis();
171 }
172
173 }