mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-25 09:34:04 +02:00
better handling of rate limited API map calls and other API errors
* retry all unsuccessful map calls after waiting 8 seconds (spinner continues to indicate loading state) * also logged-in users can be rate limited: add dedicated error message * don't log users out when requests return 401/403 (except on the _get own user data_ request, which would indicate that the oauth token was revoked): it's better to show the error message if a legitimate api call was actually unauthorized closes #10299
This commit is contained in:
@@ -43,13 +43,16 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
|||||||
#### :white_check_mark: Validation
|
#### :white_check_mark: Validation
|
||||||
#### :bug: Bugfixes
|
#### :bug: Bugfixes
|
||||||
* fix some direction cones not appearing on railway tracks ([#10843], thanks [@k-yle])
|
* fix some direction cones not appearing on railway tracks ([#10843], thanks [@k-yle])
|
||||||
|
* better handling of rate limited API calls and other API errors ([#10299])
|
||||||
#### :earth_asia: Localization
|
#### :earth_asia: Localization
|
||||||
#### :hourglass: Performance
|
#### :hourglass: Performance
|
||||||
#### :mortar_board: Walkthrough / Help
|
#### :mortar_board: Walkthrough / Help
|
||||||
#### :hammer: Development
|
#### :hammer: Development
|
||||||
|
|
||||||
|
[#10299]: https://github.com/openstreetmap/iD/issues/10299
|
||||||
[#10843]: https://github.com/openstreetmap/iD/pull/10843
|
[#10843]: https://github.com/openstreetmap/iD/pull/10843
|
||||||
|
|
||||||
|
|
||||||
# 2.32.0
|
# 2.32.0
|
||||||
##### 2025-03-05
|
##### 2025-03-05
|
||||||
|
|
||||||
|
|||||||
@@ -596,6 +596,7 @@ en:
|
|||||||
offline: The OpenStreetMap API is offline. Your edits are safe locally. Please come back later.
|
offline: The OpenStreetMap API is offline. Your edits are safe locally. Please come back later.
|
||||||
readonly: The OpenStreetMap API is currently read-only. You can continue editing, but must wait to save your changes.
|
readonly: The OpenStreetMap API is currently read-only. You can continue editing, but must wait to save your changes.
|
||||||
rateLimit: The OpenStreetMap API is limiting anonymous connections. You can fix this by logging in.
|
rateLimit: The OpenStreetMap API is limiting anonymous connections. You can fix this by logging in.
|
||||||
|
rateLimited: The OpenStreetMap API is limiting your connection, please wait.
|
||||||
local_storage_full: You have made too many edits to back up. Consider saving your changes now.
|
local_storage_full: You have made too many edits to back up. Consider saving your changes now.
|
||||||
retry: Retry
|
retry: Retry
|
||||||
commit:
|
commit:
|
||||||
|
|||||||
@@ -118,12 +118,6 @@ export function coreContext() {
|
|||||||
function afterLoad(cid, callback) {
|
function afterLoad(cid, callback) {
|
||||||
return (err, result) => {
|
return (err, result) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
// 400 Bad Request, 401 Unauthorized, 403 Forbidden..
|
|
||||||
if (err.status === 400 || err.status === 401 || err.status === 403) {
|
|
||||||
if (_connection) {
|
|
||||||
_connection.logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof callback === 'function') {
|
if (typeof callback === 'function') {
|
||||||
callback(err);
|
callback(err);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-25
@@ -524,10 +524,6 @@ function updateRtree(item, replace) {
|
|||||||
function wrapcb(thisArg, callback, cid) {
|
function wrapcb(thisArg, callback, cid) {
|
||||||
return function(err, result) {
|
return function(err, result) {
|
||||||
if (err) {
|
if (err) {
|
||||||
// 401 Unauthorized, 403 Forbidden
|
|
||||||
if (err.status === 401 || err.status === 403) {
|
|
||||||
thisArg.logout();
|
|
||||||
}
|
|
||||||
return callback.call(thisArg, err);
|
return callback.call(thisArg, err);
|
||||||
|
|
||||||
} else if (thisArg.getConnectionId() !== cid) {
|
} else if (thisArg.getConnectionId() !== cid) {
|
||||||
@@ -640,24 +636,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAuthenticated = that.authenticated();
|
if ((err && _cachedApiStatus === 'online') ||
|
||||||
|
|
||||||
// 401 Unauthorized, 403 Forbidden
|
|
||||||
// Logout and retry the request.
|
|
||||||
if (isAuthenticated && err && err.status &&
|
|
||||||
(err.status === 401 || err.status === 403)) {
|
|
||||||
that.logout();
|
|
||||||
that.loadFromAPI(path, callback, options);
|
|
||||||
// else, no retry.
|
|
||||||
} else {
|
|
||||||
// 509 Bandwidth Limit Exceeded, 429 Too Many Requests
|
|
||||||
// Set the rateLimitError flag and trigger a warning.
|
|
||||||
if (!isAuthenticated && !_rateLimitError && err && err.status &&
|
|
||||||
(err.status === 509 || err.status === 429)) {
|
|
||||||
_rateLimitError = err;
|
|
||||||
dispatch.call('change');
|
|
||||||
that.reloadApiStatus();
|
|
||||||
} else if ((err && _cachedApiStatus === 'online') ||
|
|
||||||
(!err && _cachedApiStatus !== 'online')) {
|
(!err && _cachedApiStatus !== 'online')) {
|
||||||
// If the response's error state doesn't match the status,
|
// If the response's error state doesn't match the status,
|
||||||
// it's likely we lost or gained the connection so reload the status
|
// it's likely we lost or gained the connection so reload the status
|
||||||
@@ -676,7 +655,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.authenticated()) {
|
if (this.authenticated()) {
|
||||||
return oauth.xhr({
|
return oauth.xhr({
|
||||||
@@ -1098,6 +1076,12 @@ export default {
|
|||||||
var hadRequests = hasInflightRequests(_tileCache);
|
var hadRequests = hasInflightRequests(_tileCache);
|
||||||
abortUnwantedRequests(_tileCache, tiles);
|
abortUnwantedRequests(_tileCache, tiles);
|
||||||
if (hadRequests && !hasInflightRequests(_tileCache)) {
|
if (hadRequests && !hasInflightRequests(_tileCache)) {
|
||||||
|
if (_rateLimitError) {
|
||||||
|
// was rate limited, but has settled
|
||||||
|
_rateLimitError = undefined;
|
||||||
|
dispatch.call('change');
|
||||||
|
this.reloadApiStatus();
|
||||||
|
}
|
||||||
dispatch.call('loaded'); // stop the spinner
|
dispatch.call('loaded'); // stop the spinner
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,23 +1107,43 @@ export default {
|
|||||||
|
|
||||||
_tileCache.inflight[tile.id] = this.loadFromAPI(
|
_tileCache.inflight[tile.id] = this.loadFromAPI(
|
||||||
path + tile.extent.toParam(),
|
path + tile.extent.toParam(),
|
||||||
tileCallback,
|
tileCallback.bind(this),
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
function tileCallback(err, parsed) {
|
function tileCallback(err, parsed) {
|
||||||
delete _tileCache.inflight[tile.id];
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
delete _tileCache.inflight[tile.id];
|
||||||
delete _tileCache.toLoad[tile.id];
|
delete _tileCache.toLoad[tile.id];
|
||||||
_tileCache.loaded[tile.id] = true;
|
_tileCache.loaded[tile.id] = true;
|
||||||
var bbox = tile.extent.bbox();
|
var bbox = tile.extent.bbox();
|
||||||
bbox.id = tile.id;
|
bbox.id = tile.id;
|
||||||
_tileCache.rtree.insert(bbox);
|
_tileCache.rtree.insert(bbox);
|
||||||
|
} else {
|
||||||
|
// map tile loading error: e.g. network connection error,
|
||||||
|
// 509 Bandwidth Limit Exceeded, 429 Too Many Requests
|
||||||
|
if (!_rateLimitError && err.status === 509 || err.status === 429) {
|
||||||
|
// show "API rate limiting" warning
|
||||||
|
_rateLimitError = err;
|
||||||
|
dispatch.call('change');
|
||||||
|
this.reloadApiStatus();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
// retry loading the tiles
|
||||||
|
delete _tileCache.inflight[tile.id];
|
||||||
|
this.loadTile(tile, callback);
|
||||||
|
}, 8000);
|
||||||
}
|
}
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(err, Object.assign({ data: parsed }, tile));
|
callback(err, Object.assign({ data: parsed }, tile));
|
||||||
}
|
}
|
||||||
if (!hasInflightRequests(_tileCache)) {
|
if (!hasInflightRequests(_tileCache)) {
|
||||||
|
if (_rateLimitError) {
|
||||||
|
// was rate limited, but has settled
|
||||||
|
_rateLimitError = undefined;
|
||||||
|
dispatch.call('change');
|
||||||
|
this.reloadApiStatus();
|
||||||
|
}
|
||||||
dispatch.call('loaded'); // stop the spinner
|
dispatch.call('loaded'); // stop the spinner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ export function uiAccount(context) {
|
|||||||
if (!osm.authenticated()) { // logged out
|
if (!osm.authenticated()) { // logged out
|
||||||
render(selection, null);
|
render(selection, null);
|
||||||
} else {
|
} else {
|
||||||
osm.userDetails((err, user) => render(selection, user));
|
osm.userDetails((err, user) => {
|
||||||
|
if (err && err.status === 401) {
|
||||||
|
// 401 Unauthorized
|
||||||
|
// cannot load own user data: there must be something wrong (e.g. API token was revoked)
|
||||||
|
// -> log out to allow user to reauthenticate
|
||||||
|
osm.logout();
|
||||||
|
}
|
||||||
|
render(selection, user);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function uiStatus(context) {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
} else if (apiStatus === 'rateLimited') {
|
} else if (apiStatus === 'rateLimited') {
|
||||||
|
if (!osm.authenticated()) {
|
||||||
selection
|
selection
|
||||||
.call(t.append('osm_api_status.message.rateLimit'))
|
.call(t.append('osm_api_status.message.rateLimit'))
|
||||||
.append('a')
|
.append('a')
|
||||||
@@ -35,6 +36,9 @@ export function uiStatus(context) {
|
|||||||
osm.authenticate();
|
osm.authenticate();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
selection.call(t.append('osm_api_status.message.rateLimited'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
// don't allow retrying too rapidly
|
// don't allow retrying too rapidly
|
||||||
var throttledRetry = _throttle(function() {
|
var throttledRetry = _throttle(function() {
|
||||||
|
|||||||
@@ -163,63 +163,6 @@ describe('iD.serviceOsm', function () {
|
|||||||
expect(typeof payload).to.eql('object');
|
expect(typeof payload).to.eql('object');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('retries an authenticated call unauthenticated if 401 Unauthorized', async () => {
|
|
||||||
fetchMock.mock('https://www.openstreetmap.org' + path, {
|
|
||||||
body: response,
|
|
||||||
status: 200,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
serverXHR.respondWith('GET', 'https://www.openstreetmap.org' + path,
|
|
||||||
[401, { 'Content-Type': 'text/plain' }, 'Unauthorized']);
|
|
||||||
|
|
||||||
login();
|
|
||||||
|
|
||||||
const xml = promisify(connection.loadFromAPI).call(connection, path);
|
|
||||||
serverXHR.respond();
|
|
||||||
|
|
||||||
expect(typeof await xml).to.eql('object');
|
|
||||||
expect(connection.authenticated()).to.be.not.ok;
|
|
||||||
expect(fetchMock.called()).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('retries an authenticated call unauthenticated if 401 Unauthorized', async () => {
|
|
||||||
fetchMock.mock('https://www.openstreetmap.org' + path, {
|
|
||||||
body: response,
|
|
||||||
status: 200,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
serverXHR.respondWith('GET', 'https://www.openstreetmap.org' + path,
|
|
||||||
[401, { 'Content-Type': 'text/plain' }, 'Unauthorized']);
|
|
||||||
|
|
||||||
login();
|
|
||||||
|
|
||||||
const xml = promisify(connection.loadFromAPI).call(connection, path);
|
|
||||||
serverXHR.respond();
|
|
||||||
|
|
||||||
expect(typeof await xml).to.eql('object');
|
|
||||||
expect(connection.authenticated()).to.be.not.ok;
|
|
||||||
expect(fetchMock.called()).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('retries an authenticated call unauthenticated if 403 Forbidden', async () => {
|
|
||||||
fetchMock.mock('https://www.openstreetmap.org' + path, {
|
|
||||||
body: response,
|
|
||||||
status: 200,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
serverXHR.respondWith('GET', 'https://www.openstreetmap.org' + path,
|
|
||||||
[403, { 'Content-Type': 'text/plain' }, 'Forbidden']);
|
|
||||||
|
|
||||||
login();
|
|
||||||
const xml = promisify(connection.loadFromAPI).call(connection, path);
|
|
||||||
serverXHR.respond();
|
|
||||||
|
|
||||||
expect(typeof await xml).to.eql('object');
|
|
||||||
expect(connection.authenticated()).to.be.not.ok;
|
|
||||||
expect(fetchMock.called()).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('dispatches change event if 509 Bandwidth Limit Exceeded', async () => {
|
it('dispatches change event if 509 Bandwidth Limit Exceeded', async () => {
|
||||||
fetchMock.mock('https://www.openstreetmap.org' + path, {
|
fetchMock.mock('https://www.openstreetmap.org' + path, {
|
||||||
body: 'Bandwidth Limit Exceeded',
|
body: 'Bandwidth Limit Exceeded',
|
||||||
|
|||||||
Reference in New Issue
Block a user