diff --git a/parse/src/main/java/com/parse/ParseException.java b/parse/src/main/java/com/parse/ParseException.java index 5dc3b5f9..6a6ff6b2 100644 --- a/parse/src/main/java/com/parse/ParseException.java +++ b/parse/src/main/java/com/parse/ParseException.java @@ -102,6 +102,11 @@ public class ParseException extends Exception { public static final int FILE_DELETE_ERROR = 153; /** Error code indicating that the application has exceeded its request limit. */ public static final int REQUEST_LIMIT_EXCEEDED = 155; + /** + * Error code indicating that the request was a duplicate and has been discarded due to + * idempotency rules. + */ + public static final int DUPLICATE_REQUEST = 159; /** Error code indicating that the provided event name is invalid. */ public static final int INVALID_EVENT_NAME = 160; /** Error code indicating that the username is missing or empty. */ diff --git a/parse/src/main/java/com/parse/ParseRESTCommand.java b/parse/src/main/java/com/parse/ParseRESTCommand.java index 60cd1c59..20ac6ae1 100644 --- a/parse/src/main/java/com/parse/ParseRESTCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTCommand.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; +import java.util.UUID; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -33,6 +34,7 @@ class ParseRESTCommand extends ParseRequest { /* package */ static final String HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version"; /* package */ static final String HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version"; /* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version"; + /* package */ static final String HEADER_REQUEST_ID = "X-Parse-Request-Id"; /* package */ static final String HEADER_INSTALLATION_ID = "X-Parse-Installation-Id"; /* package */ static final String USER_AGENT = "User-Agent"; @@ -49,6 +51,7 @@ class ParseRESTCommand extends ParseRequest { /* package */ String httpPath; private String installationId; private String operationSetUUID; + private final String requestId = UUID.randomUUID().toString(); private String localId; public ParseRESTCommand( @@ -215,6 +218,7 @@ protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) { if (masterKey != null) { requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey); } + requestBuilder.addHeader(HEADER_REQUEST_ID, requestId); } @Override diff --git a/parse/src/test/java/com/parse/ParseRESTCommandTest.java b/parse/src/test/java/com/parse/ParseRESTCommandTest.java index 9bfbb7f5..743ea43a 100644 --- a/parse/src/test/java/com/parse/ParseRESTCommandTest.java +++ b/parse/src/test/java/com/parse/ParseRESTCommandTest.java @@ -10,9 +10,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -30,6 +32,7 @@ import java.io.InputStream; import java.net.URL; import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; import org.json.JSONArray; import org.json.JSONObject; import org.junit.After; @@ -552,4 +555,33 @@ public void testSaveObjectCommandUpdate() { ParsePlugins.reset(); Parse.destroy(); } + + @Test + public void testIdempotencyLogic() throws Exception { + ParseHttpClient mockHttpClient = mock(ParseHttpClient.class); + AtomicReference requestIdAtomicReference = new AtomicReference<>(); + when(mockHttpClient.execute( + argThat( + argument -> { + assertNotNull( + argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID)); + if (requestIdAtomicReference.get() == null) + requestIdAtomicReference.set( + argument.getHeader( + ParseRESTCommand.HEADER_REQUEST_ID)); + assertEquals( + argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID), + requestIdAtomicReference.get()); + return true; + }))) + .thenThrow(new IOException()); + + ParseRESTCommand.server = new URL("http://parse.com"); + ParseRESTCommand command = new ParseRESTCommand.Builder().build(); + Task task = command.executeAsync(mockHttpClient).makeVoid(); + task.waitForCompletion(); + + verify(mockHttpClient, times(ParseRequest.DEFAULT_MAX_RETRIES + 1)) + .execute(any(ParseHttpRequest.class)); + } }