From 4260beffac50eed73ed82dd198d778911b2e7608 Mon Sep 17 00:00:00 2001 From: John Peter Harvey Date: Fri, 3 Feb 2023 09:04:07 +0000 Subject: [PATCH 1/4] Try to fix upload path --- .github/workflows/code-examples.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-examples.yml b/.github/workflows/code-examples.yml index 9a8e616..4732dc7 100644 --- a/.github/workflows/code-examples.yml +++ b/.github/workflows/code-examples.yml @@ -29,11 +29,11 @@ jobs: - name: Build with Gradle uses: gradle/gradle-build-action@v2 with: - arguments: build example + arguments: build --scan example - name: Upload built artifacts uses: actions/upload-artifact@v3 with: name: Package path: | - build/libs - build/reports + build/libs/ + build/reports/ From 0d308f680edec5afa2f9581226ee60dd7c7502ba Mon Sep 17 00:00:00 2001 From: John Peter Harvey Date: Fri, 10 Feb 2023 13:37:54 +0000 Subject: [PATCH 2/4] FLIN-2981 Rate limit exception --- .../duffel/exception/RateLimitException.java | 24 +++++++++++++++++++ src/main/java/com/duffel/net/ApiClient.java | 14 ++++++++++- .../example/BookWithCancelForAnyReasonIT.java | 13 ++++++---- .../com/duffel/service/ExceptionTest.java | 22 +++++++++++++++++ .../fixtures/exceptions/429_rate_limit.json | 15 ++++++++++++ 5 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/duffel/exception/RateLimitException.java create mode 100644 src/test/resources/fixtures/exceptions/429_rate_limit.json diff --git a/src/main/java/com/duffel/exception/RateLimitException.java b/src/main/java/com/duffel/exception/RateLimitException.java new file mode 100644 index 0000000..29e67ea --- /dev/null +++ b/src/main/java/com/duffel/exception/RateLimitException.java @@ -0,0 +1,24 @@ +package com.duffel.exception; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * HTTP 429 rate limited + */ +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@ToString(callSuper = true) +public class RateLimitException extends DuffelException { + + /** + * When will the rate limit reset again. + */ + private LocalDateTime rateLimitReset; + +} diff --git a/src/main/java/com/duffel/net/ApiClient.java b/src/main/java/com/duffel/net/ApiClient.java index 49e2f5d..45f4df4 100644 --- a/src/main/java/com/duffel/net/ApiClient.java +++ b/src/main/java/com/duffel/net/ApiClient.java @@ -2,6 +2,7 @@ import com.duffel.DuffelApiClient; import com.duffel.exception.DuffelException; +import com.duffel.exception.RateLimitException; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; @@ -18,6 +19,8 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; @@ -28,6 +31,7 @@ public class ApiClient { private final HttpClient HTTP_CLIENT; private static final String APPLICATION_JSON = "application/json"; + private static final String RATE_LIMIT_HEADER = "ratelimit-reset"; private final Map headers; private final String baseEndpoint; @@ -90,7 +94,7 @@ public T post(String endpoint, Class clazz, O postObject) { return executeCall(endpoint, RequestMethod.POST.name(), clazz, postObject); } - public T patch(String endpoint, Class clazz, O postObject) { + public T patch(String endpoint, Class clazz, O postObject) { return executeCall(endpoint, RequestMethod.PATCH.name(), clazz, postObject); } @@ -129,6 +133,14 @@ private T executeCall(String endpoint, String httpMethod, Class respon } else { return objectMapper.readValue(response.body(), responseType); } + } else if (response.statusCode() == 429) { + LocalDateTime rateLimitReset = (response.headers().firstValue(RATE_LIMIT_HEADER).isPresent()) ? + LocalDateTime.parse(response.headers().firstValue(RATE_LIMIT_HEADER).get(), DateTimeFormatter.RFC_1123_DATE_TIME) + : null; + LOG.debug("Duffel returned an rate limit response with a reset of {}", rateLimitReset); + RateLimitException exception = objectMapper.readValue(response.body(), RateLimitException.class); + exception.setRateLimitReset(rateLimitReset); + throw exception; } else { LOG.debug("Duffel returned an error with status code {}", response.statusCode()); throw objectMapper.readValue(response.body(), DuffelException.class); diff --git a/src/test/java/com/duffel/example/BookWithCancelForAnyReasonIT.java b/src/test/java/com/duffel/example/BookWithCancelForAnyReasonIT.java index eb19e26..456e896 100644 --- a/src/test/java/com/duffel/example/BookWithCancelForAnyReasonIT.java +++ b/src/test/java/com/duffel/example/BookWithCancelForAnyReasonIT.java @@ -7,6 +7,7 @@ import com.duffel.model.request.Payment; import com.duffel.model.request.ServiceRequest; import com.duffel.model.response.*; +import com.duffel.model.response.order.metadata.CancelForAnyReasonMetadata; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.Test; @@ -31,14 +32,14 @@ void bookWithCancelForAnyReason() { // Create an offer request OfferRequest.Slice slice = new OfferRequest.Slice(); - slice.setDepartureDate(LocalDate.now().plusDays(60).format(DateTimeFormatter.ISO_DATE)); - slice.setOrigin("LGW"); - slice.setDestination("CDG"); + slice.setDepartureDate(LocalDate.now().plusDays(100).format(DateTimeFormatter.ISO_DATE)); + slice.setOrigin("LHR"); + slice.setDestination("FRA"); Passenger passenger = new Passenger(); passenger.setType(PassengerType.adult); passenger.setGivenName("Test"); - passenger.setFamilyName("User"); + passenger.setFamilyName("Cancel"); OfferRequest request = new OfferRequest(); request.setMaxConnections(0); @@ -60,6 +61,8 @@ void bookWithCancelForAnyReason() { Service cfar = offer.getAvailableServices().stream().filter(s -> ServiceType.Type.cancel_for_any_reason == s.getServiceType()).findFirst().orElseThrow(); LOG.info("๐Ÿงจ Cancel For Any Reason service available with cost {}{}", cfar.getTotalCurrency(), cfar.getTotalAmount()); + LOG.info("๐Ÿ” T&Cs {}", ((CancelForAnyReasonMetadata) cfar.getMetadata()).getTermsAndConditionsUrl()); + LOG.info("๐Ÿ“ Copy {}", ((CancelForAnyReasonMetadata) cfar.getMetadata()).getMerchantCopy()); BigDecimal newOrderTotalCost = offer.getTotalAmount().add(cfar.getTotalAmount()); LOG.info("๐Ÿ’ณ Cost of flight offer plus CFAR is {}{}", offer.getTotalCurrency(), newOrderTotalCost); @@ -68,7 +71,7 @@ void bookWithCancelForAnyReason() { OrderPassenger orderPassenger = new OrderPassenger(); orderPassenger.setEmail("test@duffel.com"); orderPassenger.setGivenName("Test"); - orderPassenger.setFamilyName("User"); + orderPassenger.setFamilyName("Cancel"); orderPassenger.setTitle("Ms"); orderPassenger.setBornOn("1990-01-01"); orderPassenger.setPassengerType(PassengerType.adult); diff --git a/src/test/java/com/duffel/service/ExceptionTest.java b/src/test/java/com/duffel/service/ExceptionTest.java index 48cdff8..a12529b 100644 --- a/src/test/java/com/duffel/service/ExceptionTest.java +++ b/src/test/java/com/duffel/service/ExceptionTest.java @@ -2,6 +2,7 @@ import com.duffel.DuffelApiClient; import com.duffel.exception.DuffelException; +import com.duffel.exception.RateLimitException; import com.duffel.exception.StandardError; import com.duffel.exception.ValidationError; import com.duffel.model.request.OfferRequest; @@ -10,6 +11,8 @@ import org.mockserver.client.MockServerClient; import org.mockserver.junit.jupiter.MockServerExtension; +import java.time.LocalDateTime; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockserver.model.HttpRequest.request; @@ -49,6 +52,25 @@ void validation_error(MockServerClient mockClient) { mockClient.reset(); } + @Test + void rate_limit_error(MockServerClient mockClient) { + mockClient.when(request().withMethod("POST")) + .respond(response().withStatusCode(429) + .withHeader("ratelimit-limit", "5") + .withHeader("ratelimit-remaining", "0") + .withHeader("ratelimit-reset", "Fri, 10 Feb 2023 13:24:43 GMT") + .withBody(FixtureHelper.readFixture(this.getClass(), "/fixtures/exceptions/429_rate_limit.json"))); + + DuffelApiClient client = new DuffelApiClient("testKey", "http://localhost:" + mockClient.getPort()); + + RateLimitException exception = assertThrows(RateLimitException.class, () -> client.offerRequestService.post(new OfferRequest())); + assertEquals("429", exception.getMeta().getStatus()); + assertEquals("rate_limit_exceeded", exception.getErrors().get(0).getCode()); + assertEquals(LocalDateTime.parse("2023-02-10T13:24:43"), exception.getRateLimitReset()); + + mockClient.reset(); + } + @Test void airline_internal(MockServerClient mockClient) { mockClient.when(request().withMethod("POST")) diff --git a/src/test/resources/fixtures/exceptions/429_rate_limit.json b/src/test/resources/fixtures/exceptions/429_rate_limit.json new file mode 100644 index 0000000..c4abda4 --- /dev/null +++ b/src/test/resources/fixtures/exceptions/429_rate_limit.json @@ -0,0 +1,15 @@ +{ + "meta": { + "status": 429, + "request_id": "F0J5ZCaJVEn580QADT4B" + }, + "errors": [ + { + "type": "rate_limit_error", + "title": "Rate limit exceeded", + "message": "Too many requests hit the API too quickly. Please retry your request after the time specified in the `ratelimit-reset` header.", + "documentation_url": "https://duffel.com/docs/api/overview/errors", + "code": "rate_limit_exceeded" + } + ] +} From 4c39f1d21106a3aa3329e995c0f9ff6d4919b228 Mon Sep 17 00:00:00 2001 From: John Peter Harvey Date: Fri, 10 Feb 2023 13:44:52 +0000 Subject: [PATCH 3/4] FLIN-2981 Rate limit exception --- .github/workflows/code-examples.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-examples.yml b/.github/workflows/code-examples.yml index 4732dc7..ace80e5 100644 --- a/.github/workflows/code-examples.yml +++ b/.github/workflows/code-examples.yml @@ -29,8 +29,9 @@ jobs: - name: Build with Gradle uses: gradle/gradle-build-action@v2 with: - arguments: build --scan example + arguments: build example - name: Upload built artifacts + if: success() || failure() uses: actions/upload-artifact@v3 with: name: Package From 6c94d65a4f7600edc944923b43ebb77e5ce57815 Mon Sep 17 00:00:00 2001 From: John Peter Harvey Date: Fri, 10 Feb 2023 13:57:54 +0000 Subject: [PATCH 4/4] FLIN-2981 Rate limit exception --- .github/workflows/build.yml | 5 +++-- .github/workflows/publish.yml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b437436..2867e11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,9 +28,10 @@ jobs: with: arguments: build - name: Upload built artifacts + if: success() || failure() uses: actions/upload-artifact@v3 with: name: Package path: | - build/libs - build/reports + build/libs/ + build/reports/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4469dd4..1c57e6b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,9 +33,10 @@ jobs: with: arguments: build publish -Psign - name: Upload built artifacts + if: success() || failure() uses: actions/upload-artifact@v3 with: name: Package path: | - build/libs - build/reports + build/libs/ + build/reports/