Skip to content
This repository has been archived by the owner on Sep 12, 2024. It is now read-only.

Commit

Permalink
Merge pull request #49 from duffelhq/FLIN-2981-ratelimit
Browse files Browse the repository at this point in the history
FLIN-2981 Parse rate limit exceptions and return the reset time
  • Loading branch information
johnpeterharvey authored Feb 21, 2023
2 parents 8a32c91 + 6c94d65 commit ba2552c
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 12 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
5 changes: 3 additions & 2 deletions .github/workflows/code-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ jobs:
with:
arguments: build example
- name: Upload built artifacts
if: success() || failure()
uses: actions/upload-artifact@v3
with:
name: Package
path: |
build/libs
build/reports
build/libs/
build/reports/
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
24 changes: 24 additions & 0 deletions src/main/java/com/duffel/exception/RateLimitException.java
Original file line number Diff line number Diff line change
@@ -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;

}
14 changes: 13 additions & 1 deletion src/main/java/com/duffel/net/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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<String, String> headers;
private final String baseEndpoint;

Expand Down Expand Up @@ -90,7 +94,7 @@ public <T, O> T post(String endpoint, Class<T> clazz, O postObject) {
return executeCall(endpoint, RequestMethod.POST.name(), clazz, postObject);
}

public <T,O > T patch(String endpoint, Class<T> clazz, O postObject) {
public <T, O> T patch(String endpoint, Class<T> clazz, O postObject) {
return executeCall(endpoint, RequestMethod.PATCH.name(), clazz, postObject);
}

Expand Down Expand Up @@ -129,6 +133,14 @@ private <T, O> T executeCall(String endpoint, String httpMethod, Class<T> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -68,7 +71,7 @@ void bookWithCancelForAnyReason() {
OrderPassenger orderPassenger = new OrderPassenger();
orderPassenger.setEmail("[email protected]");
orderPassenger.setGivenName("Test");
orderPassenger.setFamilyName("User");
orderPassenger.setFamilyName("Cancel");
orderPassenger.setTitle("Ms");
orderPassenger.setBornOn("1990-01-01");
orderPassenger.setPassengerType(PassengerType.adult);
Expand Down
22 changes: 22 additions & 0 deletions src/test/java/com/duffel/service/ExceptionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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"))
Expand Down
15 changes: 15 additions & 0 deletions src/test/resources/fixtures/exceptions/429_rate_limit.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}

0 comments on commit ba2552c

Please sign in to comment.