Better error handling for common osm api error conditions

* if 509 Bandwidth Exceeded / 429 Too Many Requests, prompt for login
(closes #2262)
* if 400 Bad Request / 401 Unauthorized / 403 Forbidden - logout and retry
(closes #3546)
This commit is contained in:
Bryan Housel
2016-11-08 21:10:53 -05:00
parent 7069ca8ef7
commit 16ada1f29a
10 changed files with 108 additions and 40 deletions
+6 -1
View File
@@ -2601,12 +2601,17 @@ img.tile-removing {
text-align: right;
width: 100%;
padding: 0px 10px;
color: #eee;
}
.api-status.offline,
.api-status.readonly,
.api-status.error {
background: red;
background: #a22;
}
.api-status-login {
color: #aaf;
}
/* Modals
+2
View File
@@ -180,6 +180,7 @@ en:
localized_translation_language: Choose language
localized_translation_name: Name
zoom_in_edit: Zoom in to Edit
login: login
logout: logout
loading_auth: "Connecting to OpenStreetMap..."
report_a_bug: Report a bug
@@ -191,6 +192,7 @@ en:
error: Unable to connect to API.
offline: The API is offline. Please try editing later.
readonly: The API is read-only. You will need to wait to save your changes.
rateLimit: The API is limiting anonymous connections. You can fix this by logging in.
commit:
title: Save Changes
description_placeholder: Brief description of your contributions (required)
+3 -1
View File
@@ -227,6 +227,7 @@
"localized_translation_name": "Name"
},
"zoom_in_edit": "Zoom in to Edit",
"login": "login",
"logout": "logout",
"loading_auth": "Connecting to OpenStreetMap...",
"report_a_bug": "Report a bug",
@@ -238,7 +239,8 @@
"status": {
"error": "Unable to connect to API.",
"offline": "The API is offline. Please try editing later.",
"readonly": "The API is read-only. You will need to wait to save your changes."
"readonly": "The API is read-only. You will need to wait to save your changes.",
"rateLimit": "The API is limiting anonymous connections. You can fix this by logging in."
},
"commit": {
"title": "Save Changes",
+2
View File
@@ -64,6 +64,8 @@ export function rendererMap(context) {
context
.on('change.map', immediateRedraw);
context.connection()
.on('change.map', immediateRedraw);
context.history()
.on('change.map', immediateRedraw);
context.background()
+60 -23
View File
@@ -9,7 +9,7 @@ import { utilDetect } from '../util/detect';
import { utilRebind } from '../util/rebind';
var dispatch = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading', 'loaded'),
var dispatch = d3.dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded'),
useHttps = window.location.protocol === 'https:',
protocol = useHttps ? 'https:' : 'http:',
urlroot = protocol + '//www.openstreetmap.org',
@@ -20,25 +20,28 @@ var dispatch = d3.dispatch('authenticating', 'authenticated', 'auth', 'loading',
url: urlroot,
oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT',
oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL',
loading: authenticating,
done: authenticated
loading: authLoading,
done: authDone
}),
rateLimitError,
userDetails,
off;
function authenticating() {
dispatch.call('authenticating');
function authLoading() {
dispatch.call('authLoading');
}
function authenticated() {
dispatch.call('authenticated');
function authDone() {
dispatch.call('authDone');
}
function abortRequest(i) {
i.abort();
if (i) {
i.abort();
}
}
@@ -129,10 +132,10 @@ var parsers = {
};
function parse(dom) {
if (!dom || !dom.childNodes) return;
function parse(xml) {
if (!xml || !xml.childNodes) return;
var root = dom.childNodes[0],
var root = xml.childNodes[0],
children = root.childNodes,
entities = [];
@@ -151,7 +154,7 @@ function parse(dom) {
export default {
init: function() {
this.event = utilRebind(this, dispatch, 'on');
utilRebind(this, dispatch, 'on');
},
@@ -189,9 +192,34 @@ export default {
loadFromAPI: function(path, callback) {
function done(err, dom) {
return callback(err, parse(dom));
var that = this;
function done(err, xml) {
var isAuthenticated = that.authenticated();
// 400 Bad Request, 401 Unauthorized, 403 Forbidden
// Logout and retry the request..
if (isAuthenticated && err &&
(err.status === 400 || err.status === 401 || err.status === 403)) {
that.logout();
that.loadFromAPI(path, callback);
// 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 === 509 || err.status === 429)) {
rateLimitError = err;
dispatch.call('change');
}
if (callback) {
callback(err, parse(xml));
}
}
}
if (this.authenticated()) {
return oauth.xhr({ method: 'GET', path: path }, done);
} else {
@@ -402,8 +430,13 @@ export default {
status: function(callback) {
function done(capabilities) {
var apiStatus = capabilities.getElementsByTagName('status');
callback(undefined, apiStatus[0].getAttribute('api'));
if (rateLimitError) {
callback(rateLimitError, 'rateLimited');
} else {
var apiStatus = capabilities.getElementsByTagName('status'),
val = apiStatus[0].getAttribute('api');
callback(undefined, val);
}
}
d3.xml(urlroot + '/api/capabilities').get()
.on('load', done)
@@ -467,8 +500,10 @@ export default {
inflight[id] = that.loadFromAPI(
'/api/0.6/map?bbox=' + tile.extent.toParam(),
function(err, parsed) {
loadedTiles[id] = true;
delete inflight[id];
if (!err) {
loadedTiles[id] = true;
}
if (callback) {
callback(err, _.extend({ data: parsed }, tile));
@@ -477,7 +512,8 @@ export default {
if (_.isEmpty(inflight)) {
dispatch.call('loaded');
}
});
}
);
});
},
@@ -487,10 +523,10 @@ export default {
oauth.options(_.extend({
url: urlroot,
loading: authenticating,
done: authenticated
loading: authLoading,
done: authDone
}, options));
dispatch.call('auth');
dispatch.call('change');
this.reset();
return this;
},
@@ -512,7 +548,7 @@ export default {
logout: function() {
userDetails = undefined;
oauth.logout();
dispatch.call('auth');
dispatch.call('change');
return this;
},
@@ -520,7 +556,8 @@ export default {
authenticate: function(callback) {
userDetails = undefined;
function done(err, res) {
dispatch.call('auth');
rateLimitError = undefined;
dispatch.call('change');
if (callback) callback(err, res);
}
return oauth.authenticate(done);
+3 -1
View File
@@ -67,7 +67,9 @@ export function uiAccount(context) {
.attr('id', 'userLink')
.classed('hide', true);
connection.event.on('auth.account', function() { update(selection); });
connection
.on('change.account', function() { update(selection); });
update(selection);
};
}
+4 -3
View File
@@ -271,14 +271,15 @@ export function uiInit(context) {
.call(uiRestore(context));
var authenticating = uiLoading(context)
.message(t('loading_auth'));
.message(t('loading_auth'))
.blocking(true);
context.connection()
.on('authenticating.ui', function() {
.on('authLoading.ui', function() {
context.container()
.call(authenticating);
})
.on('authenticated.ui', function() {
.on('authDone.ui', function() {
authenticating.close();
});
}
+2 -2
View File
@@ -8,13 +8,13 @@ export function uiSpinner(context) {
.attr('src', context.imagePath('loader-black.gif'))
.style('opacity', 0);
connection.event
connection
.on('loading.spinner', function() {
img.transition()
.style('opacity', 1);
});
connection.event
connection
.on('loaded.spinner', function() {
img.transition()
.style('opacity', 0);
+23 -6
View File
@@ -1,18 +1,36 @@
import * as d3 from 'd3';
import { t } from '../util/locale';
import { svgIcon } from '../svg/index';
export function uiStatus(context) {
var connection = context.connection(),
errCount = 0;
var connection = context.connection();
return function(selection) {
function update() {
connection.status(function(err, apiStatus) {
selection.html('');
if (err && errCount++ < 2) return;
if (err) {
selection.text(t('status.error'));
if (apiStatus === 'rateLimited') {
selection
.text(t('status.rateLimit'))
.append('a')
.attr('class', 'api-status-login')
.attr('target', '_blank')
.call(svgIcon('#icon-out-link', 'inline'))
.append('span')
.text(t('login'))
.on('click.login', function() {
d3.event.preventDefault();
connection.authenticate();
});
} else {
// TODO: nice messages for different error types
selection.text(t('status.error'));
}
} else if (apiStatus === 'readonly') {
selection.text(t('status.readonly'));
} else if (apiStatus === 'offline') {
@@ -20,12 +38,11 @@ export function uiStatus(context) {
}
selection.attr('class', 'api-status ' + (err ? 'error' : apiStatus));
if (!err) errCount = 0;
});
}
connection
.on('auth', function() { update(selection); });
.on('change', function() { update(selection); });
window.setInterval(update, 90000);
update(selection);
+3 -3
View File
@@ -44,9 +44,9 @@ describe('iD.serviceOsm', function () {
expect(connection.changesetURL(1)).to.equal('http://example.com/changeset/1');
});
it('emits an auth event', function(done) {
connection.on('auth', function() {
connection.on('auth', null);
it('emits a change event', function(done) {
connection.on('change', function() {
connection.on('change', null);
done();
});
connection.switch({ urlroot: 'http://example.com' });