From: Artur Signell Date: Thu, 16 Apr 2015 11:30:16 +0000 (+0300) Subject: Separate XHR error handling to its own class (#11733) X-Git-Tag: 7.6.0.alpha5~16^2~43 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=010b9641ddee5c2d2aa32599f0caffc85b6ed742;p=vaadin-framework.git Separate XHR error handling to its own class (#11733) Change-Id: Iddd885a6a3a753c0be08c9afe6e97b9e589b8368 --- diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index 7dbcf5e1cf..89ad931b31 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -50,8 +50,6 @@ import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; -import com.google.gwt.regexp.shared.MatchResult; -import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Timer; @@ -61,9 +59,10 @@ import com.google.gwt.user.client.Window.ClosingHandler; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConfiguration.ErrorMessage; -import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent; import com.vaadin.client.ResourceLoader.ResourceLoadEvent; import com.vaadin.client.ResourceLoader.ResourceLoadListener; +import com.vaadin.client.communication.CommunicationProblemEvent; +import com.vaadin.client.communication.CommunicationProblemHandler; import com.vaadin.client.communication.HasJavaScriptConnectorHelper; import com.vaadin.client.communication.Heartbeat; import com.vaadin.client.communication.JsonDecoder; @@ -177,6 +176,9 @@ public class ApplicationConnection implements HasHandlers { */ public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; + private final String JSON_COMMUNICATION_PREFIX = "for(;;);["; + private final String JSON_COMMUNICATION_SUFFIX = "]"; + // will hold the CSRF token once received private String csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE; @@ -487,6 +489,9 @@ public class ApplicationConnection implements HasHandlers { loadingIndicator.setConnection(this); serverRpcQueue = GWT.create(ServerRpcQueue.class); serverRpcQueue.setConnection(this); + communicationProblemHandler = GWT + .create(CommunicationProblemHandler.class); + communicationProblemHandler.setConnection(this); } public void init(WidgetSet widgetSet, ApplicationConfiguration cnf) { @@ -820,21 +825,8 @@ public class ApplicationConnection implements HasHandlers { uri = SharedUtil.addGetParameters(uri, UIConstants.UI_ID_PARAMETER + "=" + configuration.getUIId()); - doUidlRequest(uri, payload); - - } - - /** - * Sends an asynchronous or synchronous UIDL request to the server using the - * given URI. - * - * @param uri - * The URI to use for the request. May includes GET parameters - * @param payload - * The contents of the request to send - */ - protected void doUidlRequest(final String uri, final JsonObject payload) { doUidlRequest(uri, payload, true); + } /** @@ -849,21 +841,16 @@ public class ApplicationConnection implements HasHandlers { * true when a status code 0 should be retried * @since 7.3.7 */ - protected void doUidlRequest(final String uri, final JsonObject payload, + public void doUidlRequest(final String uri, final JsonObject payload, final boolean retry) { RequestCallback requestCallback = new RequestCallback() { + @Override public void onError(Request request, Throwable exception) { - handleError(exception.getMessage(), -1); - } - - private void handleError(String details, int statusCode) { - handleCommunicationError(details, statusCode); - endRequest(); - - // Consider application not running any more and prevent all - // future requests - setApplicationRunning(false); + getCommunicationProblemHandler().xhrException( + payload, + new CommunicationProblemEvent(request, uri, payload, + exception)); } @Override @@ -875,105 +862,40 @@ public class ApplicationConnection implements HasHandlers { int statusCode = response.getStatusCode(); - switch (statusCode) { - case 0: - if (retry) { - /* - * There are 2 situations where the error can pop up: - * - * 1) Request was most likely canceled because the - * browser is maybe navigating away from the page. Just - * send the request again without displaying any error - * in case the navigation isn't carried through. - * - * 2) The browser failed to establish a network - * connection. This was observed with keep-alive - * requests, and under wi-fi roaming conditions. - * - * Status code 0 does indicate that there was no server - * side processing, so we can retry the request. - */ - getLogger().warning("Status code 0, retrying"); - (new Timer() { - @Override - public void run() { - doUidlRequest(uri, payload, false); - } - }).schedule(100); - } else { - handleError("Invalid status code 0 (server down?)", - statusCode); - } - return; - - case 401: - /* - * Authorization has failed. Could be that the session has - * timed out and the container is redirecting to a login - * page. - */ - showAuthenticationError(""); - endRequest(); - return; - - case 503: - /* - * We'll assume msec instead of the usual seconds. If - * there's no Retry-After header, handle the error like a - * 500, as per RFC 2616 section 10.5.4. - */ - String delay = response.getHeader("Retry-After"); - if (delay != null) { - getLogger().warning( - "503, retrying in " + delay + "msec"); - (new Timer() { - @Override - public void run() { - doUidlRequest(uri, payload); - } - }).schedule(Integer.parseInt(delay)); - return; - } - } + if (statusCode != 200) { + // There was a problem + CommunicationProblemEvent problemEvent = new CommunicationProblemEvent( + request, uri, payload, response); - if ((statusCode / 100) == 4) { - // Handle all 4xx errors the same way as (they are - // all permanent errors) - showCommunicationError( - "UIDL could not be read from server. Check servlets mappings. Error code: " - + statusCode, statusCode); - endRequest(); - return; - } else if ((statusCode / 100) == 5) { - // Something's wrong on the server, there's nothing the - // client can do except maybe try again. - handleError("Server error. Error code: " + statusCode, - statusCode); + getCommunicationProblemHandler().xhrInvalidStatusCode( + problemEvent, retry); return; } String contentType = response.getHeader("Content-Type"); if (contentType == null || !contentType.startsWith("application/json")) { - /* - * A servlet filter or equivalent may have intercepted the - * request and served non-UIDL content (for instance, a - * login page if the session has expired.) If the response - * contains a magic substring, do a synchronous refresh. See - * #8241. - */ - MatchResult refreshToken = RegExp.compile( - UIDL_REFRESH_TOKEN + "(:\\s*(.*?))?(\\s|$)").exec( - response.getText()); - if (refreshToken != null) { - redirect(refreshToken.getGroup(2)); - return; - } + getCommunicationProblemHandler().xhrInvalidContent( + new CommunicationProblemEvent(request, uri, + payload, response)); + return; + } + + // for(;;);["+ realJson +"]" + String responseText = response.getText(); + + if (!responseText.startsWith(JSON_COMMUNICATION_PREFIX)) { + getCommunicationProblemHandler().xhrInvalidContent( + new CommunicationProblemEvent(request, uri, + payload, response)); + return; } - // for(;;);[realjson] - final String jsonText = response.getText().substring(9, - response.getText().length() - 1); + final String jsonText = responseText.substring( + JSON_COMMUNICATION_PREFIX.length(), + responseText.length() + - JSON_COMMUNICATION_SUFFIX.length()); + handleJSONText(jsonText, statusCode); } }; @@ -983,8 +905,8 @@ public class ApplicationConnection implements HasHandlers { try { doAjaxRequest(uri, payload, requestCallback); } catch (RequestException e) { - getLogger().log(Level.SEVERE, "Error in server request", e); - endRequest(); + getCommunicationProblemHandler().xhrException(payload, + new CommunicationProblemEvent(null, uri, payload, e)); } } } @@ -1125,6 +1047,8 @@ public class ApplicationConnection implements HasHandlers { private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; protected ServerRpcQueue serverRpcQueue; + protected CommunicationProblemHandler communicationProblemHandler; + /** * The value of an undefined sync id. *

@@ -1192,7 +1116,7 @@ public class ApplicationConnection implements HasHandlers { * @param details * Optional details. */ - protected void showAuthenticationError(String details) { + public void showAuthenticationError(String details) { getLogger().severe("Authentication error: " + details); showError(details, configuration.getAuthorizationError()); } @@ -1231,7 +1155,7 @@ public class ApplicationConnection implements HasHandlers { eventBus.fireEvent(new RequestStartingEvent(this)); } - protected void endRequest() { + public void endRequest() { if (!hasActiveRequest) { getLogger().severe("No active request"); } @@ -1345,11 +1269,11 @@ public class ApplicationConnection implements HasHandlers { private static native ValueMap parseJSONResponse(String jsonText) /*-{ - try { - return JSON.parse(jsonText); - } catch (ignored) { - return eval('(' + jsonText + ')'); - } + try { + return JSON.parse(jsonText); + } catch (ignored) { + return eval('(' + jsonText + ')'); + } }-*/; private void handleReceivedJSONMessage(Date start, String jsonText, @@ -1448,7 +1372,7 @@ public class ApplicationConnection implements HasHandlers { if (json.containsKey("redirect")) { String url = json.getValueMap("redirect").getString("url"); getLogger().info("redirecting to " + url); - redirect(url); + WidgetUtil.redirect(url); return; } @@ -1534,7 +1458,8 @@ public class ApplicationConnection implements HasHandlers { redirectTimer = new Timer() { @Override public void run() { - redirect(timedRedirect.getString("url")); + WidgetUtil.redirect(timedRedirect + .getString("url")); } }; sessionExpirationInterval = timedRedirect @@ -2607,16 +2532,6 @@ public class ApplicationConnection implements HasHandlers { } } - // Redirect browser, null reloads current page - public static native void redirect(String url) - /*-{ - if (url) { - $wnd.location = url; - } else { - $wnd.location.reload(false); - } - }-*/; - private void addVariableToQueue(String connectorId, String variableName, Object value, boolean immediate) { boolean lastOnly = !immediate; @@ -2637,19 +2552,19 @@ public class ApplicationConnection implements HasHandlers { } public void doSendPendingVariableChanges() { - if (isApplicationRunning()) { - if (hasActiveRequest() || (push != null && !push.isActive())) { - // There is an active request or push is enabled but not active - // -> send when current request completes or push becomes active - } else { - sendInvocationsToServer(); - } - } else { + if (!isApplicationRunning()) { getLogger() .warning( - "Trying to send variable changes from not yet started or stopped application"); + "Trying to send RPC from not yet started or stopped application"); return; } + + if (hasActiveRequest() || (push != null && !push.isActive())) { + // There is an active request or push is enabled but not active + // -> send when current request completes or push becomes active + } else { + sendInvocationsToServer(); + } } /** @@ -3357,7 +3272,7 @@ public class ApplicationConnection implements HasHandlers { } } - private void handleCommunicationError(String details, int statusCode) { + public void handleCommunicationError(String details, int statusCode) { boolean handled = false; if (communicationErrorDelegate != null) { handled = communicationErrorDelegate.onError(details, statusCode); @@ -3568,4 +3483,13 @@ public class ApplicationConnection implements HasHandlers { public ServerRpcQueue getServerRpcQueue() { return serverRpcQueue; } + + /** + * Gets the communication error handler for this application + * + * @return the server RPC queue + */ + public CommunicationProblemHandler getCommunicationProblemHandler() { + return communicationProblemHandler; + } } diff --git a/client/src/com/vaadin/client/WidgetUtil.java b/client/src/com/vaadin/client/WidgetUtil.java index 4906197b29..9f7fdbdb6b 100644 --- a/client/src/com/vaadin/client/WidgetUtil.java +++ b/client/src/com/vaadin/client/WidgetUtil.java @@ -64,6 +64,23 @@ public class WidgetUtil { debugger; }-*/; + /** + * Redirects the browser to the given url or refreshes the page if url is + * null + * + * @since + * @param url + * The url to redirect to or null to refresh + */ + public static native void redirect(String url) + /*-{ + if (url) { + $wnd.location = url; + } else { + $wnd.location.reload(false); + } + }-*/; + /** * Helper method for a bug fix #14041. For mozilla getKeyCode return 0 for * space bar (because space is considered as char). If return 0 use diff --git a/client/src/com/vaadin/client/communication/CommunicationProblemEvent.java b/client/src/com/vaadin/client/communication/CommunicationProblemEvent.java new file mode 100644 index 0000000000..e3e14bbd82 --- /dev/null +++ b/client/src/com/vaadin/client/communication/CommunicationProblemEvent.java @@ -0,0 +1,91 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.Response; + +import elemental.json.JsonObject; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class CommunicationProblemEvent { + + private Throwable exception; + private Request request; + private Response response; + private String uri; + private JsonObject payload; + + /** + * @param exception + */ + public CommunicationProblemEvent(Request request, String uri, + JsonObject payload, Throwable exception) { + this.request = request; + this.exception = exception; + this.uri = uri; + } + + /** + * @param request + * @param statusCode + */ + public CommunicationProblemEvent(Request request, String uri, + JsonObject payload, Response response) { + this.request = request; + this.response = response; + this.uri = uri; + } + + /** + * @return the exception + */ + public Throwable getException() { + return exception; + } + + /** + * @return the request + */ + public Request getRequest() { + return request; + } + + /** + * @return the response + */ + public Response getResponse() { + return response; + } + + /** + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * @return the payload + */ + public JsonObject getPayload() { + return payload; + } +} \ No newline at end of file diff --git a/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java b/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java new file mode 100644 index 0000000000..23b2678365 --- /dev/null +++ b/client/src/com/vaadin/client/communication/CommunicationProblemHandler.java @@ -0,0 +1,223 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.communication; + +import java.util.logging.Logger; + +import com.google.gwt.http.client.Response; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.Timer; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.WidgetUtil; + +import elemental.json.JsonObject; + +/** + * TODO + * + * @since + * @author Vaadin Ltd + */ +public class CommunicationProblemHandler { + + private ApplicationConnection connection; + + /** + * Sets the application connection this queue is connected to + * + * @param connection + * the application connection this queue is connected to + */ + public void setConnection(ApplicationConnection connection) { + this.connection = connection; + } + + public static Logger getLogger() { + return Logger.getLogger(CommunicationProblemHandler.class.getName()); + } + + /** + * @param payload + * @param communicationProblemEvent + */ + public void xhrException(JsonObject payload, CommunicationProblemEvent event) { + handleUnrecoverableCommunicationError( + event.getException().getMessage(), event); + + } + + /** + * @param event + * @param retry + */ + public void xhrInvalidStatusCode(final CommunicationProblemEvent event, + boolean retry) { + Response response = event.getResponse(); + int statusCode = response.getStatusCode(); + if (statusCode == 0) { + handleNoConnection(event, retry); + } else if (statusCode == 401) { + handleAuthorizationFailed(event); + } else if (statusCode == 503) { + handleServiceUnavailable(event); + } else if ((statusCode / 100) == 4) { + // Handle all 4xx errors the same way as (they are + // all permanent errors) + String msg = "UIDL could not be read from server." + + " Check servlets mappings. Error code: " + statusCode; + handleUnrecoverableCommunicationError(msg, event); + } else if ((statusCode / 100) == 5) { + // Something's wrong on the server, there's nothing the + // client can do except maybe try again. + String msg = "Server error. Error code: " + statusCode; + handleUnrecoverableCommunicationError(msg, event); + return; + } + + } + + /** + * @since + * @param event + */ + private void handleServiceUnavailable(final CommunicationProblemEvent event) { + /* + * We'll assume msec instead of the usual seconds. If there's no + * Retry-After header, handle the error like a 500, as per RFC 2616 + * section 10.5.4. + */ + String delay = event.getResponse().getHeader("Retry-After"); + if (delay != null) { + getLogger().warning("503, retrying in " + delay + "msec"); + (new Timer() { + @Override + public void run() { + // doUidlRequest does not call startRequest so we do + // not call endRequest before it + connection.doUidlRequest(event.getUri(), + event.getPayload(), true); + } + }).schedule(Integer.parseInt(delay)); + return; + } else { + String msg = "Server error. Error code: " + + event.getResponse().getStatusCode(); + handleUnrecoverableCommunicationError(msg, event); + } + + } + + /** + * @since + * @param event + */ + private void handleAuthorizationFailed(CommunicationProblemEvent event) { + /* + * Authorization has failed (401). Could be that the session has timed + * out and the container is redirecting to a login page. + */ + connection.showAuthenticationError(""); + endRequestAndStopApplication(); + } + + /** + * @since + */ + private void endRequestAndStopApplication() { + connection.endRequest(); + + // Consider application not running any more and prevent all + // future requests + connection.setApplicationRunning(false); + } + + /** + * @since + * @param event + * @param retry + */ + private void handleNoConnection(final CommunicationProblemEvent event, + boolean retry) { + if (retry) { + /* + * There are 2 situations where the error can pop up: + * + * 1) Request was most likely canceled because the browser is maybe + * navigating away from the page. Just send the request again + * without displaying any error in case the navigation isn't carried + * through. + * + * 2) The browser failed to establish a network connection. This was + * observed with keep-alive requests, and under wi-fi roaming + * conditions. + * + * Status code 0 does indicate that there was no server side + * processing, so we can retry the request. + */ + getLogger().warning("Status code 0, retrying"); + (new Timer() { + @Override + public void run() { + // doUidlRequest does not call startRequest so we do + // not call endRequest before it + connection.doUidlRequest(event.getUri(), + event.getPayload(), false); + } + }).schedule(100); + } else { + handleUnrecoverableCommunicationError( + "Invalid status code 0 (server down?)", event); + } + + } + + private void handleUnrecoverableCommunicationError(String details, + CommunicationProblemEvent event) { + Response response = event.getResponse(); + int statusCode = -1; + if (response != null) { + statusCode = response.getStatusCode(); + } + connection.handleCommunicationError(details, statusCode); + + endRequestAndStopApplication(); + } + + /** + * @since + * @param event + */ + public void xhrInvalidContent(CommunicationProblemEvent event) { + String responseText = event.getResponse().getText(); + /* + * A servlet filter or equivalent may have intercepted the request and + * served non-UIDL content (for instance, a login page if the session + * has expired.) If the response contains a magic substring, do a + * synchronous refresh. See #8241. + */ + MatchResult refreshToken = RegExp.compile( + ApplicationConnection.UIDL_REFRESH_TOKEN + + "(:\\s*(.*?))?(\\s|$)").exec(responseText); + if (refreshToken != null) { + WidgetUtil.redirect(refreshToken.getGroup(2)); + } else { + handleUnrecoverableCommunicationError( + "Invalid JSON response from server: " + responseText, event); + } + + } +} diff --git a/client/src/com/vaadin/client/ui/VNotification.java b/client/src/com/vaadin/client/ui/VNotification.java index 2f68976471..d5387b761a 100644 --- a/client/src/com/vaadin/client/ui/VNotification.java +++ b/client/src/com/vaadin/client/ui/VNotification.java @@ -655,7 +655,7 @@ public class VNotification extends VOverlay { n.show(html.toString(), VNotification.CENTERED_TOP, VNotification.STYLE_SYSTEM); } else { - ApplicationConnection.redirect(url); + WidgetUtil.redirect(url); } } @@ -674,7 +674,7 @@ public class VNotification extends VOverlay { @Override public void notificationHidden(HideEvent event) { - ApplicationConnection.redirect(url); + WidgetUtil.redirect(url); } } diff --git a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java index 0da1c6c775..139ee7cf3a 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/MockApplicationConnection.java @@ -72,11 +72,11 @@ public class MockApplicationConnection extends ApplicationConnection { } @Override - protected void doUidlRequest(String uri, JsonObject payload) { + public void doUidlRequest(String uri, JsonObject payload, boolean retry) { JsonValue jsonValue = payload.get(ApplicationConstants.CSRF_TOKEN); lastCsrfTokenSent = jsonValue != null ? jsonValue.toJson() : null; - super.doUidlRequest(uri, payload); + super.doUidlRequest(uri, payload, retry); } }