Subject: [PATCH] Handle rate limiting with a more specific message
---
Index: src/org/openstreetmap/josm/io/OsmApi.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/io/OsmApi.java b/src/org/openstreetmap/josm/io/OsmApi.java
--- a/src/org/openstreetmap/josm/io/OsmApi.java	(revision 18966)
+++ b/src/org/openstreetmap/josm/io/OsmApi.java	(date 1706718724843)
@@ -830,6 +830,10 @@
                     CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER);
                     throw new OsmApiException(retCode, errorHeader, errorBody, activeConnection.getURL().toString(),
                             doAuthenticate ? retrieveBasicAuthorizationLogin(client) : null, response.getContentType());
+                case 429: // This is the code that OSM uses for rate limiting
+                    // FIXME: Use a specific exception, we may also want to inform the user that they may want to wait an hour.
+                    throw new OsmApiException(retCode, errorHeader, errorBody, activeConnection.getURL().toString(),
+                            doAuthenticate ? retrieveBasicAuthorizationLogin(client) : null, response.getContentType());
                 default:
                     throw new OsmApiException(retCode, errorHeader, errorBody);
                 }
Index: test/unit/org/openstreetmap/josm/io/OsmApiTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/io/OsmApiTest.java b/test/unit/org/openstreetmap/josm/io/OsmApiTest.java
--- a/test/unit/org/openstreetmap/josm/io/OsmApiTest.java	(revision 18966)
+++ b/test/unit/org/openstreetmap/josm/io/OsmApiTest.java	(date 1706721781307)
@@ -1,26 +1,65 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.io;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.io.ByteArrayInputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuth20Exception;
+import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
+import org.openstreetmap.josm.data.oauth.OAuth20Token;
+import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.data.osm.Changeset;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.User;
 import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
 
+import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+
 /**
  * Unit tests of {@link OsmApi} class.
  */
+@org.openstreetmap.josm.testutils.annotations.OsmApi(org.openstreetmap.josm.testutils.annotations.OsmApi.APIType.NONE)
 class OsmApiTest {
+    @RegisterExtension
+    static WireMockExtension wireMockExtension = WireMockExtension.newInstance().options(
+            WireMockConfiguration.options().usingFilesUnderDirectory(TestUtils.getTestDataRoot())
+    ).failOnUnmatchedRequests(true).build();
+
+    @BeforeEach
+    void setup(WireMockRuntimeInfo info) throws OAuth20Exception {
+        OAuthAccessTokenHolder.getInstance().setAccessToken(info.getHttpBaseUrl(), new OAuth20Token(new OAuth20Parameters("clientId", "clientSecret",
+                "tokenUrl", "authorizeUrl", info.getHttpBaseUrl(), "redirectUrl"),
+                "{\"access_token\": \"test_token\", \"token_type\": \"bearer\"}"));
+    }
+
+    @AfterEach
+    void tearDown(WireMockRuntimeInfo info) {
+        OAuthAccessTokenHolder.getInstance().setAccessToken(info.getHttpBaseUrl(), (IOAuthToken) null);
+    }
+
     /**
      * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/12675">Bug #12675</a>.
      * @throws IllegalDataException if an error occurs
      */
     @Test
     void testTicket12675() throws IllegalDataException {
+
         OsmApi api = OsmApi.getOsmApi();
         Changeset cs = new Changeset();
         cs.setUser(User.getAnonymous());
@@ -36,4 +75,19 @@
                 NullProgressMonitor.INSTANCE).iterator().next();
         assertEquals(User.getAnonymous(), cs2.getUser());
     }
+
+    @Test
+    void testTooManyRequests429(WireMockRuntimeInfo info) {
+        final OsmApi osmApi = OsmApi.getOsmApi(info.getHttpBaseUrl());
+        final ResponseDefinitionBuilder responseDefinitionBuilder = WireMock.status(429).withBody("Upload has been blocked due to rate limiting. Please try again later.")
+                        .withHeader("Error", "Upload has been blocked due to rate limiting. Please try again later.");
+        info.getWireMock().register(WireMock.get("/capabilities").willReturn(WireMock.aResponse().withBodyFile("api/0.6/capabilities")));
+        info.getWireMock().register(WireMock.put("/0.6/changeset/create").willReturn(WireMock.aResponse().withBody(Integer.toString(Integer.MAX_VALUE))));
+        info.getWireMock().register(WireMock.post("/0.6/changeset/2147483647/upload").willReturn(responseDefinitionBuilder));
+        Changeset changeset = new Changeset();
+        assertDoesNotThrow(() -> osmApi.openChangeset(changeset, NullProgressMonitor.INSTANCE));
+        assertEquals(Integer.MAX_VALUE, changeset.getId());
+        osmApi.setChangeset(changeset);
+        assertThrows(OsmApiException.class, () -> osmApi.uploadDiff(Collections.singletonList(new Node(LatLon.ZERO)), NullProgressMonitor.INSTANCE));
+    }
 }
