GWT-RPC

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler and the server with -sourceLevel 17, the Jakarta Servlet 6 server (Jetty 12.1.10, EE 10) of the Bookshelf scenario and the gwt-user and gwt-servlet-jakarta 2.13.1 jars and sources, checked on 2026-10-09. GWT-RPC spans three kinds of code at once. The shared interfaces and data classes are compiled twice, once by GWT to JavaScript and once by javac to bytecode. The client part is the generated proxy, the AsyncCallback objects and the code of the calling presenter, all compiled to JavaScript. The server part is a servlet that decodes the request, calls your Java method and encodes the result. The build has to deploy one extra artifact, the serialisation policy file that the compiler writes next to the JavaScript. Every output block on this page was produced by running the examples in a private copy of the Bookshelf scenario: a lab module compiled with the 2.13.1 compiler, served by Jetty 12 on port 8932 and driven by headless Chrome 155 through the DevTools protocol, probe modules compiled with the 2.13.1 compiler for the generator messages, curl against the servlet for the wire format, and plain JUnit tests on JDK 21 for the presenter. Where a statement comes only from the Developer Guide, the Javadoc or the 2.13.1 source, the text says so.

GWT-RPC is a remote procedure call mechanism between a GWT client and a Java servlet that is built into the toolkit. You write a Java interface, a servlet that implements it and a callback, and GWT generates the code that turns method arguments and results into a text payload, sends it over XMLHttpRequest and turns the answer back into Java objects. The Developer Guide is explicit about what it is not: the service is not a SOAP or REST web service, it is not meant to be a public API for other clients, and it does not replace a Jakarta EE back end. It is "getting from the client to the server" for the application that shipped the client, and the rest of this page follows from that. Other ways to talk to a server (JSON, XML, REST libraries, RequestFactory) are compared on the communication overview page (Client-Server Communication).

The moving parts

A service is a small family of types, and the naming rules between them are checked by the compiler.

Table 1. The types behind one GWT-RPC service
Type Runs in Who writes it and what it does

CatalogService extends RemoteService

shared, client (as a type), server

You. The synchronous interface is the definitive contract. RemoteService is an empty marker interface. The @RemoteServiceRelativePath("catalog") annotation supplies the default URL.

CatalogServiceAsync

shared, client

You (or an IDE). Same package and name plus Async, one method per service method with an extra last AsyncCallback parameter and a return type of void, Request or RequestBuilder. Never implemented by hand in production code.

CatalogService_Proxy

client

Generated by ServiceInterfaceProxyGenerator when your code calls GWT.create(CatalogService.class). It implements the Async interface, ServiceDefTarget, HasRpcToken and SerializationStreamFactory, and serialises the arguments. The name appears in messages (see the pitfalls).

CatalogService_TypeSerializer and field serializers

client

Generated per service. One serialiser per type in the policy, with the type signature hash that the server checks.

AsyncCallback<T>

client

You. onSuccess(T) for the result, onFailure(Throwable) for every error.

CatalogServiceImpl extends RemoteServiceServlet implements CatalogService

server

You. The servlet class that is mapped to the URL. It implements the synchronous interface, not the Async one.

<hash>.gwt.rpc

written by the compiler, read by the server

The serialisation policy: the whitelist of types that may travel, one file per service interface and build.

rpcPolicyManifest/manifest.txt

build output

Maps each service class to its policy file. It is written to the compiler’s deploy directory (WEB-INF/deploy/<module>/rpcPolicyManifest/), not to the served module directory.

The compiler also reads the rpc.blacklist and rpc.enhancedClasses configuration properties and the gwt.suppressNonStaticFinalFieldWarnings and rpc.XserializeFinalFields properties that RemoteService.gwt.xml defines; they appear in the sections where they matter. The module that declares all of this is already inherited: com.google.gwt.user.User inherits com.google.gwt.user.RemoteService, which inherits com.google.gwt.http.HTTP, so a module that inherits User needs no extra <inherits> line for RPC. If your module inherits a smaller set (for example only com.google.gwt.core.Core), add <inherits name="com.google.gwt.user.RemoteService"/>.

The life of one call

The diagram follows getBooks(0, 10) of the Bookshelf catalogue from the presenter to the callback. The names of the checks are the real ones of the 2.13.1 sources.

sequenceDiagram participant P as Presenter participant X as CatalogService_Proxy participant S as RemoteServiceServlet participant L as Policy file (.gwt.rpc) participant I as CatalogServiceImpl P->>X: getBooks(0, 10, callback) X->>X: serialise the arguments (type serializer) X->>S: POST text/x-gwt-rpc + X-GWT-Permutation + X-GWT-Module-Base S->>S: checkPermutationStrongName (header present?) S->>L: load policy by strong name (cached per module base and name) S->>S: RPC.decodeRequest (interface, method, signatures, types) S->>I: onAfterRequestDeserialized, then invoke getBooks I-->>S: BookPage, or a declared exception S->>S: encode //OK[...] or //EX[...] against the policy S-->>X: 200 application/json, optionally gzip X->>X: parse (JSON.parse from RPC version 8, eval before) X-->>P: callback.onSuccess(BookPage) or callback.onFailure(caught)

Three things in the diagram explain most of the pitfalls later on. First, nothing is sent until the browser’s event loop is free again: the call returns immediately and the callback runs later (see Asynchronous calls). Second, the server decides what may be deserialised from the policy file, not from the class path, which is why a missing policy file breaks every call (see Serialisation policy files). Third, an error can happen at four different places (the proxy, the transport, the servlet, the callback), and each place reports through a different exception (see Exceptions: everything that can reach onFailure).

Defining the Bookshelf CatalogService

The running scenario of this section is the Bookshelf (books, authors, reviews, loans) as a three-module GWT application: bookshelf-shared, bookshelf-client and bookshelf-server (see Project structure and modules). The service lives in the shared module because both sides compile it.

bookshelf-shared: the synchronous service interface
package com.example.bookshelf.shared;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

/** GWT-RPC service. Mapped to the module base URL plus "catalog". */
@RemoteServiceRelativePath("catalog")
public interface CatalogService extends RemoteService {
  BookPage getBooks(int start, int length);

  Book getBook(long id) throws BookNotFoundException;

  Book saveBook(Book book) throws BookNotFoundException, AccessDeniedException;

  int lend(long id) throws BookNotFoundException;

  java.util.ArrayList<Review> reviews(long bookId) throws BookNotFoundException;

  java.util.HashMap<Format, Integer> copiesByFormat();

  int revision();
}
bookshelf-shared: the asynchronous interface
package com.example.bookshelf.shared;

import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.user.client.rpc.AsyncCallback;

public interface CatalogServiceAsync {
  void getBooks(int start, int length, AsyncCallback<BookPage> callback);

  Request getBook(long id, AsyncCallback<Book> callback);

  RequestBuilder saveBook(Book book, AsyncCallback<Book> callback);

  void lend(long id, AsyncCallback<Integer> callback);

  void reviews(long bookId, AsyncCallback<java.util.ArrayList<Review>> callback);

  void copiesByFormat(AsyncCallback<java.util.HashMap<Format, Integer>> callback);

  void revision(AsyncCallback<Integer> callback);
}

The rules that the generator enforces, all from ServiceInterfaceProxyGenerator:

  • The Async interface has the same package and simple name plus Async: com.example.bookshelf.shared.CatalogServiceAsync for com.example.bookshelf.shared.CatalogService.

  • Each synchronous method has one asynchronous method with the same name and the same parameters plus a last AsyncCallback<R> parameter, where R is the return type boxed (Integer for int, Void for void).

  • The asynchronous return type is void, com.google.gwt.http.client.Request or com.google.gwt.http.client.RequestBuilder. A Request lets the caller ask isPending() and cancel(). A RequestBuilder means that the call is prepared but not sent until the caller invokes send(), so the caller can still change headers or the timeout.

  • A synchronous method may declare checked exceptions; they travel back as the exceptions of onFailure (see Exceptions: everything that can reach onFailure).

  • @RemoteServiceRelativePath is a runtime-retained annotation on the synchronous interface. Its value is resolved against GWT.getModuleBaseURL(). Without the annotation the service has no URL until ServiceDefTarget.setServiceEntryPoint(String) is called.

When the generator cannot find the Async interface, or a method is missing, the compile fails with messages that print the interface it expected. The two cases below were reproduced with probe interfaces named P7 and P8:

Checking the synchronous interface 'com.example.probe.client.Services.P7' against its asynchronous version 'com.example.probe.client.Services.P7Async'
   [ERROR] Missing asynchronous version of the synchronous method 'public abstract java.lang.String hello(java.lang.String name)'
   A valid definition for the asynchronous version of interface 'com.example.probe.client.Services.P7' would be:
package com.example.probe.client;

public interface P7Async {
	void hello(java.lang.String name, com.google.gwt.user.client.rpc.AsyncCallback<java.lang.String> arg2);
}

[ERROR] Could not find an asynchronous version for the service interface com.example.probe.client.Services.P8

The generator prints a ready-to-paste Async interface, which is the quickest way to write the first version. The messages also appear as Failed to resolve '...' via deferred binding on the line of the GWT.create call (see Deferred binding and generators for how generators report errors).

The data classes of the example

Book is a plain DTO. It implements java.io.Serializable, has a public no-argument constructor, and keeps server-only bookkeeping in a transient field.

bookshelf-shared: Book (accessors omitted)
public class Book implements Serializable {
  private long id;
  private String title;
  private String author;
  private int year;
  private int copies;
  private Isbn isbn;                       // immutable value type: needs a custom field serializer
  private Format format = Format.PAPERBACK; // enum: travels as its ordinal
  private Date added;                      // java.util.Date: built-in serializer
  private transient String internalNote;   // never sent in either direction

  public Book() {}

  public Book(long id, String title, String author, int year, int copies) { /* ... */ }
  // getters and setters
}

BookPage carries one page of the catalogue and the total number of rows, which is what a paged cell widget needs (see Paging a cell widget through the service).

public class BookPage implements Serializable {
  private ArrayList<Book> books = new ArrayList<>();
  private int total;

  public BookPage() {}

  public BookPage(List<Book> books, int total) {
    this.books = new ArrayList<>(books);
    this.total = total;
  }

  public List<Book> getBooks() { return books; }
  public int getTotal() { return total; }
}

Implementing the service

A service implementation is a servlet. It extends RemoteServiceServlet and implements the synchronous interface. GWT 2.13.1 ships the servlet in two flavours, because the servlet API changed its package name between Servlet 4 and Servlet 5.

Table 2. The two flavours of the server-side classes in 2.13.1
javax.servlet jakarta.servlet

Maven artifact

org.gwtproject:gwt-servlet

org.gwtproject:gwt-servlet-jakarta (since 2.11.0)

Base class

com.google.gwt.user.server.rpc.RemoteServiceServlet

com.google.gwt.user.server.rpc.jakarta.RemoteServiceServlet

XSRF servlets

...server.rpc.XsrfProtectedServiceServlet, XsrfTokenServiceServlet

...server.rpc.jakarta.XsrfProtectedServiceServlet, XsrfTokenServiceServlet

Other servlet classes

RPC, RPCServletUtils, AbstractRemoteServiceServlet in ...server.rpc

the same names in ...server.rpc.jakarta

Servlet API you provide

javax.servlet:javax.servlet-api (the GWT BOM manages 3.1.0)

jakarta.servlet:jakarta.servlet-api (6.0.0 in the Bookshelf)

Containers

those that implement Servlet 4.0 or older

those that implement Servlet 5.0 or later (Jetty 12 EE 10 is what the examples ran on)

The classes that do not touch the servlet API stay in the old package and are shared by both artifacts: RPCRequest, SerializationPolicy, SerializationPolicyLoader, SerializationPolicyProvider, ServerCustomFieldSerializer, UnexpectedException, and the annotations XsrfProtect and NoXsrfProtect. The client-side RPC types (RemoteService, AsyncCallback, the exceptions) are identical in both artifacts and live in com.google.gwt.user.client.rpc. Neither artifact depends on a servlet API; you supply it (as provided scope in a WAR). In the jars of 2.13.1 the javax jar holds RemoteServiceServlet only in com.google.gwt.user.server.rpc, and the jakarta jar holds it only in com.google.gwt.user.server.rpc.jakarta, so a servlet written for one flavour does not compile against the other jar.

bookshelf-server: pom.xml (dependencies only; the BOM is imported in the parent)
<dependencies>
  <dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>bookshelf-shared</artifactId>
    <version>${project.version}</version>
  </dependency>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-servlet-jakarta</artifactId>
  </dependency>
  <dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <scope>provided</scope>
  </dependency>
</dependencies>

The javax variant of the same class compiled against gwt-servlet-2.13.1.jar and javax.servlet-api 3.1.0; it was not run in a container:

import com.google.gwt.user.server.rpc.RemoteServiceServlet; // javax.servlet flavour: gwt-servlet, not gwt-servlet-jakarta

public class LegacyCatalogImpl extends RemoteServiceServlet implements NoPathService {
  @Override
  public String ping() {
    return getThreadLocalRequest().getContextPath(); // javax.servlet.http.HttpServletRequest
  }
}
bookshelf-server: the jakarta implementation (excerpt)
package com.example.bookshelf.server;

import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.jakarta.RemoteServiceServlet;
import jakarta.servlet.http.HttpServletRequest;
// other imports omitted

public class CatalogServiceImpl extends RemoteServiceServlet implements CatalogService {
  /** Called after the request was decoded and before the service method runs: a place for audit logging. */
  @Override
  protected void onAfterRequestDeserialized(RPCRequest rpcRequest) {
    log("rpc " + rpcRequest.getMethod().getName() + " with " + rpcRequest.getParameters().length + " parameter(s)");
  }

  @Override
  public BookPage getBooks(int start, int length) {
    if (start < 0 || length < 0) {
      throw new IllegalArgumentException("start and length must be >= 0");
    }
    return CatalogRepository.INSTANCE.page(start, Math.min(length, 100));
  }

  @Override
  public Book getBook(long id) throws BookNotFoundException {
    return CatalogRepository.INSTANCE.get(id);
  }

  @Override
  public Book saveBook(Book book) throws BookNotFoundException, AccessDeniedException {
    // Authorisation is a server-side decision: never trust a flag sent by the browser.
    if (getThreadLocalRequest().getUserPrincipal() == null) {
      throw new AccessDeniedException();
    }
    if (book.getTitle() == null || book.getTitle().isBlank() || book.getTitle().length() > 200) {
      throw new IllegalArgumentException("Invalid title");
    }
    return CatalogRepository.INSTANCE.save(book);
  }
  // lend, reviews, copiesByFormat and revision omitted
}

This excerpt is the plain, unprotected form used to introduce the servlet API. Because saveBook changes state, the deployed Bookshelf service extends the jakarta XsrfProtectedServiceServlet instead, and its onAfterRequestDeserialized override calls super.onAfterRequestDeserialized(rpcRequest) first so the token check still runs (see XSRF protection).

getBooks caps the page size at 100 (Math.min(length, 100)) because the length comes from the browser. Treat every argument as hostile input and validate it again on the server; the client-side checks of the user interface are a convenience only.

Mapping the servlet to a URL

The URL of a service is the module base URL plus the value of @RemoteServiceRelativePath. The module base URL is the directory that contains the module’s *.nocache.js file, with the context path in front: with rename-to="bookshelf" and a web application deployed at the root, GWT.getModuleBaseURL() is http://localhost:8932/bookshelf/ and the servlet is mapped to /bookshelf/catalog.

bookshelf-server: WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
  version="5.0">

  <servlet>
    <servlet-name>catalogServlet</servlet-name>
    <servlet-class>com.example.bookshelf.server.CatalogServiceImpl</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>catalogServlet</servlet-name>
    <url-pattern>/bookshelf/catalog</url-pattern>
  </servlet-mapping>
</web-app>

The annotation-based alternative needs no web.xml entry. It was run in Jetty 12 EE 10 and gave the same lab results as the web.xml mapping:

@WebServlet(name = "catalogServlet", urlPatterns = "/bookshelf/catalog")
public class CatalogServiceImpl extends RemoteServiceServlet implements CatalogService {
  // ...
}

Three details of the mapping decide whether calls arrive:

  • The part of the pattern before the last segment must equal the module base path. If the module is renamed with rename-to, the renamed value is used; either way the segment must be the directory inside the WAR where the module’s files are served.

  • The servlet only handles POST. A GET of the same URL answers 405 Method Not Allowed, because AbstractRemoteServiceServlet.doPost is the only handler (doPost is final).

  • The servlet must be in the same web application as the module’s files: it loads the policy file with ServletContext.getResourceAsStream, and the module path must start with the context path (see Serialisation policy files).

A servlet around a plain service class

RemoteServiceServlet has a second constructor, RemoteServiceServlet(Object delegate), for a service implementation that is not itself a servlet. That makes the logic a plain class that can be unit-tested, reused, or created by a dependency-injection container.

/** Plain class: it does not extend a servlet. */
public class PingLogic implements NoPathService {
  @Override
  public String ping() {
    return "pong";
  }
}

/** Wraps a service implementation that is not itself a servlet. */
@WebServlet(name = "pingServlet", urlPatterns = "/bookshelf/ping")
public class PingServlet extends RemoteServiceServlet {
  public PingServlet() {
    super(new PingLogic());
  }
}

NoPathService is a service interface without @RemoteServiceRelativePath, so the client sets the URL explicitly (see ServiceDefTarget and RpcRequestBuilder). The call returned //OK[1,["pong"],0,7] in the lab. In a Spring application the same constructor lets a @Service bean become the delegate of a servlet registered through a ServletRegistrationBean (the registration itself belongs to Server integration and deployment and is not repeated here).

What the servlet does for you, and the hooks it offers

The base classes implement a fixed pipeline. Each step has a protected method you can override; most applications override only the first two rows.

Table 3. The extension points of RemoteServiceServlet and AbstractRemoteServiceServlet
Method Use and default

onAfterRequestDeserialized(RPCRequest)

Called before the service method runs, with the decoded method and arguments. Default: nothing. A natural place for audit logging or per-method authorisation. In AbstractXsrfProtectedServiceServlet it validates the XSRF token, so an override there must call super.

onBeforeRequestDeserialized(String) and onAfterResponseSerialized(String)

See the raw payload. Default: nothing. Never log whole payloads in production: they contain user data.

getThreadLocalRequest() and getThreadLocalResponse()

The current HttpServletRequest and HttpServletResponse, for the session, the principal and headers. Valid only on the request thread.

getRequestModuleBasePath() and getPermutationStrongName()

The module path and the permutation hash from the X-GWT-Module-Base and X-GWT-Permutation headers.

checkPermutationStrongName()

Throws SecurityException ("Blocked request without GWT permutation header (XSRF attack?)") if the permutation header is missing. May be overridden to a no-op for non-GWT clients.

processCall(String) and processCall(RPCRequest)

Public so that they can be tested without HTTP. Override to route the call to a framework component.

doGetSerializationPolicy(request, moduleBaseURL, strongName)

Loads the policy; the default reads a servlet resource. Override to load policies from somewhere else.

shouldCompressResponse(request, response, payload)

Gzip is used when the client sends Accept-Encoding: gzip and the estimated size (twice the number of characters) exceeds 256 bytes.

doUnexpectedFailure(Throwable)

Called when anything escapes the pipeline. The default logs the exception to the servlet context and answers 500 with the text "The call failed on the server; see server log for details".

readContent(HttpServletRequest)

Reads the body and enforces Content-Type: text/x-gwt-rpc and charset=utf-8.

getCodeServerPolicyUrl(String) and loadPolicyFromCodeServer(String)

Super Dev Mode support (see Policies in Super Dev Mode).

The gzip rule was checked with curl --header 'Accept-Encoding: gzip' against the lab: the 464-byte getBooks answer came back with Content-Encoding: gzip and Content-Length: 290, and the 14-byte revision answer came back uncompressed. Every response also carries Content-Type: application/json;charset=utf-8 and Content-Disposition: attachment.

Calling the service from the client

The client side of a call always has the same three steps: create the proxy with GWT.create(), write the callback, make the call.

CatalogServiceAsync catalog = GWT.create(CatalogService.class); // the cast is implicit: the proxy implements the Async interface

catalog.getBooks(0, 10, new AsyncCallback<BookPage>() {
  @Override
  public void onSuccess(BookPage page) {
    display.showRows(0, page.getBooks(), page.getTotal());
  }

  @Override
  public void onFailure(Throwable caught) {
    display.showError("The catalogue could not be loaded.");
  }
});

The assignment from GWT.create(CatalogService.class) to the Async type compiles without a cast because GWT.create is generic; older code and the Developer Guide write (CatalogServiceAsync) GWT.create(...). The proxy can be created once and shared by every presenter; it holds no per-call state except the optional RPC token and the request builder. In the Bookshelf the ClientFactory creates it and the facade below wraps it.

Asynchronous calls

Every call returns before the server has answered. The code after the call runs first, then the browser returns to its event loop, and only when the response arrives is onSuccess or onFailure invoked. GWT offers no synchronous variant, because a synchronous XMLHttpRequest would freeze the single JavaScript thread of the page.

The consequences for the code you write are practical:

  • A field that the callback sets is not yet set on the line after the call. Move the follow-up work into the callback, or into a method the callback calls.

  • Two calls started one after the other may complete in either order. If the order matters, start the second call from the first one’s callback (see [chaining]).

  • If the user can trigger a call twice (a double click on a button), disable the control until the callback ran, or use Request.cancel() on the earlier call.

  • If the widget that owns the callback has been removed from the page when the answer arrives, the callback still runs. Check a flag or the attached state before touching the DOM.

The Developer Guide adds a claim that most browsers allow only two simultaneous connections. That figure comes from an older generation of browsers and HTTP/1.0 era limits; do not design around it, and measure on the target browsers instead.

The same-origin policy

A GWT-RPC call is an XMLHttpRequest like any other, so it obeys the browser’s same-origin policy. By default the entry point is the module base URL plus the relative path, which is on the same origin as the page, and nothing needs configuring. This is the reason the Bookshelf serves the host page, the compiled module and the servlet from one web application. The GWT FAQ states that the policy limits every form of server call, GWT-RPC included, and recommends either serving the application and its services from one server or putting a reverse proxy in front of both. Pointing setServiceEntryPoint at another origin is possible only if that server answers the CORS preflight (see ServiceDefTarget and RpcRequestBuilder and CORS), and the cross-site loading topic of the bootstrap (see Linkers and bootstrap) is a separate question: it concerns where the module’s JavaScript files are loaded from, not where the calls go.

Cancelling and preparing calls

An Async method that returns Request hands back an object that tracks the call. In the lab, getBook(2) was cancelled right after it was made:

Request r = catalog.getBook(2, callback);
log("pending before cancel: " + r.isPending());
r.cancel();
log("pending after cancel: " + r.isPending());
pending before cancel: true
pending after cancel: false
callback fired after cancel: false

cancel() does not undo anything on the server, which may already have executed the method; it only guarantees that the callback will not run. The returned Request can be null when the request could not even be started (doInvoke returns null after reporting the failure to the callback), so null-check it before use.

An Async method that returns RequestBuilder prepares the call without sending it. The lab logs the payload when the builder is created and the response only after send():

  >> 7|0|10|http://localhost:8932/bookshelf/|4F134919A31D9111F950CD21A4354BB3|com.example.bookshelf.shared.CatalogService|saveBook|...
  built, not yet sent: http://localhost:8932/bookshelf/catalog
  rb.send() called
  << 200 application/json;charset=utf-8 //EX[1,["com.example.bookshelf.shared.AccessDeniedException/35684112"],0,7]

If the arguments cannot be serialised, the generated code reports the error at once for void and Request methods and when send() is called for a RequestBuilder method (the generator returns a FailingRequestBuilder in that case).

A facade, callback chaining and plain Java presenters

Calling the proxy directly from every presenter spreads the plumbing. A small facade that wraps the proxy gives presenters one place to configure the request builder and the error handling, and lets them pass lambdas instead of anonymous AsyncCallback classes.

/** Facade: the only class that knows the RPC proxy. */
public class CatalogGateway {
  private final CatalogServiceAsync service = GWT.create(CatalogService.class);

  public CatalogGateway(int timeoutMillis) {
    ((ServiceDefTarget) service).setRpcRequestBuilder(new BookshelfRpcRequestBuilder(timeoutMillis));
  }

  public CatalogServiceAsync raw() {
    return service;
  }

  static <T> AsyncCallback<T> callback(Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
    return new AsyncCallback<T>() {
      @Override
      public void onSuccess(T result) {
        onSuccess.accept(result);
      }

      @Override
      public void onFailure(Throwable caught) {
        onFailure.accept(caught);
      }
    };
  }

  public void books(int start, int length, Consumer<BookPage> ok, Consumer<Throwable> failed) {
    service.getBooks(start, length, callback(ok, failed));
  }

  public void book(long id, Consumer<Book> ok, Consumer<Throwable> failed) {
    service.getBook(id, callback(ok, failed));
  }

  /** Callback chaining: the second call starts from the first call's result. */
  public void firstBookOfPage(int start, Consumer<Book> ok, Consumer<Throwable> failed) {
    books(start, 5, page -> {
      if (page.getBooks().isEmpty()) {
        failed.accept(new IllegalStateException("empty page"));
      } else {
        book(page.getBooks().get(0).getId(), ok, failed);
      }
    }, failed);
  }
}

AsyncCallback has two methods, so it is not a functional interface and cannot be written as a lambda directly; the static callback(...) adapter above is the small price.

The chained call (firstBookOfPage) ran in the lab and produced two requests, the second starting only after the first answered:

== 3 chaining firstBookOfPage(5)
  >> 7|0|5|http://localhost:8932/bookshelf/|4F13...|com.example.bookshelf.shared.CatalogService|getBooks|I|1|2|3|4|2|5|5|5|5|
  << 200 application/json;charset=utf-8 //OK[25,1999,21,20,7,"K",-9,1,19,...
  >> 7|0|5|http://localhost:8932/bookshelf/|4F13...|com.example.bookshelf.shared.CatalogService|getBook|J|1|2|3|4|1|5|G|
  << 200 application/json;charset=utf-8 //OK[2004,7,6,5,"G",2,4,1,3,"YvP5WgA",2,1,[...
OK 6 Working Effectively with Legacy Code

The == lines are the lab’s own step headings. The "command pattern" chain that older books build from small command objects is, in current Java, a chain of lambdas like the one above; for long sequences, name each step as a method so that the nesting stays shallow.

The presenter should not call GWT.create() itself. If the presenter receives the Async interface (or the facade) in its constructor, a plain JUnit test on the JVM can pass a hand-written fake that answers synchronously:

bookshelf-client: a presenter that never touches the DOM or GWT.create
public class CatalogPresenter {
  private final CatalogServiceAsync service;
  private final CatalogDisplay display;

  public CatalogPresenter(CatalogServiceAsync service, CatalogDisplay display) {
    this.service = service;
    this.display = display;
  }

  public void load(int start, int length) {
    service.getBooks(start, length, new AsyncCallback<BookPage>() {
      @Override
      public void onSuccess(BookPage page) {
        display.showRows(start, page.getBooks(), page.getTotal());
      }

      @Override
      public void onFailure(Throwable caught) {
        display.showError(messageFor(caught));
      }
    });
  }

  /** Never show getMessage() of an RPC failure: for a StatusCodeException it contains the whole response body. */
  static String messageFor(Throwable caught) {
    if (caught instanceof IncompatibleRemoteServiceException) {
      return "This page is out of date. Please reload it.";
    }
    if (caught instanceof StatusCodeException) {
      int status = ((StatusCodeException) caught).getStatusCode();
      return status == 0 ? "The server cannot be reached." : "The server answered with status " + status + ".";
    }
    return "The request failed. Please try again.";
  }
}

The four JUnit tests of this presenter ran in 0.016 s without compiling anything with GWT:

[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 s -- in com.example.bookshelf.client.CatalogPresenterTest

One of them feeds the presenter a StatusCodeException(500, "Server Error", "<html>stack trace of the server</html>") and asserts that the user sees only "The server answered with status 500.". The test exists because getMessage() of a StatusCodeException is the status, the status text and the whole response body; the Developer Guide’s own sample, which prints caught.getMessage() into a label, would show a container error page to the user. The Testing page covers GWTTestCase and mocking in more depth; the presenter and activity structure is in MVP, Activities & Places.

Paging a cell widget through the service

getBooks(start, length) returns a page plus the total, which is the shape that an AsyncDataProvider needs. The provider asks for a range when the display needs one, the presenter calls the service, and the callback pushes rows and the row count back.

provider = new AsyncDataProvider<Book>() {
  @Override
  protected void onRangeChanged(HasData<Book> display) {
    Range r = display.getVisibleRange();
    listener.onRange(r.getStart(), r.getLength()); // the presenter calls CatalogPresenter.load(start, length)
  }
};

// in CatalogDisplay.showRows(start, rows, total), called from the RPC callback:
provider.updateRowCount(total, true);
provider.updateRowData(start, rows);

In the scratch application this drove a DataGrid with ten rows per page over 25 books (1-10 of 25 in the pager). Add the display to the provider only after the presenter exists, because addDataDisplay fires onRangeChanged immediately. The cell widget side of this loop (providers, pagers, FieldUpdater) is on Cell widgets.

Polling and emulated server push

GWT-RPC is request and response: the server cannot call the browser. The classic workaround is polling with a Timer. The poller below asks for the catalogue’s revision number every few hundred milliseconds and reports a change:

public class RevisionPoller {
  private final CatalogServiceAsync service;
  private final IntConsumer onChange;
  private int last = -1;
  private boolean waiting;
  private final Timer timer = new Timer() {
    @Override
    public void run() {
      if (waiting) {
        return; // never stack requests when the server is slow
      }
      waiting = true;
      service.revision(new AsyncCallback<Integer>() {
        @Override
        public void onSuccess(Integer revision) {
          waiting = false;
          if (last >= 0 && revision != last) {
            onChange.accept(revision);
          }
          last = revision;
        }

        @Override
        public void onFailure(Throwable caught) {
          waiting = false; // keep polling; a real application would back off
        }
      });
    }
  };

  public RevisionPoller(CatalogServiceAsync service, IntConsumer onChange) {
    this.service = service;
    this.onChange = onChange;
  }

  public void start(int periodMillis) { timer.scheduleRepeating(periodMillis); }

  public void stop() { timer.cancel(); }
}

In the lab the poller ran every 150 ms, a second client call lend(3) changed the revision, and the poller noticed on its next tick:

== 8 poller sees lend(3)
  >> ...|revision|1|2|3|4|0|
  << 200 application/json;charset=utf-8 //OK[0,[],0,7]
  >> ...|lend|J|1|2|3|4|1|5|D|
  << 200 application/json;charset=utf-8 //OK[1,[],0,7]
lend(3) -> copies left 1
  >> ...|revision|1|2|3|4|0|
  << 200 application/json;charset=utf-8 //OK[1,[],0,7]
poller: revision changed to 1

Polling costs one request per tick per client, so keep the period long, make the polled call cheap, stop the timer when the view is detached (Timer.cancel()), and add a back-off after failures. Older books emulate push with a servlet that blocks in a Thread.sleep loop until data is ready. That design ties up a container thread per browser and is replaced today by WebSockets or Server-Sent Events, which a GWT client reaches through Elemental2 or JsInterop; those options are compared on the RequestBuilder, JSON and XML page and on the communication overview page linked at the top. The timer API is described on Delayed logic and Scheduler.

What travels on the wire

The payload is a text format that GWT designed for compactness, not for humans, and it is an implementation detail of the toolkit. Reading it once explains most error messages, and the lab captured real samples through a custom RpcRequestBuilder (see ServiceDefTarget and RpcRequestBuilder) and replayed them with curl.

The request

POST /bookshelf/catalog HTTP/1.1
Content-Type: text/x-gwt-rpc; charset=UTF-8
X-GWT-Permutation: 5F59F36FB51BE2BFE4844F629D22FF5C
X-GWT-Module-Base: http://localhost:8932/bookshelf/

7|0|5|http://localhost:8932/bookshelf/|4F134919A31D9111F950CD21A4354BB3|com.example.bookshelf.shared.CatalogService|getBooks|I|1|2|3|4|2|5|5|0|2|

The body is a list of tokens separated by |:

Table 4. The tokens of the request for getBooks(0, 2)
Tokens Meaning

7

Stream version. The server accepts versions 5 to 8; anything else is answered with an IncompatibleRemoteServiceException ("Expecting version between 5 and 8 from client, got 1.").

0

Flags. 1 means type names are elided (see Eliding type names), 2 means an RPC token is included (XSRF protection).

5

Number of strings in the table that follows.

the five strings

The module base URL, the strong name of the serialisation policy, the service interface, the method name, and the parameter type I (int).

1|2|3|4

Indexes into the string table: module base, policy, interface, method.

2

Number of parameters.

5|5

The type of each parameter, again as table indexes (both are I).

0|2

The argument values: start = 0, length = 2.

The headers matter as much as the body. Content-Type: text/x-gwt-rpc; charset=UTF-8 is checked by the servlet. X-GWT-Permutation is the strong name of the permutation (the compiled JavaScript file) that made the call, and the servlet refuses a request without it. The module base string in the body tells the servlet where the policy file is, and X-GWT-Module-Base carries the same address for getRequestModuleBasePath().

A call with an object argument is longer because the table also lists the types and the strings of the object. This is the saveBook request of the lab, truncated:

7|0|10|http://localhost:8932/bookshelf/|4F13...|com.example.bookshelf.shared.CatalogService|saveBook|com.example.bookshelf.shared.Book/2062416652|Andrew Hunt|com.example.bookshelf.shared.Format/576054756|com.example.bookshelf.shared.Isbn/1614986744|978-0-00-000001-0|Edited title|1|2|3|4|1|5|5|0|6|2|7|0|B|8|9|10|1999|

Book/2062416652 is the type name followed by the type signature: a hash of the class’s serialisable fields and their types. The server recomputes it from its own class and refuses the call when it differs (see the pitfalls); that is how an out-of-date browser tab is detected after a deployment that changed Book.

The response

HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Content-Disposition: attachment
Content-Length: 464

//OK[25,2001,12,11,7,"C",1,6,1,10,"YvP5WgA",4,3,1999,9,8,7,"B",0,6,0,5,"YvP5WgA",4,3,2,2,1,["com.example.bookshelf.shared.BookPage/3107328605","java.util.ArrayList/4159755760","com.example.bookshelf.shared.Book/2062416652","java.util.Date/3385151746","Andrew Hunt","com.example.bookshelf.shared.Format/576054756","com.example.bookshelf.shared.Isbn/1614986744","978-0-00-000001-0","The Pragmatic Programmer","Joshua Bloch","978-0-00-000002-0","Effective Java"],0,7]

//OK marks a returned value and //EX a thrown, declared exception. The rest is an array that the client reads from the end, because the server writes the values in reverse order: the version 7 and the flags 0 come last, before them the string table, and before the table the values. Reading backwards, the sample decodes as follows:

1          type 1 of the table: BookPage
2, 2       type 2 (ArrayList), then its size: 2 elements
3          type 3: the first Book
4, "YvP5WgA"   field added: type 4 (java.util.Date) and its time as a base-64 long
5          field author: string 5 = "Andrew Hunt"
0          field copies: 0
6, 0       field format: type 6 (Format), ordinal 0 = PAPERBACK
"B"        field id: the long 1, base-64 encoded
7, 8       field isbn: type 7 (Isbn) and string 8 = "978-0-00-000001-0"
9          field title: string 9 = "The Pragmatic Programmer"
1999       field year
...        the second Book follows, and the total (25) is the first number of the array

The fields of a class are written in the alphabetical order of their names (FIELD_COMPARATOR in SerializabilityUtil), superclasses first. Renaming a field therefore changes the order and the type signature. internalNote, which is transient, does not appear at all: the lab logs internalNote is null=true on the client.

A declared exception comes back as //EX:

//EX["Pn",1,["com.example.bookshelf.shared.BookNotFoundException/1919793663"],0,7]

Here the custom field serializer of BookNotFoundException wrote only the id ("Pn" is the long 999 in base 64) and the type; the message is rebuilt by the constructor on the client (see Custom field serializers).

Two protocol versions and what the client does with them

The server writes version 7 by default. The system property gwt.rpc.version (read by RPC.getRpcVersion() on every response) can raise it to 8. The client reader accepts versions 5 to 8; for versions before 8 it evaluates the payload with the JavaScript eval, for version 8 it parses it with JsonUtils.safeEval, which in 2.13.1 calls JSON.parse.

That difference is invisible until the page has a Content Security Policy without 'unsafe-eval'. With script-src 'self' 'unsafe-inline' and the default version 7, every call of the lab failed in the browser:

  << 200 application/json;charset=utf-8 //OK[25,2001,12,11,7,"C",1,6,1,...
onFailure com.google.gwt.core.client.JavaScriptException: (EvalError) : Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: script-src 'self' 'unsafe-inline'".

The same lab with the server started with -Dgwt.rpc.version=8 gave the same results as without a policy (every answer ends ,0,8] instead of ,0,7]), so version 8 is the way to keep 'unsafe-eval' out of the policy. The 2.12.0 release notes mention CSP workarounds for GWT-RPC payloads without saying what they are; the property is what the 2.13.1 sources and the lab show, but it is not described on the Developer Guide pages, so verify it on your own build. The 'unsafe-inline' in that policy was needed by the GWT bootstrap of the lab. The lab used the default obfuscated output, for which the xsiframe linker installs the permutation code inline through script.text, so a policy with only script-src 'self' stopped the module from starting with "Executing inline script violates the following Content Security Policy directive". The same policy lets a module start when it is compiled with -style PRETTY or linked with direct_install or sso; the Linkers and bootstrap page has the measured matrix of output styles and policies, and the SafeHtml and XSS page the policy as a whole.

Eliding type names

Inheriting com.google.gwt.user.RemoteServiceObfuscateTypeNames sets the configuration property gwt.elideTypeNamesFromRPC to true. The payload then replaces the service name and the type names with short ids, and the flags become 1:

  >> 7|1|5|http://localhost:8932/bookshelf/|032A2D0ED58CAB31526FBD4FD26CC63A|_|getBooks|I|1|2|3|4|2|5|5|0|2|
  << 200 application/json;charset=utf-8 //OK[25,2001,12,11,7,"C",1,6,1,10,"YvP5WgA",4,3,1999,9,8,7,"B",0,6,0,5,"YvP5WgA",4,3,2,2,1,["4","l","2","m","Andrew Hunt","5","6","978-0-00-000001-0","The Pragmatic Programmer","Joshua Bloch","978-0-00-000002-0","Effective Java"],1,7]

It makes the payload smaller and hides class names, and it requires the server to use the policy file (the servlet answers with an IncompatibleRemoteServiceException if the policy does not implement TypeNameObfuscator). The module file itself warns that the way elision is switched on is likely to change and that the configuration line should not simply be copied, so treat it as an optimisation to try, not as a security feature. The lab passed all its scenarios with it (the same six intended failures).

Replaying a call without a browser

Nothing in the format is secret, and GWT-RPC is not authentication. A script can replay a captured call as long as it sends the content type and a permutation header:

curl -s -i -X POST http://localhost:8932/bookshelf/catalog \
  -H 'Content-Type: text/x-gwt-rpc; charset=utf-8' \
  -H 'X-GWT-Permutation: 5F59F36FB51BE2BFE4844F629D22FF5C' \
  -H 'X-GWT-Module-Base: http://localhost:8932/bookshelf/' \
  --data '7|0|5|http://localhost:8932/bookshelf/|4F134919A31D9111F950CD21A4354BB3|com.example.bookshelf.shared.CatalogService|getBooks|I|1|2|3|4|2|5|5|0|2|'

The answer was the 200 and the //OK[...] array shown above. Without the X-GWT-Permutation header the answer was 500 with "The call failed on the server; see server log for details", and the container log held java.lang.SecurityException: Blocked request without GWT permutation header (XSRF attack?). With a different Content-Type it was also 500, and the log held jakarta.servlet.ServletException: Content-Type was 'application/json'. Expected 'text/x-gwt-rpc'. A GET of the same URL gave 405 Method Not Allowed.

The permutation check is a weak CSRF defence: a cross-site form cannot set a custom header, and a cross-origin fetch with one needs a CORS preflight that the server must allow. It is not authentication and it does not replace the token mechanism described in Security of the RPC channel.

Serialisable types

GWT’s idea of "serialisable" is its own, smaller than Java’s. It is decided at compile time from the types in the service interface, and enforced at run time by the policy file. A type may appear in a service method (as a parameter, a return type, a declared exception, or reachable from any of them through its fields) when at least one of the following holds, which is the list of the Developer Guide checked against the generator:

  • It is a primitive, String, Date (and the java.sql subclasses, which the policy lists), or a primitive wrapper.

  • It is an enum. Only the ordinal travels; the fields of the constants are not sent, and an enum cannot have a custom field serializer.

  • It is an array of serialisable types.

  • It is a user-defined class that is assignable to IsSerializable or java.io.Serializable (directly or through a superclass), whose non-final, non-transient instance fields are themselves serialisable, and that has a no-argument constructor of any visibility (or no constructor at all).

  • It has at least one serialisable subclass (the compiler then includes the subclasses it can see).

  • It has a custom field serializer (see Custom field serializers).

java.io.Serializable has been accepted since GWT 1.4, as the Developer Guide’s FAQ explains, on the condition that the policy file is deployed. IsSerializable, which older material requires, is still accepted and is equivalent.

The compiler classifies the problems it finds, and most of them are not fatal for the whole build but exclude the type. The table lists what each construct did when it was put in a probe service and compiled with 2.13.1.

Table 5. What the 2.13.1 compiler does with common DTO shapes
Shape Result Message and remedy

Class with a final field and only a constructor with arguments

Error

'com.example.probe.client.Types.NoDefaultCtor' has no instantiable subtypes. Add a no-argument constructor, or write a custom field serializer.

A Java record

Error

'com.example.probe.client.Types.BookRecord' has no instantiable subtypes. Records have final fields and no default constructor. They work with a custom field serializer (see Custom field serializers); the lab sent ArrayList<Review> of records that way.

A field of type Object

Error

'com.example.probe.client.Types.HasObject' has no instantiable subtypes. The generator source gives the reason: "In order to produce smaller client-side code, 'Object' is not allowed; please use a more specific type". A service method that used Object directly failed with 'java.lang.Object' has no instantiable subtypes.

A class that is not Serializable or IsSerializable

Error

'com.example.probe.client.Types.NotMarked' has no instantiable subtypes.

An interface with no serialisable implementation

Error

'com.example.probe.client.Types.Shape' has no instantiable subtypes.

A non-static nested class

Excluded

The generator source says it "is nested but not static; it will be excluded from the set of serializable types". Declare nested DTOs static.

A final instance field

Warning

[WARN] Field 'private final java.lang.String isbn' will not be serialized because it is final. The field is not sent, so it keeps its initial value. Make it non-final, or transient, or set the module property gwt.suppressNonStaticFinalFieldWarnings to true.

A raw List field

Warning, and larger output

[WARN] Checking all subtypes of Object which qualify for serialization: the compiler enumerates every serialisable class it can see (the probe pulled in its own other DTO and com.google.gwt.validation.client.impl.PathImpl). Always parameterise collections.

A field of type java.io.File or Thread

Error, before RPC

No source code is available for type java.io.File; did you forget to inherit a required module?. The type is not in the JRE emulation (see JRE emulation and shared code), so the whole compilation unit fails. Keep such types out of the shared source path.

The module property rpc.XserializeFinalFields (values false and true, default false) turns on experimental serialisation of final fields. The module file declares it experimental and warns that it can change or disappear in any release; it was not used in the lab.

Transient fields and what stays on the server

transient is honoured in both directions, and the Bookshelf relies on it for Book.internalNote. GWT also accepts any annotation whose simple name is GwtTransient, with com.google.gwt.user.client.rpc.GwtTransient as the shipped one. It is meant for types shared with other serialisation systems that must keep the field in their own serialisation, because it means the same as transient only for GWT.

public class BookSummary implements Serializable {
  private transient String internalNote;       // Java serialisation and GWT-RPC both skip it
  @GwtTransient private String cachedText;     // only GWT-RPC skips it (Java serialisation keeps it)
}

GwtTransient is documented in the Javadoc; only transient was compiled in the lab.

Collections, maps and polymorphism

Parameterise every collection and prefer concrete types in the service signature. The Developer Guide gives two reasons: smaller JavaScript, because the compiler only includes the serialisers of the types you declare, and protection against denial-of-service attacks, because the server verifies each deserialised value against the generic parameter of the method signature. ArrayList<Review> and HashMap<Format, Integer> crossed the wire in the lab; the policy file lists only ArrayList, HashMap and LinkedHashMap for them, plus Integer and its superclass Number.

== 13 copiesByFormat(): HashMap with enum keys
  >> 7|0|4|http://localhost:8932/bookshelf/|4F13...|com.example.bookshelf.shared.CatalogService|copiesByFormat|1|2|3|4|0|
  << 200 application/json;charset=utf-8 //OK[-3,1,2,11,3,2,2,12,3,0,2,3,1,["java.util.HashMap/1797211028","com.example.bookshelf.shared.Format/576054756","java.lang.Integer/3438268394"],0,7]
OK PAPERBACK -> 12
OK HARDCOVER -> 12
OK EBOOK -> 11

A method that returns List<Book> is legal, but the compiler then has to include a serialiser for every List implementation it can see. Returning ArrayList<Book> limits the code, and the wire format shows the same type either way (java.util.ArrayList/4159755760). If the server returns an instance of a subclass that the client never saw, the server refuses to serialise it (see Serialisation policy files).

A parameter whose runtime type is not the declared type is rejected by the server, because the type check of the deserialiser (SerializedTypeViolationException) compares the two. Sending an ArrayList where the signature says Book got this answer from the servlet:

//EX[2,1,["com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException/3936916533","Attempt to deserialize an object of type class java.util.ArrayList when an object of type class com.example.bookshelf.shared.Book is expected"],0,7]

The SerializedTypeViolationException is thrown while decoding and surfaces to the browser as an IncompatibleRemoteServiceException carrying its message.

Blocking a type

The configuration property rpc.blacklist holds regular expressions applied to type names, each optionally prefixed with - (the default, block) or + (allow again); the last matching entry wins.

<extend-configuration-property name="rpc.blacklist" value="com\.example\.probe\.client\.Types\.HasFinal" />

The probe with this line failed for the blocked type with the same message as a non-serialisable type: 'com.example.probe.client.Types.HasFinal' has no instantiable subtypes. Use the property to keep a type out of RPC when a broad base type (a Serializable interface, a base entity) would otherwise pull it in.

Serialisation policy files

At compile time the proxy generator writes one serialisation policy file per service interface into the module output directory. The file name is the policy’s strong name followed by .gwt.rpc. The strong name is the second string of every request, so the server finds the file with the module base path and that name.

bookshelf-client output: 4F134919A31D9111F950CD21A4354BB3.gwt.rpc (26 lines, the middle ones cut)
@FinalFields, false
com.example.bookshelf.shared.AccessDeniedException, true, true, false, false, com.example.bookshelf.shared.AccessDeniedException/35684112, 35684112
com.example.bookshelf.shared.Book, true, true, true, true, com.example.bookshelf.shared.Book/2062416652, 2062416652
com.example.bookshelf.shared.BookNotFoundException, true, true, false, false, com.example.bookshelf.shared.BookNotFoundException/1919793663, 1919793663
com.example.bookshelf.shared.BookPage, true, true, false, false, com.example.bookshelf.shared.BookPage/3107328605, 3107328605
com.example.bookshelf.shared.CatalogService, false, false, false, false, _, 4045853246
com.example.bookshelf.shared.Format, true, true, true, true, com.example.bookshelf.shared.Format/576054756, 576054756
com.example.bookshelf.shared.Isbn, true, true, true, true, com.example.bookshelf.shared.Isbn/1614986744, 1614986744
com.example.bookshelf.shared.Review, true, true, false, false, com.example.bookshelf.shared.Review/2693756894, 2693756894
...
com.google.gwt.user.client.rpc.XsrfToken, false, false, true, true, com.google.gwt.user.client.rpc.XsrfToken/4254043109, 4254043109
java.lang.Exception, true, false, true, false, java.lang.Exception/1920171873, 1920171873
...
java.util.ArrayList, true, true, false, false, java.util.ArrayList/4159755760, 4159755760
java.util.Date, true, true, true, true, java.util.Date/3385151746, 3385151746
java.util.HashMap, true, true, false, false, java.util.HashMap/1797211028, 1797211028
Table 6. The columns of a policy line (SerializationPolicyLoader)
Column Meaning

1

Binary class name

2 and 3

Fields may be serialised (sent to the browser), and the type may be instantiated when serialising

4 and 5

Fields may be deserialised (received from the browser), and the type may be instantiated when deserialising

6

Type id: the name with the signature hash (Book/2062416652), or _ for the service interface

7

The signature hash itself

The sample reads naturally: Book is true in all four columns because it is both sent and received; BookPage and the exceptions only go to the browser; XsrfToken only comes from it. A line @ClientFields,<class>,<fields...> appears for classes configured as "enhanced" (see JPA, JDO and DTOs: the 2.10.1 and 2.11 change), and @FinalFields, false records the rpc.XserializeFinalFields setting.

The compiler also writes rpcPolicyManifest/manifest.txt with one line per service, into the deploy directory of the module (bookshelf-client/target/gwt/deploy/bookshelf/rpcPolicyManifest/manifest.txt in the Maven build):

# Module bookshelf
# RPC service class, partial path of RPC policy file
com.example.bookshelf.shared.NoPathService, D310D7D4714B06BEF59A04DA01C61505.gwt.rpc
com.example.bookshelf.shared.CatalogService, 4F134919A31D9111F950CD21A4354BB3.gwt.rpc

Build scripts that copy the policies by hand, and servers that do not share a web application with the client, use the manifest to find them.

How the servlet finds the policy

RemoteServiceServlet.getSerializationPolicy runs once per module base URL and strong name and caches the result:

  1. It strips the context path from the module base path and opens <module path><strong name>.gwt.rpc with ServletContext.getResourceAsStream. The module path must be inside the servlet’s own web application.

  2. If the file is not found, and the system property gwt.codeserver.port is set, it downloads the policy from the Super Dev Mode code server (see Policies in Super Dev Mode).

  3. If nothing is found, it logs a warning and falls back to the legacy 1.3.3 policy, which accepts only IsSerializable types.

The fallback is the source of the most confusing failure of GWT-RPC. It was reproduced by deleting the *.gwt.rpc files from the deployed module directory of a running lab server before the first call:

[INFO] catalogServlet: ERROR: The serialization policy file '/bookshelf/4F134919A31D9111F950CD21A4354BB3.gwt.rpc' was not found; did you forget to include it in this deployment?
[INFO] catalogServlet: WARNING: Failed to get the SerializationPolicy '4F13...' for module 'http://localhost:8932/bookshelf/'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.
  [WARNING] Exception while dispatching incoming RPC call
com.google.gwt.user.client.rpc.SerializationException: Type 'com.example.bookshelf.shared.BookPage' was not assignable to 'com.google.gwt.user.client.rpc.IsSerializable' and did not have a custom field serializer.For security purposes, this type will not be serialized.: instance = com.example.bookshelf.shared.BookPage@79fa53da

The browser got 500 Server Error The call failed on the server; see server log for details. describeRequest(), which returns a String, kept working, and the declared exceptions kept working, because String and the exceptions with custom serialisers do not need the policy. The cause is always the deployment: the files were not copied, the servlet is in another web application, a proxy rewrites the path, or the client was compiled again and the server still serves the old module directory.

A type that is in the policy of the module but is not the declared or a known subtype is refused differently. getBook(13) in the lab returned a server-only subclass LegacyBook extends Book, which the compiler never saw:

  [WARNING] Exception while dispatching incoming RPC call
com.google.gwt.user.client.rpc.SerializationException: Type 'com.example.bookshelf.server.LegacyBook' was not included in the set of types which can be serialized by this SerializationPolicy or its Class object could not be loaded. For security purposes, this type will not be serialized.: instance = com.example.bookshelf.server.LegacyBook@72c810bb

Return a new Book(...) copy, never the subclass (the same rule makes JPA entities and proxies of Hibernate unusable as results, see JPA, JDO and DTOs: the 2.10.1 and 2.11 change).

Policies in Super Dev Mode

In development the Java server and the code server are separate processes. Two mechanisms exist, and the lab ran both.

The first one copies the policies. When the Maven plugin is configured with a launcherDir (the modular-webapp archetype does this), the code server copies each policy file next to the bootstrap script in that directory, and the Jetty profile env-dev adds that directory to its resource bases (see the archetype’s bookshelf-server/pom.xml). A comment in LauncherDir.java explains the purpose: the copies let the RemoteServiceServlet subclass find the policies in the module directory. In the lab the first page load compiled the module on demand and the calls worked.

The second one downloads them. Start the servlet container with -Dgwt.codeserver.port=9932 (the code server’s port) and RemoteServiceServlet.getCodeServerPolicyUrl returns http://localhost:<port>/policies/<strong name>.gwt.rpc. With the copies deleted, the first call logged:

[INFO] catalogServlet: ERROR: The serialization policy file '/bookshelf/4F134919A31D9111F950CD21A4354BB3.gwt.rpc' was not found; did you forget to include it in this deployment?
[INFO] catalogServlet: Downloaded serialization policy from http://localhost:9932/policies/4F134919A31D9111F950CD21A4354BB3.gwt.rpc

and the lab then passed. The Javadoc of getCodeServerPolicyUrl warns what this is: a policy fetched from a server that is not trusted "could be used as a way to disable security checks", so use the property on a developer machine only, never in a deployed container. The default loader is a plain java.net.URL client with no authentication and 5-second connect and read timeouts.

See Development mode and debugging for the code server itself.

Custom field serializers

A custom field serializer (CFS) replaces the automatic, reflection-based serialisation of one class with code you write. Use one when the class has no no-argument constructor (an immutable value type), when a more compact wire representation is worth the code, for a Java record, for a type from a library that you cannot change, and, as the next sections show, for your own exception classes on Java 17 and later. Most of the JRE collection classes in GWT have one, as do Date, the wrapper types and StackTraceElement.

The rules, from the Developer Guide and checked against CustomFieldSerializerValidator:

  • The class is named after the class it serialises with the suffix _CustomFieldSerializer and sits in the same package (Isbn_CustomFieldSerializer next to Isbn). Types of the JRE can be served from com.google.gwt.user.client.rpc.core.<package>.

  • It must be visible to the GWT compiler (in the source path), because the generated client code calls it. A serialiser in a server-only package is never used by the browser.

  • The compiler requires public static methods: void serialize(SerializationStreamWriter, T), void deserialize(SerializationStreamReader, T), and T instantiate(SerializationStreamReader) when the type is not default-instantiable.

  • The server prefers an instance: a CFS that also extends CustomFieldSerializer<T> and overrides serializeInstance, deserializeInstance, and (for custom instantiation) hasCustomInstantiateInstance returning true plus instantiateInstance. Every JRE serialiser of GWT does both and delegates the instance methods to the static ones.

  • The compiler rejects enums with a serializer: "Enumerated types cannot have custom field serializers."

The Developer Guide only says that a CFS should extend CustomFieldSerializer. A class that does that but has no static methods does not compile. The first version of the Bookshelf’s Isbn_CustomFieldSerializer had only the instance methods, and the compiler answered:

[ERROR] Custom Field Serializer 'com.example.bookshelf.shared.Isbn_CustomFieldSerializer' does not define a deserialize method: 'public static void deserialize(com.google.gwt.user.client.rpc.SerializationStreamReader reader,com.example.bookshelf.shared.Isbn instance)' (reached via com.example.bookshelf.shared.Book)
[ERROR] Custom Field Serializer 'com.example.bookshelf.shared.Isbn_CustomFieldSerializer' does not define a serialize method: 'public static void serialize(com.google.gwt.user.client.rpc.SerializationStreamWriter writer,com.example.bookshelf.shared.Isbn instance)' (reached via com.example.bookshelf.shared.Book)
[ERROR] Custom Field Serializer 'com.example.bookshelf.shared.Isbn_CustomFieldSerializer' does not define an instantiate method: 'public static com.example.bookshelf.shared.Isbn instantiate(com.google.gwt.user.client.rpc.SerializationStreamReader reader)'; but 'com.example.bookshelf.shared.Isbn' is not default instantiable (reached via com.example.bookshelf.shared.Book)
[ERROR] com.example.bookshelf.shared.Isbn has no available instantiable subtypes. (reached via com.example.bookshelf.shared.Book)
   [ERROR]    subtype Custom Field Serializer 'com.example.bookshelf.shared.Isbn_CustomFieldSerializer' does not define a deserialize method: ...
[ERROR] Errors in 'com/example/bookshelf/client/CatalogGateway.java'
   [ERROR] Line 14: Failed to resolve 'com.example.bookshelf.shared.CatalogService' via deferred binding

The corrected class, in the shared module:

bookshelf-shared: an immutable value type and its serializer
public final class Isbn {
  private final String value;

  private Isbn(String value) { this.value = value; }

  public static Isbn of(String value) {
    if (value == null || value.length() > 20 || !value.matches("[0-9X-]+")) {
      throw new IllegalArgumentException("Invalid ISBN");
    }
    return new Isbn(value);
  }

  public String value() { return value; }
  // equals, hashCode, toString omitted
}

public final class Isbn_CustomFieldSerializer extends CustomFieldSerializer<Isbn> {
  public static void deserialize(SerializationStreamReader reader, Isbn instance) {
    // Everything was read by instantiate(): there is no state left to restore.
  }

  public static Isbn instantiate(SerializationStreamReader reader) throws SerializationException {
    return Isbn.of(reader.readString());
  }

  public static void serialize(SerializationStreamWriter writer, Isbn instance) throws SerializationException {
    writer.writeString(instance.value());
  }

  @Override
  public void deserializeInstance(SerializationStreamReader reader, Isbn instance) {
    deserialize(reader, instance);
  }

  @Override
  public boolean hasCustomInstantiateInstance() {
    return true;
  }

  @Override
  public Isbn instantiateInstance(SerializationStreamReader reader) throws SerializationException {
    return instantiate(reader);
  }

  @Override
  public void serializeInstance(SerializationStreamWriter writer, Isbn instance) throws SerializationException {
    serialize(writer, instance);
  }
}

instantiate runs Isbn.of(...), so the validation of the value type also runs on the server for every ISBN that arrives from a browser. That is a useful property: the constructor of a value type is the natural place to reject malformed input, and a CFS makes the framework use it. In the wire sample the ISBN is the pair 7, 8 (the type Isbn/1614986744 and the string 978-0-00-000001-0).

Records

A record needs the same treatment, because its components are final fields and it has no default constructor. The compiler accepts records in shared source (the Bookshelf compiles at -sourceLevel 17), but RPC fails without a serializer (see the probe table above).

public record Review(long bookId, int stars, String comment) implements Serializable {}

public final class Review_CustomFieldSerializer extends CustomFieldSerializer<Review> {
  public static void deserialize(SerializationStreamReader reader, Review instance) { }

  public static Review instantiate(SerializationStreamReader reader) throws SerializationException {
    return new Review(reader.readLong(), reader.readInt(), reader.readString());
  }

  public static void serialize(SerializationStreamWriter writer, Review instance) throws SerializationException {
    writer.writeLong(instance.bookId());
    writer.writeInt(instance.stars());
    writer.writeString(instance.comment());
  }
  // the four instance methods delegate to the static ones, as in Isbn_CustomFieldSerializer
}

The lab called reviews(2), which returns ArrayList<Review>:

== 12 reviews(2): a record via a custom field serializer, ArrayList<Review>
  >> 7|0|5|http://localhost:8932/bookshelf/|4F13...|com.example.bookshelf.shared.CatalogService|reviews|J|1|2|3|4|1|5|C|
  << 200 application/json;charset=utf-8 //OK[4,3,"C",2,3,5,"C",2,2,1,["java.util.ArrayList/4159755760","com.example.bookshelf.shared.Review/2693756894","Still the best introduction","Dated in places"],0,7]
OK review 2 5 Still the best introduction
OK review 2 3 Dated in places

Notice that the type of Review is listed in the string table once, and each record is the triple the serializer wrote (bookId as a base-64 long, stars, the comment string). The policy file has Review ... true, true, false, false: the server may send reviews but not accept them.

A type-checked server-side serializer

A server must deserialise whatever bytes a browser sends, before the service method can reject them. That makes deserialisers a target for denial-of-service payloads (very large or deeply nested values). Since GWT 2.4 a CFS can have a server-only twin, <Name>_ServerCustomFieldSerializer extends ServerCustomFieldSerializer<T>, whose methods receive the expected generic parameter types and are supposed to read nested objects with the type-checking readObject overloads. The built-in HashMap_ServerCustomFieldSerializer is the reference.

The lookup of the name replaces the first occurrence of the text client in the fully-qualified class name with server. com.example.bookshelf.shared.Isbn has no client in it, so its twin must be named com.example.bookshelf.shared.Isbn_ServerCustomFieldSerializer, in the same package as the shared class. That is possible without GWT seeing it: put the class in the server module (a split package on the class path), where the GWT source path never reaches.

bookshelf-server: src/main/java/com/example/bookshelf/shared/Isbn_ServerCustomFieldSerializer.java (excerpt)
package com.example.bookshelf.shared; // the shared package, but in the server module

public final class Isbn_ServerCustomFieldSerializer extends ServerCustomFieldSerializer<Isbn> {
  @Override
  public boolean hasCustomInstantiateInstance() {
    return true;
  }

  @Override
  public Isbn instantiateInstance(ServerSerializationStreamReader reader, Type[] expectedParameterTypes,
      DequeMap<TypeVariable<?>, Type> resolvedTypes) throws SerializationException {
    return Isbn.of(reader.readString());
  }

  @Override
  public void deserializeInstance(ServerSerializationStreamReader reader, Isbn instance,
      Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes) { }

  @Override
  public void deserializeInstance(SerializationStreamReader reader, Isbn instance) { }

  @Override
  public void serializeInstance(SerializationStreamWriter writer, Isbn instance) throws SerializationException {
    writer.writeString(instance.value());
  }
}

With a temporary System.out.println in instantiateInstance, the container log showed the twin used when the saveBook request (which carries a Book with an ISBN) arrived. Isbn holds only a string, so there is nothing to type-check here; the example shows where the class goes. A twin that reads collections or other objects must pass the expected type to readObject, as the built-in ones do; getting that wrong is what leaves the server exposed. The unit that does the type check for the normal path is ServerSerializationStreamReader, which throws SerializedTypeViolationException when a value is not of the expected type (see the table of exceptions).

Exceptions: everything that can reach onFailure

Every failure of a call, wherever it happens, arrives in AsyncCallback.onFailure(Throwable), except one that is thrown synchronously by the proxy (see the table). The flow chart sorts the cases by where they occur.

flowchart TD A["Call made on the Async proxy"] --> B{"Service entry point known?"} B -- "no" --> X1["NoServiceEntryPointSpecifiedException thrown from the call itself"] B -- "yes" --> C{"Arguments serialisable and request sent?"} C -- "no" --> X2["InvocationException: Unable to initiate the asynchronous service invocation"] C -- "yes" --> D{"Timeout or network error?"} D -- "timeout" --> X3["RequestTimeoutException"] D -- "status 0, blocked or aborted" --> X4["StatusCodeException with status 0"] D -- "answered" --> E{"HTTP status"} E -- "not 200" --> X5["StatusCodeException with the status, the status text and the body"] E -- "200" --> F{"Body starts with OK or EX?"} F -- "no (login page, proxy page)" --> X6["InvocationException with the body and the method name"] F -- "yes" --> G{"Which?"} G -- "OK" --> H["onSuccess with the result"] G -- "EX" --> I["onFailure with the declared exception, IncompatibleRemoteServiceException or RpcTokenException"]

The table lists every exception that appeared in the lab or in the sources, where it is created, and what to do.

Table 7. The exceptions of GWT-RPC
Exception Where When What the application should do

A declared checked exception (BookNotFoundException)

server, then client

The service method throws it and its throws clause lists it. It must be serialisable. Arrives as //EX[...] and onFailure receives an instance of the same class.

Handle it as part of the API (show "not found"). Give it a custom field serializer on Java 17 and later (see Java 17 and later: exceptions that cannot be serialised).

An undeclared exception (IllegalArgumentException, NullPointerException, a database error)

server

The service method throws it. The server wraps it in com.google.gwt.user.server.rpc.UnexpectedException, logs "Exception while dispatching incoming RPC call" with the whole cause, and answers 500 with plain text. The browser gets a StatusCodeException.

Fix the cause or declare and handle it. Never rely on the message reaching the browser; this is deliberate, because it hides internals.

StatusCodeException (extends InvocationException)

client

The HTTP status is not 200. Methods: getStatusCode(), getStatusText(), getEncodedResponse(). Status 0 means a network error, a cross-origin request that the server did not allow, or an aborted request (for instance when the user leaves the page).

Branch on the status code. Show your own text: getMessage() is the status, the status text and the whole response body.

InvocationException

client

Three cases: a 200 answer that is neither //OK nor //EX (the message is the body plus " from <proxy>.<method>"), "No response payload from <method>", and "Unable to initiate the asynchronous service invocation (<method>) — check the network connection" when RequestBuilder.send() threw.

An HTML body means a proxy, a gateway or an SSO page answered instead of the servlet; ask the user to sign in again.

IncompatibleRemoteServiceException

server, then client

The client and the server disagree: unknown method, wrong parameter count, a changed type signature, an unknown RPC version, a service interface the servlet does not implement, or a response that the client cannot deserialise. Arrives as //EX; getCause() is always null on the client.

The page is out of date. Get the application into a state where a reload is safe (save what you can) and reload. See the messages in the pitfalls table.

SerializationException (checked, com.google.gwt.user.client.rpc)

server (and custom serialisers)

The server cannot serialise a result (type not in the policy) or a custom serializer fails. It is logged and the browser sees a 500.

Fix the policy deployment or the type (see Serialisation policy files). Throw it from your own CFS to signal bad data.

SerializedTypeViolationException (extends SerializationException)

server

A value in the request is not of the type the method signature expects: a tampered request. The servlet turns it into an IncompatibleRemoteServiceException with the message "Attempt to deserialize an object of type …​".

Log it as a possible attack. Nothing to handle in the client.

RpcTokenException (unchecked)

server, then client

The XSRF token is missing or wrong. If an RpcTokenExceptionHandler is set on the proxy it receives the exception instead of onFailure.

Fetch a new token and retry once (see Security of the RPC channel).

ServiceDefTarget.NoServiceEntryPointSpecifiedException (extends InvocationException)

client

Thrown by the Async call itself, before anything is sent: the service has no @RemoteServiceRelativePath and no setServiceEntryPoint call. The lab caught it with a try/catch around the call; onFailure was not invoked.

A programming error. Fix the mapping.

com.google.gwt.http.client.RequestTimeoutException

client

A timeout set on the request builder expired. onFailure receives it directly, not wrapped.

Retry for a read call, or tell the user that the result is uncertain for a write.

UnexpectedException (unchecked, server)

server

RPC.encodeResponseForFailure found an exception that the method did not declare. Visible only in the container log.

Look at its cause in the log.

SecurityException (server)

server

"Blocked request without GWT permutation header (XSRF attack?)", or "Blocked attempt to access inaccessible method …​ this is either misconfiguration or a hack attempt". Logged; the browser gets a 500.

Check that the caller is a GWT client, or disable checkPermutationStrongName() deliberately for other clients.

jakarta.servlet.ServletException (server)

server

"Content-Type was 'application/json'. Expected 'text/x-gwt-rpc'." when the body is read.

The request did not come from a GWT proxy.

SerializableException

client and server

Deprecated since GWT 1.5; an Exception is serialisable on its own.

Extend Exception.

Design the exceptions that cross the wire like an API. Keep them small, built from serialisable fields only, and without messages that reveal paths, SQL or stack traces. In the captured //EX payloads of the Bookshelf the server’s stack trace never appeared; the payload held the type and the message (or just the id, with the custom serializer).

A handler that follows the table is in the presenter above (messageFor), and it is covered by JUnit tests that feed it each exception.

Java 17 and later: exceptions that cannot be serialised

GWT-RPC serialises an exception through reflection, like any other class, and every exception inherits the private fields of java.lang.Throwable. Since JDK 17 the module system denies reflective access to the private fields of java.base classes from unnamed modules unless the JVM is started with --add-opens. The consequence is a failure that is easy to hit and hard to diagnose. This is open upstream as gwtproject/gwt issue 9793 ("Serialization fails with Unable to make field private java.lang.String java.lang.Throwable.detailMessage accessible", opened on 2023-01-09 against GWT 2.10.0 on Java 17, still open when this page was checked on 2026-10-09, with three comments and no fix).

Reproducing it

The Bookshelf lab was started on JDK 21 with no extra JVM options, and the first versions of its exceptions had no custom serializer. The call getBook(999) should answer with the declared BookNotFoundException:

== 4 getBook(999): declared checked exception
  >> 7|0|5|http://localhost:8932/bookshelf/|D981D547C35841434CBFC68CFB9C1AEA|com.example.bookshelf.shared.CatalogService|getBook|J|1|2|3|4|1|5|Pn|
  << 500 text/plain The call failed on the server; see server log for details
onFailure com.google.gwt.user.client.rpc.StatusCodeException: 500 Server Error The call failed on the server; see server log for details [status=500]

The browser never sees the exception. The container log has the cause, and the top of the trace points into the serialiser:

  [WARNING] Exception while dispatching incoming RPC call
java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.Throwable.detailMessage accessible: module java.base does not "opens java.lang" to unnamed module @35d7b746
    at java.lang.reflect.AccessibleObject.throwInaccessibleObjectException (AccessibleObject.java:391)
    at java.lang.reflect.Field.setAccessible (Field.java:177)
    at com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter.serializeClass (ServerSerializationStreamWriter.java:784)
    ...
    at com.google.gwt.user.server.rpc.jakarta.RPC.encodeResponse (RPC.java:645)
    at com.google.gwt.user.server.rpc.jakarta.RPC.encodeResponseForFailure (RPC.java:420)
    at com.google.gwt.user.server.rpc.jakarta.RPC.invokeAndEncodeResponse (RPC.java:606)
    at com.google.gwt.user.server.rpc.jakarta.RemoteServiceServlet.processCall (RemoteServiceServlet.java:350)

The identifier after @ changes from run to run. Every exception that has to be serialised fails in the same way: a declared exception thrown by the service (BookNotFoundException, AccessDeniedException), and the exceptions of GWT’s own protocol. The latter are the worst, because the mechanisms that tell the client what is wrong stop working:

  • IncompatibleRemoteServiceException (the "reload the page" signal after a deployment) is never delivered. A curl request for an unknown method got 500 ... The call failed on the server, where the same request on a JVM with the option got the //EX[...IncompatibleRemoteServiceException...] shown in the pitfalls table. The container log shows the incompatible-exception warning followed by the same InaccessibleObjectException.

  • RpcTokenException (a failed XSRF check) hits the same problem; Group 1 of the scratch application saw it the first time a call without a token was made.

  • The StockWatcher tutorial of the Developer Guide teaches DelistedException extends Exception implements Serializable with a field, which has the same shape as BookNotFoundException, so following the tutorial on a current JDK ends in the generic 500.

Three ways out

1. Open the package for the servlet container’s JVM. The option that GWT’s own exceptions need:

# Maven Jetty plugin: the container runs inside the Maven JVM
MAVEN_OPTS='--add-opens java.base/java.lang=ALL-UNNAMED' mvn -B -pl bookshelf-server -am verify

# any container started by a script or an executable JAR
java --add-opens java.base/java.lang=ALL-UNNAMED -jar bookshelf.jar

With the option, the same getBook(999) call returned the exception, with the type, the id and the message written by reflection:

  << 200 application/json;charset=utf-8 //EX[2,"Pn",1,["com.example.bookshelf.shared.BookNotFoundException/2446222147","No book with id 999"],0,7]
onFailure com.example.bookshelf.shared.BookNotFoundException: No book with id 999
  id=999

The option has to reach the JVM that hosts the servlet container: MAVEN_OPTS for the Maven Jetty plugin was the form that was run. Tomcat reads CATALINA_OPTS, and an application server has its own JVM option file; those forms were not run. Note that the module is java.base, package java.lang, target ALL-UNNAMED: the exceptions of a library (java.util, for example) would need their own package.

2. Give each of your own exceptions a custom field serializer. A CFS replaces the reflective walk over the class and its superclasses, so the private fields of Throwable are never touched. The serializer of BookNotFoundException writes the id and the constructor rebuilds the message:

public final class BookNotFoundException_CustomFieldSerializer
    extends CustomFieldSerializer<BookNotFoundException> {
  public static void deserialize(SerializationStreamReader reader, BookNotFoundException instance) {
    // Nothing left to read: instantiate() consumed the id.
  }

  public static BookNotFoundException instantiate(SerializationStreamReader reader) throws SerializationException {
    return new BookNotFoundException(reader.readLong());
  }

  public static void serialize(SerializationStreamWriter writer, BookNotFoundException instance)
      throws SerializationException {
    writer.writeLong(instance.getId());
  }
  // deserializeInstance, hasCustomInstantiateInstance, instantiateInstance and serializeInstance
  // delegate to the three static methods, as in Isbn_CustomFieldSerializer
}

On JDK 21 with no --add-opens, the lab then returned the declared exceptions, with a smaller payload (no message, no cause):

  << 200 application/json;charset=utf-8 //EX["Pn",1,["com.example.bookshelf.shared.BookNotFoundException/1919793663"],0,7]
onFailure com.example.bookshelf.shared.BookNotFoundException: No book with id 999
  id=999
  << 200 application/json;charset=utf-8 //EX[1,["com.example.bookshelf.shared.AccessDeniedException/35684112"],0,7]
onFailure com.example.bookshelf.shared.AccessDeniedException: Access denied (declared)

This is the better answer for your exceptions: it needs no JVM option, it never sends a stack trace, and it sends only what you choose. It cannot help with IncompatibleRemoteServiceException and RpcTokenException, which are GWT classes, so the first option is still needed for them (or accept that they appear as 500).

3. Avoid exceptions for expected outcomes. Return a result object with a status (Found, NotFound, Denied) for outcomes that are part of normal operation, and keep exceptions for the unexpected. That also makes the contract visible in the type of the result, and it is independent of the JVM options.

Both 1 and 2 are workarounds for a missing feature of the toolkit, so re-test them when a newer GWT is adopted, and keep the JVM option in the same build file as the server’s start-up so that it is not forgotten.

ServiceDefTarget and RpcRequestBuilder

The generated proxy implements ServiceDefTarget, the interface that configures where and how it calls. Cast the Async proxy to it:

Table 8. The methods of ServiceDefTarget
Method Use

setServiceEntryPoint(String address)

Sets the URL. @RemoteServiceRelativePath does exactly this, with GWT.getModuleBaseURL() in front, when the proxy is created. Call it to point a service somewhere else (another path, another origin).

getServiceEntryPoint()

The current URL, or null if neither the annotation nor the setter supplied one.

setRpcRequestBuilder(RpcRequestBuilder builder)

Installs a builder that configures every HTTP request of this proxy. null restores the default.

getSerializationPolicyName()

The strong name of the policy that the proxy was compiled against (the second string of each request).

setServiceEntryPoint is the old way, and the only way for a service whose interface cannot carry the annotation. The lab used it for a delegate servlet (ping) and to point a proxy at a wrong path on purpose. When neither the annotation nor the setter was used, the first call threw before sending anything:

== 10 service without @RemoteServiceRelativePath
  entry point is null: true
thrown com.google.gwt.user.client.rpc.ServiceDefTarget$NoServiceEntryPointSpecifiedException: Service implementation URL not specified

RpcRequestBuilder

An RpcRequestBuilder creates and configures the RequestBuilder that carries one RPC. Its public methods are final and run in a fixed order, create, setContentType, setRequestData, setCallback, setRequestId, finish, and each delegates to a protected doXyz method that you can override:

Table 9. The override points of RpcRequestBuilder
Method Default behaviour

doCreate(String serviceEntryPoint)

A POST RequestBuilder for the entry point.

doFinish(RequestBuilder)

Sets the headers X-GWT-Permutation (GWT.getPermutationStrongName()) and X-GWT-Module-Base (GWT.getModuleBaseURL()). Call super.doFinish(rb) or the servlet rejects the request.

doSetContentType(RequestBuilder, String)

Sets the Content-Type header (text/x-gwt-rpc; charset=utf-8).

doSetRequestData(RequestBuilder, String)

Passes the payload to the RequestBuilder.

doSetCallback(RequestBuilder, RequestCallback)

Passes the callback. Wrap it to observe responses.

doSetRequestId(RequestBuilder, int)

Nothing; it receives the request number.

The constants CONTENT_TYPE_HEADER, MODULE_BASE_HEADER (X-GWT-Module-Base) and STRONG_NAME_HEADER (X-GWT-Permutation) are public. The Bookshelf builder adds an application header and a timeout to every call, and logs the wire format (the log lines are lab-only; the header and the timeout are the real purpose):

bookshelf-client: BookshelfRpcRequestBuilder
public class BookshelfRpcRequestBuilder extends RpcRequestBuilder {
  private final int timeoutMillis;

  public BookshelfRpcRequestBuilder(int timeoutMillis) {
    this.timeoutMillis = timeoutMillis;
  }

  @Override
  protected void doFinish(RequestBuilder rb) {
    super.doFinish(rb); // keeps X-GWT-Permutation and X-GWT-Module-Base
    rb.setHeader("X-Bookshelf-Client", "lab");
    rb.setTimeoutMillis(timeoutMillis);
  }

  @Override
  protected void doSetRequestData(RequestBuilder rb, String data) {
    RpcLab.log("  >> " + data);        // lab only: shows the request payload
    super.doSetRequestData(rb, data);
  }

  @Override
  protected void doSetCallback(RequestBuilder rb, final RequestCallback callback) {
    super.doSetCallback(rb, new RequestCallback() {
      @Override
      public void onResponseReceived(Request request, Response response) {
        RpcLab.log("  << " + response.getStatusCode() + " " + response.getHeader("Content-Type") + " "
            + response.getText());      // lab only: shows the response
        callback.onResponseReceived(request, response);
      }

      @Override
      public void onError(Request request, Throwable exception) {
        RpcLab.log("  << onError " + exception.getClass().getName() + ": " + exception.getMessage());
        callback.onError(request, exception);
      }
    });
  }
}

The server saw the header in the describeRequest() call of the lab:

== 2 describeRequest()
  << 200 application/json;charset=utf-8 //OK[1,["method=POST; content-type=text/x-gwt-rpc; charset=UTF-8; module-base-path=/bookshelf/; permutation=5F59F36FB51BE2BFE4844F629D22FF5C; X-Bookshelf-Client=lab"],0,7]
OK method=POST; content-type=text/x-gwt-rpc; charset=UTF-8; module-base-path=/bookshelf/; permutation=5F59F36FB51BE2BFE4844F629D22FF5C; X-Bookshelf-Client=lab

(= is the JavaScript escape of =, which the server applies to every string in a response.) The timeout of 1000 ms made a slow call fail with an exception that is not wrapped:

== 9 slow(1500) with a 1000 ms timeout
  >> 7|0|5|http://localhost:8932/bookshelf/|4F13...|com.example.bookshelf.shared.CatalogService|slow|I|1|2|3|4|1|5|1500|
  << onError com.google.gwt.http.client.RequestTimeoutException: A request timeout has expired after 1000 ms
onFailure com.google.gwt.http.client.RequestTimeoutException: A request timeout has expired after 1000 ms

(This scenario needs real time: the first runs used Chrome’s virtual time budget, in which the 1.5-second sleep finished before the timer fired, so the lab was driven through the DevTools protocol instead.)

Other uses of a custom builder are a correlation id header for logs, a different content-encoding strategy, and the places where credentials are attached when the server is on another origin. For a cross-origin service the browser sends a CORS preflight, because Content-Type: text/x-gwt-rpc and the X-GWT-* headers are not CORS-safelisted, and the server must answer the preflight with matching Access-Control-Allow-Headers; the general mechanism is on CORS. An unreachable origin and a blocked cross-origin call both surface as StatusCodeException with status 0 (checked with an unreachable address: 0 followed by two spaces, the empty status text and the empty body). A successful cross-origin RPC was not run in the lab.

JPA, JDO and DTOs: the 2.10.1 and 2.11 change

Persistence frameworks "enhance" entity classes on the server with extra fields (for example the state of a lazy proxy). Before 2.10.1, GWT-RPC had a feature for that case: a class annotated with javax.jdo.annotations.PersistenceCapable(detachable=true) or javax.persistence.Entity, or named in the rpc.enhancedClasses property, was an enhanced class. The server serialised the fields that the client did not know with Java serialisation into one Base64 string, sent it to the browser as an opaque blob, and deserialised it again when the browser sent the object back. That is a Java deserialisation of data controlled by the client, which is a known class of remote-code-execution vulnerabilities. Issue gwtproject/gwt#9709 ("Java Deserialization vulnerability in GWT-RPC") reported it. The maintainers' note at the top of the issue lists the mitigation that follows and two follow-up issues, signing the payloads (#9880) and not serialising such classes at all (#9881), which were still waiting for contributors when the note was written.

The 2.11.0 release (2024-01-09) disabled it by default and added a warning when it is in use, and 2.10.1 (2024-01-09) backported the change. The Developer Guide still has a section "Serializing Enhanced Classes" written for GWT 2.0 that does not mention this, so read it as history.

What 2.13.1 does now

The lab marked Book as an enhanced class with a single line in the module file, which is the same effect as an @Entity on the compiler’s class path:

<extend-configuration-property name="rpc.enhancedClasses" value="com.example.bookshelf.shared.Book" />

The compiler warned, twice (once per direction), and wrote a @ClientFields line into the policy:

[WARN] The class com.example.bookshelf.shared.Book has JPA/JDO annotations or is explicitly configured as an enhanced class using the configuration property rpc.enhancedClasses. This makes the server vulnerable to an issue with deserialization of unsafe data. See https://github.com/gwtproject/gwt/issues/9709 for more information.
...
@ClientFields,com.example.bookshelf.shared.Book,id,title,author,year,copies,isbn,format,added

The servlet then refused the whole service. It rejects the policy and falls back to the legacy one, so every call fails with a 500:

[INFO] catalogServlet: ERROR: Service deserializes enhanced JPA/JDO classes, which is unsafe. Review build logs to see which classes are affected, or set gwt.enhancedClasses.enabled to true to allow using this service. See https://github.com/gwtproject/gwt/issues/9709 for more detail.
[INFO] catalogServlet: WARNING: Failed to get the SerializationPolicy '8FE2...' for module 'http://localhost:8932/bookshelf/'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.

Starting the JVM with -Dgwt.enhancedClasses.enabled=true (the constant SerializationPolicyLoader.ENABLE_GWT_ENHANCED_CLASSES_PROPERTY) is the explicit opt-in. With it the lab log carried a different message, WARNING: Service deserializes enhanced JPA/JDO classes, which is unsafe. See https://github.com/gwtproject/gwt/issues/9709 for more detail on the vulnerability that this presents., and the calls worked. The first response then contained the blob: the Base64 text of a Java serialisation stream (here with zero server-only fields), which starts with the magic number of Java serialisation, rO0AB in Base64:

...,"com.example.bookshelf.shared.Book/2062416652","rO0ABXcEAAAAAA==","java.util.Date/3385151746",...

Two details are easy to miss. First, the detection in the 2.13.1 sources looks up javax.persistence.Entity and javax.jdo.annotations.PersistenceCapable by name on the compiler’s class path; it has no lookup for the jakarta.persistence annotations, so a Jakarta entity is not treated as enhanced. That does not make it a good RPC type (see below); it only means the warning does not tell you. Second, the opt-in is a JVM property of the server, so a build that works on a developer machine can fail in a container that lacks it.

Use data transfer objects

The recommendation of the Developer Guide’s neighbour article, "Using GWT with Hibernate" (2009), still stands and is stronger now: do not send entities. Entities carry lazy collections and proxies (accessing one raises LazyInitializationException or fails to serialise), reference whole graphs, carry columns the browser must never see, and bind the wire format to the database schema. The article compares three strategies: hand-written DTOs and copy methods, the Dozer mapper, and the Gilead library that replaces proxies automatically. This page did not evaluate Dozer or Gilead; the article’s own conclusion is that DTOs are the simple, predictable approach and cost the copy code. (Its sample DTO also holds an account password; do not copy that. A DTO is a list of what the screen needs, and a password hash is never on it.)

The Bookshelf keeps the entity out of the shared module and maps it with a function that lists the fields the browser may see:

bookshelf-server: a JPA entity that never reaches the wire, and its mapper
/** Server-only persistence entity: never appears in a RemoteService signature. */
@Entity
public class BookEntity {
  @Id
  @GeneratedValue
  private Long id;

  @Column(nullable = false)
  private String title;

  private String author;
  private int year;
  private int copies;

  @OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
  private List<LoanEntity> loans = new ArrayList<>();
  // getters omitted
}

/** Copies exactly the fields the browser needs, and nothing that is lazy or confidential. */
public final class BookMapper {
  private BookMapper() {}

  public static Book toDto(BookEntity entity) {
    return new Book(entity.getId(), entity.getTitle(), entity.getAuthor(), entity.getYear(), entity.getCopies());
  }
}

The classes compiled against jakarta.persistence-api 3.1.0 with javac --release 21; they were not run against a database. Do the mapping inside the transaction (before lazy associations close), return a new ArrayList<Book>, and keep LoanEntity.memberEmail and its like out of every DTO. For many entities a mapping generator is less error-prone than hand-written copies; the server side of this is on Lombok and MapStruct, and the persistence side on Spring Data JPA and Hibernate.

Security of the RPC channel

GWT-RPC exposes your Java methods to the network. The checklist below is the minimum; the section’s security pages (SafeHtml and XSS and XSRF, RPC and server security) treat the topics in depth.

  • Authorise every method on the server. The browser can call any method of the service interface with any argument, whether or not a button exists for it. The Bookshelf’s saveBook checks getThreadLocalRequest().getUserPrincipal() and throws the declared AccessDeniedException; in the lab, without a principal, the call ended with //EX[1,["com.example.bookshelf.shared.AccessDeniedException/35684112"],0,7]. With Spring Security or the container’s security constraints, authorise by role per method (see Spring Security). Never accept an "isAdmin" flag or a user id from a parameter as proof.

  • Validate every argument again. The page size cap of getBooks and the title check of saveBook are examples. Bean Validation constraints on the shared DTO are the natural place (see Validation).

  • Protect state-changing calls against XSRF with the token mechanism below.

  • Use HTTPS and set the session cookie attributes your container supports (Secure, HttpOnly, SameSite). RPC carries session cookies like any other request.

  • Never put credentials in method parameters. The Developer Guide’s example emptyMyInbox(String username, String password) is an illustration of the call syntax and a bad design; authenticate through the container or the framework and identify the user from the session.

  • Keep secrets and personal data out of DTOs, because every field of a serialised class leaves the server. The transient keyword and a dedicated DTO are both guards.

  • Treat the policy file as part of the build artefact, deploy it with the module it belongs to, and do not enable gwt.codeserver.port or gwt.enhancedClasses.enabled in production.

  • Do not trust the response either if it can come from another origin: a client that follows a cross-origin entry point evaluates (version 7) or parses (version 8) what that server sends.

  • Set a CSP that fits GWT’s payload (see the section on the wire and Linkers and bootstrap).

GWT-RPC has some built-in defences that are easy to over-rate. The response begins with //OK or //EX, so a <script src="..."> include of the URL sees a comment and a cross-site script cannot read the data this way (CVE-2007-2378, "JavaScript hijacking", concerns JSON that is exchanged without any protection scheme). The X-GWT-Permutation header is a custom header, so a plain cross-site <form> cannot set it. The servlet answers only POST. Content-Disposition: attachment is set on every response. None of these identifies the caller, and a request replayed by curl passes all of them.

XSRF protection

The token mechanism of GWT 2.3 and later binds each protected call to the user’s session. A service interface extends XsrfProtectedService instead of RemoteService (or a method carries @XsrfProtect; @NoXsrfProtect exempts one), the implementation extends XsrfProtectedServiceServlet (the jakarta package in the Bookshelf), and a second servlet, XsrfTokenServiceServlet, hands out tokens. The default implementation derives the token from the session cookie (the Developer Guide describes an MD5 hash of the cookie value, stateless, which works because an attacker cannot read the cookie), and the gwt.xsrf.session_cookie_name context parameter names that cookie for the token servlet and for every protected servlet. A session must exist before the first token request, which the scratch application ensured with a small filter that calls getSession(true). Method-level annotations override interface-level ones.

<context-param>
  <param-name>gwt.xsrf.session_cookie_name</param-name>
  <param-value>JSESSIONID</param-value>
</context-param>

<servlet>
  <servlet-name>xsrfServlet</servlet-name>
  <servlet-class>com.google.gwt.user.server.rpc.jakarta.XsrfTokenServiceServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>xsrfServlet</servlet-name>
  <url-pattern>/bookshelf/xsrf</url-pattern>
</servlet-mapping>

The client fetches a token first and attaches it to the protected proxy through HasRpcToken:

XsrfTokenServiceAsync xsrf = GWT.create(XsrfTokenService.class);
((ServiceDefTarget) xsrf).setServiceEntryPoint(GWT.getModuleBaseURL() + "xsrf");
xsrf.getNewXsrfToken(new AsyncCallback<XsrfToken>() {
  @Override
  public void onSuccess(XsrfToken token) {
    ((HasRpcToken) catalog).setRpcToken(token);   // every later call carries it
    onReady.run();
  }

  @Override
  public void onFailure(Throwable caught) {
    GWT.log("Cannot get an XSRF token", caught);
  }
});

In the scratch Bookshelf (the same Jetty setup as the lab, checked when the project was built for this section) the token request and the protected call answered 200, and a call without a token answered with the exception of the protocol:

//EX[2,1,["com.google.gwt.user.client.rpc.RpcTokenException/2345075298","Invalid RPC token (XSRF token missing)"],0,7]

That response reaches the browser only when the JVM can serialise exceptions (see Java 17 and later: exceptions that cannot be serialised); otherwise it is the generic 500. AbstractXsrfProtectedServiceServlet validates the token inside onAfterRequestDeserialized, so an override of that hook in a protected servlet must call super.onAfterRequestDeserialized(rpcRequest) or the check disappears. The full treatment, with RpcTokenExceptionHandler and token renewal, is on the XSRF, RPC and server security page.

A servlet is shared between requests

One servlet instance serves all requests concurrently. getThreadLocalRequest() and getThreadLocalResponse() are thread-locals and are valid only on the request thread. Keep per-request state in local variables, make shared fields thread-safe (the lab’s in-memory repository uses a ConcurrentSkipListMap and an AtomicInteger), and do not hand the request object to another thread.

The Bookshelf worked example, end to end

The pieces of this page form one runnable slice of the Bookshelf: the shared types and the two service interfaces, the servlet and its mapping, a facade, a poller, a request builder, a presenter with JUnit tests, and a lab entry point that exercises 16 scenarios and prints what happened.

Table 10. The files of the RPC example
File Content

bookshelf-shared/.../shared/Book.java, BookPage.java, Format.java, Isbn.java, Review.java

DTOs, an enum, an immutable value type and a record

bookshelf-shared/.../shared/Isbn_CustomFieldSerializer.java, Review_CustomFieldSerializer.java

Custom field serializers for the two types without a default constructor

bookshelf-shared/.../shared/BookNotFoundException.java, AccessDeniedException.java and their _CustomFieldSerializer classes

Declared checked exceptions that travel on Java 21 without a JVM option

bookshelf-shared/.../shared/CatalogService.java, CatalogServiceAsync.java

The service contract

bookshelf-server/.../server/CatalogServiceImpl.java, CatalogRepository.java

The servlet (jakarta) and an in-memory catalogue of 25 books

bookshelf-server/src/main/webapp/WEB-INF/web.xml

The servlet mapping /bookshelf/catalog

bookshelf-server/.../shared/Isbn_ServerCustomFieldSerializer.java

The type-checked server twin (server module, shared package)

bookshelf-server/.../server/PingServlet.java, PingLogic.java

A delegate servlet around a plain class

bookshelf-client/.../client/CatalogGateway.java, BookshelfRpcRequestBuilder.java, RevisionPoller.java

Facade, request builder, poller

bookshelf-client/.../client/activity/CatalogPresenter.java and CatalogPresenterTest.java

A presenter and four JVM tests

bookshelf-client/.../client/RpcLab.java

The lab entry point (not part of a real application)

The commands that were run (JDK 21, Maven 3.9 offline after the first resolution, Chrome 155 headless):

# build shared, compile the client with GWT, run the JUnit tests and start Jetty 12 on port 8932
# (the lab's server pom binds the Jetty run goal to pre-integration-test to do it in one invocation)
mvn -B -o -pl bookshelf-server -am verify -DskipTests -Djetty.http.port=8932

# JVM tests of the presenter
mvn -B -o verify -pl bookshelf-client -am

# open the page in headless Chrome through the DevTools protocol and print the log it wrote
node lab.js http://localhost:8932/ 30000

The compile of the client module (one permutation, user.agent pinned to safari for speed) printed Compilation succeeded — 1,696s.

Table 11. The lab scenarios and what each one showed (final build, JDK 21, no JVM options)
# Call Result

1

getBooks(0, 2)

OK total=25 first=The Pragmatic Programmer isbn=978-0-00-000001-0 format=PAPERBACK added=true internalNote is null=true

2

describeRequest()

The server saw POST, text/x-gwt-rpc, the module path, the permutation and the custom header X-Bookshelf-Client=lab

3

firstBookOfPage(5) (chained)

Two requests in sequence, OK 6 Working Effectively with Legacy Code

4

getBook(999)

BookNotFoundException: No book with id 999, id=999 (custom serializer)

5

getBooks(-1, 1)

StatusCodeException: 500 Server Error The call failed on the server; see server log for details [status=500]

6

saveBook(...) (RequestBuilder)

Built, not sent, then sent; AccessDeniedException: Access denied (declared)

7

getBook(2) then cancel()

pending true then false; the callback never fired

8

poller and lend(3)

poller: revision changed to 1

9

slow(1500) with a 1000 ms timeout

RequestTimeoutException: A request timeout has expired after 1000 ms

10

proxy without an entry point

NoServiceEntryPointSpecifiedException thrown by the call

11

wrong path; unreachable origin

405 Method Not Allowed with Jetty’s HTML page in the message; status 0

12

reviews(2)

Two records through the custom serializer

13

copiesByFormat()

PAPERBACK -> 12, HARDCOVER -> 12, EBOOK -> 11

14

getBook(13)

500; the log: type LegacyBook "was not included in the set of types"

15

ping through setServiceEntryPoint and a delegate servlet

OK ping -> pong

16

a gateway that answers 200 with HTML

InvocationException: <html><body>Please sign in</body></html> from CatalogService_Proxy.revision

The variants that changed the result:

Table 12. The run matrix: what changed with the JVM and the build
Variant Result

Exceptions without a serializer, no JVM option

Scenarios 4 and 6 fail with 500 (see Java 17 and later: exceptions that cannot be serialised)

The same with --add-opens java.base/java.lang=ALL-UNNAMED

Both exceptions arrive as themselves; IncompatibleRemoteServiceException also works

Custom serializers for the two exceptions, no JVM option

Both arrive as themselves, with a smaller payload

Policy files removed before the first call

Every call with a custom DTO fails; see Serialisation policy files

-Dgwt.codeserver.port with the policies removed in Super Dev Mode

Policy downloaded from the code server; the lab passes

rpc.enhancedClasses on Book

Compile warning; service refused; opt-in property restores it (see JPA, JDO and DTOs: the 2.10.1 and 2.11 change)

script-src 'self' 'unsafe-inline' with version 7

Every call fails with EvalError; with -Dgwt.rpc.version=8 the lab passes

@WebServlet instead of web.xml servlet entries

The lab passes unchanged

RemoteServiceObfuscateTypeNames

The lab passes; flags 1, type names replaced by ids

Pitfalls

Table 13. Common problems with GWT-RPC
Symptom Cause Fix

StatusCodeException: 500 Server Error The call failed on the server; see server log for details with [WARNING] Exception while dispatching incoming RPC call in the container log

Anything that escapes the pipeline: a runtime exception thrown by the method (logged as com.google.gwt.user.server.rpc.UnexpectedException: Service method 'public abstract com.example.bookshelf.shared.BookPage com.example.bookshelf.shared.CatalogService.getBooks(int,int)' threw an unexpected exception: java.lang.IllegalArgumentException: start and length must be >= 0), a policy problem, or a serialisation failure

Read the log: the first line after the warning names the cause. Declare and handle the exceptions that are part of the API.

java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.Throwable.detailMessage accessible: module java.base does not "opens java.lang" to unnamed module @35d7b746 in the log, and a 500 in the browser instead of your declared exception

Java 17 and later deny the reflection that GWT-RPC uses to serialise Throwable (gwtproject/gwt issue 9793)

Give each exception a custom field serializer, and start the container with --add-opens java.base/java.lang=ALL-UNNAMED for GWT’s own exceptions (see Java 17 and later: exceptions that cannot be serialised)

ServiceDefTarget$NoServiceEntryPointSpecifiedException: Service implementation URL not specified, thrown by the call

No @RemoteServiceRelativePath and no setServiceEntryPoint

Annotate the service interface, or set the entry point before the first call

StatusCodeException: 405 Method Not Allowed <html> ... Error 405 HTTP method POST is not supported by this URL ... (Jetty; the Developer Guide describes a 404 for a mapping mistake)

The URL does not reach the servlet: the pattern in web.xml does not equal the module base path plus the annotation value

Compare GWT.getModuleBaseURL() + "catalog" with the servlet pattern, including the context path and rename-to

A StatusCodeException whose status is 0, with empty status text and body

Network error, a blocked cross-origin call (no CORS), or an aborted request

Check the entry point origin and the CORS configuration, and the browser console

InvocationException: <html><body>Please sign in</body></html> from CatalogService_Proxy.revision

A gateway, proxy or SSO filter answered 200 with a page instead of the servlet

Detect the HTML (this exception), send the user to the sign-in, and exclude the RPC path from the redirect rules

[INFO] catalogServlet: ERROR: The serialization policy file '/bookshelf/4F134919A31D9111F950CD21A4354BB3.gwt.rpc' was not found; did you forget to include it in this deployment? followed by WARNING: Failed to get the SerializationPolicy ... a legacy, 1.3.3 compatible, serialization policy will be used. and SerializationException: Type 'com.example.bookshelf.shared.BookPage' was not assignable to 'com.google.gwt.user.client.rpc.IsSerializable' and did not have a custom field serializer.For security purposes, this type will not be serialized.

The policy file is not where the servlet looks: not deployed, another web application, another path, or the client was recompiled and the server serves the old module directory

Deploy the module directory with its *.gwt.rpc files in the same web application as the servlet; in Super Dev Mode use the launcher directory or -Dgwt.codeserver.port

SerializationException: Type 'com.example.bookshelf.server.LegacyBook' was not included in the set of types which can be serialized by this SerializationPolicy or its Class object could not be loaded. For security purposes, this type will not be serialized.

The method returned an instance of a class that the compiler never saw (a server-only subclass, an entity, a proxy)

Return an instance of the shared class; map entities to DTOs

'com.example.probe.client.Types.NoDefaultCtor' has no instantiable subtypes (compile)

A class without a no-argument constructor, a record, a class that is not Serializable, an interface without implementations, an Object field, or a type named in rpc.blacklist

Add the constructor, a custom field serializer, the marker interface or a concrete type

[ERROR] Custom Field Serializer 'com.example.bookshelf.shared.Isbn_CustomFieldSerializer' does not define a deserialize method: 'public static void deserialize(...)' (compile)

The serializer has only the instance methods

Add the public static serialize, deserialize and instantiate methods (see Custom field serializers)

Missing asynchronous version of the synchronous method 'public abstract java.lang.String hello(java.lang.String name)' or Could not find an asynchronous version for the service interface com.example.probe.client.Services.P8

The Async interface is missing or lacks a method

Copy the interface that the compiler prints

No source code is available for type java.io.File; did you forget to inherit a required module? (compile)

A shared class uses a type outside the JRE emulation

Keep the class out of the GWT source path, or use a serialisable stand-in

[WARN] Field 'private final java.lang.String isbn' will not be serialized because it is final

A final field is never sent

Make it non-final or transient

IncompatibleRemoteServiceException: This application is out of date, please click the refresh button on your browser. ( Could not locate requested method 'dropAllBooks(int)' in interface 'com.example.bookshelf.shared.CatalogService' )

A client built before the service changed (method or parameters) is still open

Reload the page; in the callback, offer the reload

Invalid type signature for com.example.bookshelf.shared.Book (inside an IncompatibleRemoteServiceException)

The server’s Book has fields or types that differ from the client’s copy

Deploy client and server together; never reuse an old browser page with a new server

Expecting version between 5 and 8 from client, got 1. (inside an IncompatibleRemoteServiceException)

The request does not come from a GWT client of this generation

Not a GWT client: do not call the service by hand

Attempt to deserialize an object of type class java.util.ArrayList when an object of type class com.example.bookshelf.shared.Book is expected

A request whose value types do not match the method signature, which a real GWT client never sends

Treat it as an attack and look at the source of the request

java.lang.SecurityException: Blocked request without GWT permutation header (XSRF attack?)

A request without X-GWT-Permutation: a script, a form post, or a custom RpcRequestBuilder that forgot super.doFinish(rb)

Call super.doFinish; for deliberate non-GWT clients override checkPermutationStrongName()

jakarta.servlet.ServletException: Content-Type was 'application/json'. Expected 'text/x-gwt-rpc'.

A caller that did not use the GWT content type

Same as above

JavaScriptException: (EvalError) : Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script

Protocol version 7 is evaluated with eval

Start the server with -Dgwt.rpc.version=8 (verified in 2.13.1) or allow 'unsafe-eval'

ERROR: Service deserializes enhanced JPA/JDO classes, which is unsafe. in the log and 500 for every call

A class is an enhanced class (rpc.enhancedClasses, or a javax.persistence.Entity on the compiler’s class path)

Stop sending entities; use DTOs (see JPA, JDO and DTOs: the 2.10.1 and 2.11 change)

RequestTimeoutException: A request timeout has expired after 1000 ms

A timeout set by a request builder expired; the server may still run the method

Make write calls idempotent or tell the user that the result is unknown

Everything after the call runs before the callback

Calls are asynchronous

Move the follow-up code into the callback

The Developer Guide sample AsyncCallback callback = new AsyncCallback() { public void onSuccess(Void result) ... does not compile (<anonymous DgSnippet$1> is not abstract and does not override abstract method onSuccess(Object) in AsyncCallback)

A raw AsyncCallback expects onSuccess(Object)

Write new AsyncCallback<Void>() { ... }

No source code is available for type com.example.bookshelf.shared.Book; did you forget to inherit a required module? when the client is compiled after only mvn test

The shared module’s sources jar is only built at package

Run mvn verify (or package) so the sources jar exists

Where older material is outdated

The Developer Guide and the tutorial still teach the right architecture, with details from the Ant era. Several statements have been overtaken:

  • Direct-Eval RPC. The Server Communication page still has a section on it, while the 2.10.0 release notes say deRPC was removed. The page itself calls the feature a failed experiment and discourages its use.

  • The sample callback. The raw AsyncCallback of "Actually making a call" does not compile (see the pitfalls). Its emptyMyInbox(username, password) also sends credentials as arguments, which is a design to avoid.

  • Printing caught.getMessage(). The Developer Guide’s asynchronous example shows the message of the exception to the user. For a StatusCodeException that is the status, the status text and the whole response body.

  • Serializing enhanced classes. The section describes the GWT 2.0 support for JPA and JDO classes. Since 2.10.1 and 2.11.0 the feature is off by default and the servlet refuses the service (see JPA, JDO and DTOs: the 2.10.1 and 2.11 change).

  • gwt-servlet.jar in war/WEB-INF/lib, webAppCreator, ant war and the embedded Jetty of development mode. Today the server classes come from org.gwtproject:gwt-servlet or gwt-servlet-jakarta with Maven; the 2.13.0 release notes say the development mode server now serves only static files, so you run your own container; webAppCreator is deprecated since 2.11.

  • Two connections per browser. The advice about parallel calls assumes an old limit.

  • Exceptions in the tutorials. DelistedException and any similar declared exception need the measures of Java 17 and later: exceptions that cannot be serialised on a current JDK.

The 2007 book "GWT in Action" (Hanson and Tacy, Manning, first edition, GWT 1.3 and 1.4) devotes two chapters to the same ground: the asynchronous model and the service types, then the client architecture around them (a facade, callback chaining, polling, custom field serializers). The concepts all still hold, and this page’s facade, chaining, poller and serializers are their modern form. The details differ:

  • It requires the IsSerializable marker; java.io.Serializable has been accepted since GWT 1.4.

  • It declares collection element types with a Javadoc annotation (@gwt.typeArgs); generics replaced it in GWT 1.5, and the compiler now warns about raw collections.

  • It sets the URL with ServiceDefTarget.setServiceEntryPoint and GWT.getModuleBaseURL(); @RemoteServiceRelativePath replaced that, although the setter still exists.

  • Its examples use Java 1.4 syntax, the javax.servlet API, hosted mode and servlets that block to emulate server push.

  • The security topics of this page (policy files, XSRF tokens, CSP, deserialisation of enhanced classes, Java 17) did not exist or were not covered.

What changed recently

  • 2.10.0 (2022-06-09): the rpc.XserializeFinalFields property turns on experimental final-field serialisation; LinkedHashSet may be serialised without a policy; deRPC is removed.

  • 2.10.1 and 2.11.0 (both 2024-01-09): JPA/JDO enhanced classes are not serialisable by default, with a compile warning; 2.11.0 adds the Jakarta artifacts gwt-servlet-jakarta (and requestfactory-server-jakarta) and makes the custom field serializers of GWT ready for Java 17.

  • 2.12.0 (2024-10-29): fixes for Content Security Policy issues in linkers, development mode and, according to the notes, workarounds for GWT-RPC payloads (see the wire section).

  • 2.13.0 (2026-02-11) and 2.13.1 (2026-06-19): the release notes list no RPC-specific change. The development mode server serves only static files (2.13.0), which matters when a tutorial assumes the embedded Jetty.

  • Open: gwtproject/gwt issue 9793 (exceptions on Java 17 and later) was still open on 2026-10-09.