Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid to have unexpected exception on status code 429 TooManyRequests #433

Merged
merged 7 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/main/java/org/stellar/sdk/requests/ResponseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public T handleResponse(final Response response) throws IOException, TooManyRequ
try {
// Too Many Requests
if (response.code() == 429) {
int retryAfter = Integer.parseInt(response.header("Retry-After"));

int retryAfter = -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamirms @stellar/anchor-eng Do you have an opinion on what the default value we should be able to use here and potential impact on applications that may be consuming it?

Copy link
Contributor

@leighmcculloch leighmcculloch Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My wondering is that because this value is always positive today, folks may be doing things like using the value as a duration in addition with current time to find a new time, or calling Time.sleep with the value, or any other number of things. Throwing a negative value into the mix may have unintended consequences and it may be safer to make the value an Optional.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that returning an optional would be the best

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A reasonable default might be appropriate, such as 1000ms.

Copy link
Contributor

@sreuland sreuland Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the notion of a server or sdk provided retry interval introduces un-necessary coupling between the sdk and the client and a pattern to avoid, this could be the opportunity to remove retryAfter from TooManyRequestsException as a breaking api change to indicate clearer expectations to clients to determine their own error handling strategy(back-off algo, etc).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Retry-After header is a standard header to use to communicate how long a client needs to backoff for. Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After

The SDK is just providing access to that header, which seems reasonable. Developers can still determine their own error handling strategy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, making it optional is the best solution, as developer can decide default retry value themself

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, changing to Optional<Integer> is good trade-off for handling retyAfter, it's worth introducing the breaking change that comes with it on TooManyRequestsException.getRetryAfter() as one that will alert clients that were using it, to leverage the extra info provided in notPresent to decide how to handle.

Copy link
Contributor

@leighmcculloch leighmcculloch Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the ideal interface is an Optional, however a call needs to be made on whether breaking the API of the SDK is worth it. If not, which I suspect would be the case, using a reasonable default would be a great way to move forward with minimal impact.

Presumedly Horizon has a default/recommended value and we could use that same value here. They don't need to be the same, and it doesn't really matter if they change, as this is just a failsafe.

cc @mollykarcher I suspect you or your team can make a call if the Java SDK is accepting breaking API changes at the moment, or if a breaking change for this part of the API is outside any compatibility we'd like to guarantee.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm most concerned here that horizon.stellar.org isn't populating this header, which we'll look into and track separately in the #go monorepo. But as to whether we are accepting breaking changes to this SDK, technically this SDK is pre-1.0, so professes no backwards compatibility guarantees. I definitely think this is less than ideal, which is why I want to cut 1.0 soon (see #469) to cement expectations with developers. But given the current state of things, I think going with the breaking change here is acceptable.

if (response.header("Retry-After") != null) {
retryAfter = Integer.parseInt(response.header("Retry-After"));
}
throw new TooManyRequestsException(retryAfter);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public TooManyRequestsException(int retryAfter) {
}

/**
* Returns number of seconds a client should wait before sending requests again.
* Returns number of seconds a client should wait before sending requests again,
* or -1 this time is unknown.
*/
public int getRetryAfter() {
return retryAfter;
Expand Down
65 changes: 65 additions & 0 deletions src/test/java/org/stellar/sdk/requests/ResponseHandlerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.stellar.sdk.requests;

import static org.junit.Assert.assertEquals;

import java.io.IOException;

import org.junit.Assert;
import org.junit.Test;

import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

public class ResponseHandlerTest {

@Test
public void testTooManyRequests() throws IOException, InterruptedException {

MockResponse response = new MockResponse();
response.setResponseCode(429);
response.setHeader("Retry-After", "10");

MockWebServer mockWebServer = new MockWebServer();
mockWebServer.start();
mockWebServer.enqueue(response);

OkHttpClient okHttpClient = new OkHttpClient().newBuilder().build();
try {

AccountsRequestBuilder.execute(okHttpClient, mockWebServer.url("/"));
Assert.fail();
} catch (TooManyRequestsException tmre) {
assertEquals(10, tmre.getRetryAfter());
} finally {

mockWebServer.shutdown();
mockWebServer.close();
}
}

@Test
public void testTooManyRequestsNoHeader() throws IOException, InterruptedException {

MockResponse response = new MockResponse();
response.setResponseCode(429);

MockWebServer mockWebServer = new MockWebServer();

mockWebServer.start();
mockWebServer.enqueue(response);

OkHttpClient okHttpClient = new OkHttpClient().newBuilder().build();

try {
AccountsRequestBuilder.execute(okHttpClient, mockWebServer.url("/"));
Assert.fail();
} catch (TooManyRequestsException tmre) {
assertEquals(-1, tmre.getRetryAfter());
} finally {

mockWebServer.shutdown();
mockWebServer.close();
}
}
}