View Javadoc
1   /*
2    * Copyright 2019 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.exception.feign;
18  
19  import static feign.Util.RETRY_AFTER;
20  import static java.util.concurrent.TimeUnit.SECONDS;
21  import static org.bremersee.http.HttpHeadersHelper.buildHttpHeaders;
22  import static org.bremersee.http.HttpHeadersHelper.getContentCharset;
23  
24  import feign.Request.HttpMethod;
25  import feign.Response;
26  import feign.RetryableException;
27  import feign.Util;
28  import feign.codec.ErrorDecoder;
29  import java.nio.charset.Charset;
30  import java.nio.charset.StandardCharsets;
31  import java.time.OffsetDateTime;
32  import java.time.format.DateTimeFormatter;
33  import java.util.Collections;
34  import java.util.Date;
35  import lombok.extern.slf4j.Slf4j;
36  import org.bremersee.exception.RestApiExceptionParser;
37  import org.bremersee.exception.RestApiExceptionParserImpl;
38  import org.bremersee.exception.model.RestApiException;
39  import org.springframework.http.HttpHeaders;
40  
41  /**
42   * This error decoder produces either a {@link FeignClientException} or a {@link feign.RetryableException}.
43   *
44   * @author Christian Bremer
45   */
46  @Slf4j
47  public class FeignClientExceptionErrorDecoder implements ErrorDecoder {
48  
49    private final RestApiExceptionParser parser;
50  
51    /**
52     * Instantiates a new feign client exception error decoder.
53     */
54    @SuppressWarnings("WeakerAccess")
55    public FeignClientExceptionErrorDecoder() {
56      this.parser = new RestApiExceptionParserImpl();
57    }
58  
59    /**
60     * Instantiates a new Feign client exception error decoder.
61     *
62     * @param parser the parser
63     */
64    @SuppressWarnings("unused")
65    public FeignClientExceptionErrorDecoder(
66        final RestApiExceptionParser parser) {
67      this.parser = parser != null ? parser : new RestApiExceptionParserImpl();
68    }
69  
70    @Override
71    public Exception decode(final String methodKey, final Response response) {
72  
73      if (log.isDebugEnabled()) {
74        log.debug("msg=[Decoding error at {}]", methodKey);
75      }
76      final String body = readBody(response);
77      final String message = String.format("status %s reading %s", response.status(), methodKey);
78      final RestApiException restApiException = parser.parseException(
79          body, response.headers());
80      if (log.isDebugEnabled() && body != null) {
81        log.debug("msg=[Is error formatted as rest api exception? {}]",
82            restApiException != null && !body.equals(restApiException.getMessage()));
83      }
84      final FeignClientExceptionFeignClientException">FeignClientException feignClientException = new FeignClientException(
85          response.request(),
86          response.headers() != null ? Collections.unmodifiableMap(response.headers()) : null,
87          response.status(),
88          message,
89          restApiException);
90      final HttpHeaders httpHeaders = buildHttpHeaders(response.headers());
91      final Date retryAfter = determineRetryAfter(httpHeaders.getFirst(RETRY_AFTER));
92      if (retryAfter != null) {
93        return new RetryableException(
94            response.status(),
95            feignClientException.getMessage(),
96            findHttpMethod(response),
97            feignClientException,
98            retryAfter,
99            response.request());
100     }
101     return feignClientException;
102   }
103 
104   static String readBody(final Response response) {
105     if (response == null || response.body() == null) {
106       return null;
107     }
108     Charset charset = getContentCharset(buildHttpHeaders(response.headers()), StandardCharsets.UTF_8);
109     try {
110       return Util.toString(response.body().asReader(charset));
111     } catch (Exception ignored) {
112       return null;
113     }
114   }
115 
116   static Date determineRetryAfter(final String retryAfter) {
117     if (retryAfter == null) {
118       return null;
119     }
120     if (retryAfter.matches("^[0-9]+$")) {
121       long deltaMillis = SECONDS.toMillis(Long.parseLong(retryAfter));
122       return new Date(System.currentTimeMillis() + deltaMillis);
123     }
124     try {
125       return Date.from(OffsetDateTime.parse(retryAfter,
126           DateTimeFormatter.RFC_1123_DATE_TIME).toInstant());
127     } catch (Exception ignored) {
128       return null;
129     }
130   }
131 
132   static HttpMethod findHttpMethod(final Response response) {
133     if (response == null || response.request() == null) {
134       return null;
135     }
136     return response.request().httpMethod();
137   }
138 
139 }