diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70685b2b1a..79fa8a2150 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
- [#1240](https://github.com/plotly/dash/pull/1240) Adds `callback_context` to clientside callbacks (e.g. `dash_clientside.callback_context.triggered`). Supports `triggered`, `inputs`, `inputs_list`, `states`, and `states_list`, all of which closely resemble their serverside cousins.
+### Changed
+- [#1237](https://github.com/plotly/dash/pull/1237) Closes [#920](https://github.com/plotly/dash/issues/920): Converts hot reload fetch failures into a server status indicator showing whether the latest fetch succeeded or failed. Callback fetch failures still appear as errors but have a clearer message.
+
## [1.12.0] - 2020-05-05
### Added
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
diff --git a/dash-renderer/src/actions/api.js b/dash-renderer/src/actions/api.js
index 788da55b71..8a78708d9f 100644
--- a/dash-renderer/src/actions/api.js
+++ b/dash-renderer/src/actions/api.js
@@ -30,43 +30,61 @@ const request = {GET, POST};
export default function apiThunk(endpoint, method, store, id, body) {
return (dispatch, getState) => {
- const config = getState().config;
+ const {config} = getState();
const url = `${urlBase(config)}${endpoint}`;
+ function setConnectionStatus(connected) {
+ if (getState().error.backEndConnected !== connected) {
+ dispatch({
+ type: 'SET_CONNECTION_STATUS',
+ payload: connected,
+ });
+ }
+ }
+
dispatch({
type: store,
payload: {id, status: 'loading'},
});
return request[method](url, config.fetch, body)
- .then(res => {
- const contentType = res.headers.get('content-type');
- if (
- contentType &&
- contentType.indexOf('application/json') !== -1
- ) {
- return res.json().then(json => {
- dispatch({
- type: store,
- payload: {
- status: res.status,
- content: json,
- id,
- },
+ .then(
+ res => {
+ setConnectionStatus(true);
+ const contentType = res.headers.get('content-type');
+ if (
+ contentType &&
+ contentType.indexOf('application/json') !== -1
+ ) {
+ return res.json().then(json => {
+ dispatch({
+ type: store,
+ payload: {
+ status: res.status,
+ content: json,
+ id,
+ },
+ });
+ return json;
});
- return json;
+ }
+ logWarningOnce(
+ 'Response is missing header: content-type: application/json'
+ );
+ return dispatch({
+ type: store,
+ payload: {
+ id,
+ status: res.status,
+ },
});
+ },
+ () => {
+ // fetch rejection - this means the request didn't return,
+ // we don't get here from 400/500 errors, only network
+ // errors or unresponsive servers.
+ setConnectionStatus(false);
}
- logWarningOnce(
- 'Response is missing header: content-type: application/json'
- );
- return dispatch({
- type: store,
- payload: {
- id,
- status: res.status,
- },
- });
- })
+ )
.catch(err => {
const message = 'Error from API call: ' + endpoint;
handleAsyncError(err, message, dispatch);
diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js
index 05d0427381..a48c81a687 100644
--- a/dash-renderer/src/actions/index.js
+++ b/dash-renderer/src/actions/index.js
@@ -495,29 +495,37 @@ function handleServerside(config, payload, hooks) {
headers: getCSRFHeader(),
body: JSON.stringify(payload),
})
- ).then(res => {
- const {status} = res;
- if (status === STATUS.OK) {
- return res.json().then(data => {
- const {multi, response} = data;
- if (hooks.request_post !== null) {
- hooks.request_post(payload, response);
- }
+ ).then(
+ res => {
+ const {status} = res;
+ if (status === STATUS.OK) {
+ return res.json().then(data => {
+ const {multi, response} = data;
+ if (hooks.request_post !== null) {
+ hooks.request_post(payload, response);
+ }
- if (multi) {
- return response;
- }
+ if (multi) {
+ return response;
+ }
- const {output} = payload;
- const id = output.substr(0, output.lastIndexOf('.'));
- return {[id]: response.props};
- });
- }
- if (status === STATUS.PREVENT_UPDATE) {
- return {};
+ const {output} = payload;
+ const id = output.substr(0, output.lastIndexOf('.'));
+ return {[id]: response.props};
+ });
+ }
+ if (status === STATUS.PREVENT_UPDATE) {
+ return {};
+ }
+ throw res;
+ },
+ () => {
+ // fetch rejection - this means the request didn't return,
+ // we don't get here from 400/500 errors, only network
+ // errors or unresponsive servers.
+ throw new Error('Callback failed: the server did not respond.');
}
- throw res;
- });
+ );
}
const getVals = input =>
diff --git a/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js b/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js
index ddb18ac8d3..794070aebb 100644
--- a/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js
+++ b/dash-renderer/src/components/error/FrontEnd/FrontEndErrorContainer.react.js
@@ -9,7 +9,8 @@ class FrontEndErrorContainer extends Component {
}
render() {
- const errorsLength = this.props.errors.length;
+ const {errors, connected} = this.props;
+ const errorsLength = errors.length;
if (errorsLength === 0) {
return null;
}
@@ -17,7 +18,7 @@ class FrontEndErrorContainer extends Component {
const inAlertsTray = this.props.inAlertsTray;
let cardClasses = 'dash-error-card dash-error-card--container';
- const errorElements = this.props.errors.map((error, i) => {
+ const errorElements = errors.map((error, i) => {
return