GWT client not receiving IncompatibleRemoteService

2019-04-13 10:17发布

When the new version of app is deployed with changes to the model classes (for example adding/removing fields ). Client who has old version running gets com.google.gwt.user.client.rpc.SerializationException with the RPC made with the old client code and this is expected behavior. As a result we expect to see IncompatibleRemoteServiceException on the client side. However we get StatusCodeException.

StatusCodeException is 500 error and we can't customize the client side behavior easily (we don't want to assume every StatusCodeException or 500 error is a new version) . What could we be doing wrong here?

Note: On the server side (log) we get SerializationExcepion obviously since the serialization policy from the old client is no longer valid with the new server. So Why not throwing IncompatibleRemoteServiceException?

Thanks.

1条回答
仙女界的扛把子
2楼-- · 2019-04-13 11:06

Here's my solution facing that issue:

First extend RemoteServiceServlet (all your service servlets will extend from this new class, remember this is server code). This is the relevant code in there:

@Override
protected SerializationPolicy doGetSerializationPolicy(
        HttpServletRequest request, String moduleBaseURL, String strongName) {
    SerializationPolicy sp = super.doGetSerializationPolicy(request,
                                   moduleBaseURL, strongName);
    if(sp == null) { //no policy found, probably wrong client version
        throw new InvalidPolicyException();
    }
    return sp;
}

and

@Override
protected void doUnexpectedFailure(Throwable e) {
    if(e instanceof InvalidPolicyException) { 
        sendError(); //send message to reload client (wrong client version)
        return; //that's it
    }
    super.doUnexpectedFailure(e);
}

The private method sendError() is basically a customized copy from GWT 500 error code (sorry for the ugly Google indentation)

private void sendError() {
    ServletContext servletContext = getServletContext();
    HttpServletResponse response = getThreadLocalResponse();
    try {
          response.setContentType("text/plain");
          response.setStatus(HttpServletResponse.SC_CONFLICT);
          try {
            response.getOutputStream().write("wrong client version".getBytes("UTF-8"));
          } catch (IllegalStateException e) {
            // Handle the (unexpected) case where getWriter() was previously used
            response.getWriter().write("wrong client version");
          }
        } catch (IOException ex) {
          servletContext.log(
              "sendError failed while sending the previous custom failure to the client", ex);
        }
}

I used HttpServletResponse.SC_CONFLICT (409) but you can probably use a clever error code. The message written is not really important.

Then in your custom RpcRequestBuilder (this is the client code)

public class CustomRequestBuilder extends RpcRequestBuilder {

 private class RequestCallbackWrapper implements RequestCallback {

        private RequestCallback callback;

        RequestCallbackWrapper(RequestCallback aCallback) {
            this.callback = aCallback;
        }

        @Override
        public void onResponseReceived(Request request, Response response) {
            if(response.getStatusCode() == 409) { //wrong client version
                Navigator.closeEveryPopUp();
                Navigator.showUncloseablePopUp("Login again!", 
                                new ClickHandler() {

                    @Override
                    public void onClick(ClickEvent event) {
                        //reload your $#%^ client!!!
                        Window.Location.reload();
                    }
                });
            } else {
                //(...)irrelevant security code here(...)
                callback.onResponseReceived(request, response);
            }

        }

        @Override
        public void onError(Request request, Throwable exception) {
            callback.onError(request, exception);
        }

 }

 @Override
 protected void doFinish(RequestBuilder rb) {
    //(...)more irrelevant security code here(...)
    super.doFinish(rb);
    rb.setCallback(new RequestCallbackWrapper(rb.getCallback()));
 }
}

We use multiple popups, that's one reason for the Navigator class; of course use your own style there to warn the user.

EDIT: What's going on here?

Until GWT 1.3.3 IsSerializable was the only interface available to mark a class as GWT RPC serializabled. The next version accepted Java standard Serializable interface for the same purpose, but adding the requirement of a security policy for objects implementing this interface. By default GWT generate a policy for each compilation with a unique hash name. An old client trying to transfer object marked as Serializable will throw at server side a serialization policy exception that will be received at client side as a generic runtime error. IsSerializable allows the old clients to still use the new services as long as the signature remains the same. This means that an alternate solution for this question is to mark every object going thru GWT RPC as IsSerializable. But if for some reason you need your objects not to be referenced to a GWT interface, this is a nice solution for old clients connections.

Check GWT's guide for more details regarding the 1.3.3 fallback.

查看更多
登录 后发表回答