Client–Server Communication

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 (2026-06-19), gwt-maven-plugin 1.3.0, a JDK 21 toolchain with -sourceLevel 17 and the Jakarta Servlet 6 server of the Bookshelf scenario, and was checked on 2026-10-09. It is an overview and a decision guide, so it touches all four kinds of code of a GWT application: client code (compiled to JavaScript, where every call starts), shared code (the types that cross the wire when GWT-RPC is used), server code (servlets, filters, event streams) and build settings (which module to inherit and which artifact to add). The output blocks were produced by compiling the examples with the 2.13.1 compiler and running them in headless Chrome 155 against Jetty 12.1.10 (the Bookshelf server) and a small scripted server that serves JSON, JSONP, server-sent events and a WebSocket; where a statement comes only from the Developer Guide, the 2.13.1 source or a registry page, the text says so.

A GWT application is a program that runs in the browser, so everything it knows about the outside world arrives over HTTP (or a protocol that starts as HTTP). The compiler gives you no network layer of its own: it gives you several, built on the same browser primitives, and they differ in who defines the message format, how much Java type information crosses the wire, which origins they may talk to and how much they cost in code size. This page names the options, shows when each one fits, explains the one rule they all share (the browser never blocks, so every answer comes back through a callback or a promise) and the one wall they all meet (the same-origin policy).

The rest of the section goes deeper: the architecture page explains where the client, shared and server code live, and the section’s pages GWT-RPC, RequestBuilder, JSON & XML, REST & JSON Libraries, RequestFactory & AutoBeans and Server Integration & Deployment treat each option in full, with XSRF, RPC & Server Security and SafeHtml & XSS on protecting the calls.

The options at a glance

Table 1. The seven ways to reach a server from GWT 2.13.1
Style What travels What you write Cross-origin

GWT-RPC

A POST with the content type text/x-gwt-rpc; the body is a compact, GWT-specific text format that carries Java objects

A RemoteService interface, an Async twin, a servlet that extends RemoteServiceServlet and shared Serializable types

Always preflighted by the browser, because the content type and the X-GWT-Permutation header are not CORS-safelisted

RequestBuilder with JSON or XML

Any text: JSON, XML, form data, plain text

An HTTP request in Java, a parser for the answer (JSONParser, XMLParser, overlay types, JsInterop)

Subject to the same-origin policy (it is XMLHttpRequest underneath); works with CORS

JSONP

A <script> element whose source is a GET URL; the server wraps JSON in a call to a function the client names

JsonpRequestBuilder, an overlay type or a JsInterop type for the result

Works across origins without CORS, at the price of running the server’s reply as code

RequestFactory

JSON (application/json; charset=utf-8) describing changes to proxies of server entities

RequestFactory, RequestContext, EntityProxy and ValueProxy interfaces, @ProxyFor and @Service bindings, a servlet

Preflighted, for the same reason as RPC

REST libraries

JSON over plain HTTP verbs, designed by you or by an existing API

An annotated Java interface (DominoKit domino-rest, RestyGWT) or hand-written code

Subject to the same-origin policy; works with CORS

Elemental2 fetch

Whatever the Fetch API can send, typically JSON

DomGlobal.fetch(...) and Promise chains, in Java

Subject to the same-origin policy; works with CORS

WebSocket and server-sent events

A long-lived connection: messages in both directions (WebSocket) or events from the server (EventSource)

Elemental2 WebSocket or EventSource, plus a server endpoint of your choice

WebSocket is not subject to CORS, so the server must check the Origin header itself; a cross-origin EventSource is CORS-checked (withCredentials for cookies)

The Developer Guide groups them in two families: the GWT RPC framework, which "transparently" calls Java servlets, and the HTTP client classes, which build and send custom requests. Everything else in the table is either one of those two with more convenience around it (RequestFactory, the REST libraries) or the browser’s own API reached through Elemental2. The Guide also states that GWT does not limit you to one mechanism or one server-side language: it is "RPC agnostic", and a GWT client can talk to a server written in any language that speaks HTTP.

Choosing a style

The deciding questions are about the server and about who else uses the API, not about the browser.

Table 2. A decision table for the Bookshelf scenario
Situation Choose Why

One team ships the GWT client and a Java servlet server together, and nothing else calls the API

GWT-RPC

The least code: shared types, no JSON mapping, the compiler checks the contract. The price is coupling and a format only GWT understands

The same API must also serve a mobile app, a script or a partner

A JSON API over HTTP, called with fetch, RequestBuilder or a REST library

Any client can use it, and you can version, document and test it with ordinary HTTP tools

The service already exists and speaks JSON or XML

fetch or RequestBuilder, with a parser for the answer

Nothing is generated for you, and nothing needs to be installed on the server

The service is on another origin and sends CORS headers

fetch or RequestBuilder with setIncludeCredentials when cookies are needed

The browser enforces CORS for both; see the section on the same-origin policy below

The service is on another origin, sends no CORS headers and you control a web server in front of your page

A same-origin reverse proxy, then one of the JSON styles

The browser sees one origin, so no CORS configuration is needed

The service is on another origin, sends no CORS headers, serves read-only data and you trust the provider completely

JSONP, as a last resort

It is the only style that crosses origins without CORS, and it hands the provider the ability to run code in your page

A data-centric CRUD application with entity graphs that you maintain already uses RequestFactory

Keep RequestFactory; for new code prefer RPC with DTOs or a JSON API

The 2.13.1 BOM still publishes it and the Developer Guide names no successor; preferring RPC or JSON for new code is a judgement of this page, not an official recommendation

The server must tell the browser that something happened (a loan was registered, a chat message arrived)

Server-sent events for one-way updates, a WebSocket for two-way traffic, polling when nothing else is possible

A request/response style cannot start a conversation; see the section on push below

The same choices as a flowchart:

flowchart TD start["The browser needs to talk to a server"] --> push{"Does the server need to tell the browser about events?"} push -->|"one way, server to browser"| sse["Server-sent events: EventSource"] push -->|"both ways, low latency"| ws["WebSocket: the server checks Origin"] push -->|"only by asking again"| poll["Timer plus an RPC or JSON call"] push -->|"no, the client asks"| own{"Does one team ship the Java server and the GWT client together?"} own -->|"yes"| others{"Must other clients call the same API?"} own -->|"no, another service"| cors{"Does the service send CORS headers?"} others -->|"yes"| rest["JSON API: fetch, RequestBuilder or a REST library"] others -->|"no"| crud{"Entity graphs with change tracking?"} crud -->|"no"| rpc["GWT-RPC: Async interface and AsyncCallback<T>"] crud -->|"yes, existing code"| rf["RequestFactory"] crud -->|"yes, new code"| rpc cors -->|"yes"| rest cors -->|"no, and you run a proxy"| proxy["Same-origin reverse proxy, then a JSON API"] cors -->|"no, trusted read-only data"| jsonp["JSONP: last resort"]

Whatever the box, the bottom of the diagram is the same: a callback or a promise receives the answer. That is the next section.

Asynchronous programming

GWT offers no blocking network call. The FAQ (GWT FAQ — Server) states why: browsers run JavaScript on a single thread, so a blocking call to XMLHttpRequest would also block the user interface and make the page appear to freeze, and the workarounds that exist are not available in every browser. Every style in this page is therefore asynchronous: the method that starts the call returns at once, and the result is delivered later, when the event loop is free, to code that you registered.

The Developer Guide adds the practical consequences. The code that follows the call runs while the round trip is still in progress. Code inside onSuccess is written next to the call but runs only after the calling code has returned to the JavaScript event loop and the answer has arrived. A server that is slow or unreachable cannot hang the application, and several calls can be in flight at once, so the time they take overlaps instead of adding up.

The callback contract

GWT-RPC and JSONP use the same interface from com.google.gwt.user.client.rpc (the AsyncCallback interface lives there even when no RPC is involved):

public interface AsyncCallback<T> {
  void onFailure(Throwable caught);
  void onSuccess(T result);
}

Exactly one of the two methods is called, once, for a call that completes. RequestBuilder uses a different pair, RequestCallback.onResponseReceived(Request, Response) and onError(Request, Throwable), and Elemental2 uses Promise.then(onFulfilled, onRejected). The three shapes mean the same thing, and they differ in one way that matters: what counts as a failure.

Table 3. What each style reports as a failure
Style A failure is…​

AsyncCallback (RPC)

An exception declared in the throws clause of the service method (delivered as that exception); a status other than 200, including the status 0 of a request that failed or was blocked (StatusCodeException); a status 200 whose body is not an RPC payload (InvocationException); a payload that cannot be deserialised (IncompatibleRemoteServiceException)

RequestCallback

Only a request that could not be sent or timed out (onError). A response with any HTTP status, including 404 and 500, goes to onResponseReceived, and so does a request the browser blocked: see the pitfalls table

Promise from fetch

Only a network-level problem (the promise is rejected with a TypeError). A 404 or 500 resolves normally with ok set to false

JsonpRequestBuilder

A timeout, or the failure callback of the server if you configured one. A 404 for the script file is reported as a timeout

The order in which things happen

The following client code sends two requests, throws from a callback and tries a request that exceeds its timeout. The server answers the first request after 400 ms and the second after 50 ms; the 404 request goes to a path that does not exist on the scripted server.

Probe.java, the ordering test (excerpt)
log("1 before the calls");
RequestBuilder first = new RequestBuilder(RequestBuilder.GET, api + "/delay?ms=400&tag=first");
RequestBuilder second = new RequestBuilder(RequestBuilder.GET, api + "/delay?ms=50&tag=second");
int[] seq = {0};
for (RequestBuilder rb : new RequestBuilder[] {first, second}) {
  int mine = ++seq[0];
  rb.sendRequest(null, new RequestCallback() {
    @Override public void onError(Request request, Throwable exception) { log("onError " + exception); }
    @Override public void onResponseReceived(Request request, Response response) {
      boolean stale = mine < latest;
      latest = Math.max(latest, mine);
      log("3 response of request " + mine + ": " + response.getText() + (stale ? " (stale, ignored)" : " (applied)"));
    }
  });
}
log("2 after sendRequest: both requests are in flight");
Output in headless Chrome 155 (GWT 2.13.1)
1 before the calls
2 after sendRequest: both requests are in flight
fetch 404 resolved: ok=false status=404
uncaught: java.lang.IllegalStateException: boom in onResponseReceived
3 response of request 2: second (applied)
3 response of request 1: first (stale, ignored)
timeout onError: com.google.gwt.http.client.RequestTimeoutException: A request timeout has expired after 300 ms

Four things to read in this output.

  • Line 2 is printed before any response: the call returned immediately, and the rest of the method ran while both requests were in flight.

  • The answer of the second request arrived first (50 ms against 400 ms). Responses are not delivered in the order in which requests were sent, so a screen that reloads on every keystroke or page change can show old data over new data unless the callback ignores stale answers.

  • An exception thrown inside a callback does not reach the code that started the call, because that code has long finished. It reaches the handler registered with GWT.setUncaughtExceptionHandler, which here printed the IllegalStateException.

  • A RequestBuilder timeout set with setTimeoutMillis calls onError with a RequestTimeoutException, and no "slow answered" line appeared even though the scripted server replied to that request after 1500 ms: the late answer is not delivered.

Handling the in-between state

An asynchronous call has a state that a blocking call never had: started, not finished. A screen has to say what it shows during that time, and what happens if the user acts again. Two patterns cover most of the Bookshelf catalogue.

The first is a request counter that makes the callback ignore answers to requests that are no longer the latest one:

CatalogLoader.java: ignore stale answers
private int requestCounter;

public void load(int start, int length) {
  final int mine = ++requestCounter;
  DomGlobal.document.title = "Loading...";
  catalog.getBooks(start, length, new AsyncCallback<BookPage>() {
    @Override
    public void onFailure(Throwable caught) {
      if (mine == requestCounter) {
        DomGlobal.document.title = "Could not load the catalogue";
      }
    }

    @Override
    public void onSuccess(BookPage page) {
      if (mine != requestCounter) {
        return; // a newer request has been sent; this answer is stale
      }
      DomGlobal.document.title = page.getBooks().size() + " of " + page.getTotal();
    }
  });
}

The second is a join: start independent calls together and continue when all of them have succeeded, which is faster than chaining them. GWT has no Future, and the Elemental2 Promise.all is awkward with Java generics (see the DOM and Elemental2 page for its pitfall), so a small typed helper is enough:

Join2.java: continue when two parallel calls have both succeeded
public static final class Join2<A, B> {
  private A a;
  private B b;
  private boolean haveA, haveB, failed;
  private final AsyncCallback<Void> done;

  public Join2(AsyncCallback<Void> done) { this.done = done; }

  public AsyncCallback<A> first() {
    return new AsyncCallback<A>() {
      @Override public void onFailure(Throwable t) { fail(t); }
      @Override public void onSuccess(A result) { a = result; haveA = true; maybeDone(); }
    };
  }

  public AsyncCallback<B> second() {
    return new AsyncCallback<B>() {
      @Override public void onFailure(Throwable t) { fail(t); }
      @Override public void onSuccess(B result) { b = result; haveB = true; maybeDone(); }
    };
  }

  public A a() { return a; }
  public B b() { return b; }

  private void fail(Throwable t) {
    if (!failed) { failed = true; done.onFailure(t); }
  }

  private void maybeDone() {
    if (haveA && haveB && !failed) { done.onSuccess(null); }
  }
}

// Usage: the page of books and the first book are requested together.
Join2<BookPage, Book> join = new Join2<>(new AsyncCallback<Void>() {
  @Override public void onFailure(Throwable t) { DomGlobal.console.log("one of the calls failed: " + t); }
  @Override public void onSuccess(Void ignored) { DomGlobal.console.log("both arrived"); }
});
catalog.getBooks(0, 10, join.first());
catalog.getBook(7L, join.second());

Both classes were compiled by the 2.13.1 compiler as part of a probe module. A callback that touches a widget must also guard against the widget having been removed while the call was in flight (compare isAttached()), and a screen that can be left should cancel its Request (Request.cancel()) when it goes.

The Developer Guide’s claim that most browsers limit outgoing connections "to two at a time" describes the browsers of the first GWT releases. Current browsers allow more connections per host and HTTP/2 multiplexes requests on one connection, so the amount of parallelism you get depends on the browser and the protocol: measure it instead of designing around the number two.

The options in practice

Each subsection shows the smallest working code and the facts that differ from what a reader of another stack would expect. All client snippets below were compiled in a probe module that inherits the modules named in the text.

GWT-RPC

GWT-RPC maps a Java interface to a servlet. The client sees an Async twin of the interface whose methods take a callback, GWT generates the proxy with deferred binding, and the server side extends RemoteServiceServlet (the com.google.gwt.user.server.rpc.jakarta package for Jakarta Servlet 5 and later, shipped in gwt-servlet-jakarta; the javax variant is in gwt-servlet). The Bookshelf catalogue service is declared like this:

CatalogService.java and CatalogServiceAsync.java (shared)
@RemoteServiceRelativePath("catalog")
public interface CatalogService extends XsrfProtectedService {
  BookPage getBooks(int start, int length);
  Book getBook(long id) throws BookNotFoundException;
  Book saveBook(Book book) throws BookNotFoundException;
}

public interface CatalogServiceAsync {
  void getBooks(int start, int length, AsyncCallback<BookPage> callback);
  void getBook(long id, AsyncCallback<Book> callback);
  void saveBook(Book book, AsyncCallback<Book> callback);
}

The User module already inherits the RPC module (com.google.gwt.user.RemoteService), so a client that inherits com.google.gwt.user.User needs nothing else. The request is a POST with Content-Type: text/x-gwt-rpc; charset=utf-8, and the client also sends X-GWT-Permutation and X-GWT-Module-Base headers (the names are constants of RpcRequestBuilder). RemoteServiceServlet refuses a request whose content type is not text/x-gwt-rpc and, unless you override checkPermutationStrongName, a request without the permutation header, which is also a first line of defence against cross-site request forgery. Run against the Bookshelf server, the refusals look like this:

Calling the Bookshelf RPC servlet without being a GWT client (Jetty 12.1.10, GWT 2.13.1)
$ curl -s -i http://localhost:8931/bookshelf/catalog | head -1
HTTP/1.1 405 Method Not Allowed

$ curl -s -i -X POST -H 'Content-Type: text/plain' --data 'x' http://localhost:8931/bookshelf/catalog | sed -n '1p;$p'
HTTP/1.1 500 Server Error
The call failed on the server; see server log for details

The server log has the reason:

WARNING Exception while dispatching incoming RPC call
jakarta.servlet.ServletException: Content-Type was 'text/plain'. Expected 'text/x-gwt-rpc'.
...
WARNING Exception while dispatching incoming RPC call
java.lang.SecurityException: Blocked request without GWT permutation header (XSRF attack?)

The GWT-RPC page covers the serialisation rules, the serialisation policy file, custom field serialisers, every exception type, RpcRequestBuilder and ServiceDefTarget. Two facts belong in any overview. First, a Bookshelf service that returns JPA or Hibernate entities invites trouble: since GWT 2.10.1 and 2.11 enhanced JPA and JDO classes are no longer serialisable by default, so the wire types should be plain data transfer objects. Second, on Java 17 and later a checked exception thrown by a service cannot be serialised unless the servlet container JVM starts with --add-opens java.base/java.lang=ALL-UNNAMED (the open upstream issue is gwtproject/gwt#9793); the client then sees the status 500 text above, and the GWT-RPC page reproduces the exception. RPC calls that change data must be protected against cross-site request forgery, which the Bookshelf service does by extending XsrfProtectedService; the XSRF, RPC & Server Security page explains the token flow.

RequestBuilder with JSON or XML

RequestBuilder is the building block of every other HTTP style in the GWT library: it wraps XMLHttpRequest. Its module, com.google.gwt.http.HTTP, is inherited by User; JSON support (com.google.gwt.json.JSON) and JSONP support (com.google.gwt.jsonp.Jsonp) are separate modules, and neither is in User. A request is a method and a URL (encoded with URL.encode or its siblings), optional headers, a timeout and a callback:

Probe.java: RequestBuilder against the Bookshelf JSON endpoint
String url = api + "/api/books?start=0&length=1";
RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, URL.encode(url));
rb.setTimeoutMillis(5000);
try {
  rb.sendRequest(null, new RequestCallback() {
    @Override public void onError(Request request, Throwable exception) {
      log("RequestBuilder onError: " + exception);
    }
    @Override public void onResponseReceived(Request request, Response response) {
      log("RequestBuilder onResponseReceived: status=" + response.getStatusCode()
          + " statusText='" + response.getStatusText() + "' length=" + response.getText().length());
    }
  });
} catch (RequestException e) {
  log("RequestBuilder RequestException: " + e);
}

When the endpoint is on the same origin, or answers CORS correctly, the output is:

RequestBuilder onResponseReceived: status=200 statusText='OK' length=124

If no header is set, send and sendRequest use the content type text/plain; charset=utf-8; setting any header replaces that default, so a POST with JSON must set Content-Type itself. Parsing the answer is your job. With the JSON module, JSONParser.parseStrict is the safe parser:

Parsing the Bookshelf page (requires the JSON module)
JSONObject root = JSONParser.parseStrict(body).isObject();
int total = (int) root.get("total").isNumber().doubleValue();

The RequestBuilder, JSON & XML page shows overlay types, JsInterop and the AutoBeanCodex alternatives, why JSONParser.parse and JsonUtils.eval should not be used on untrusted text, and the XML parser.

JSONP

Browsers refuse to let a script read the answer of a cross-origin XHR, but they do run a <script> element loaded from any origin. JSONP exploits that: the client names a callback function in the URL, and the server answers with JavaScript that calls it with the data. JsonpRequestBuilder (module com.google.gwt.jsonp.Jsonp) generates the function name and sends the name in the callback parameter, which you can change with setCallbackParam; the default timeout is 10 seconds (setTimeout).

Probe.java: JSONP
JsonpRequestBuilder jsonp = new JsonpRequestBuilder();
jsonp.setTimeout(3000);
jsonp.requestObject(api + "/jsonp?x=1", new AsyncCallback<JavaScriptObject>() {
  @Override public void onFailure(Throwable caught) { log("JSONP onFailure: " + caught); }
  @Override public void onSuccess(JavaScriptObject result) { log("JSONP onSuccess: " + new JSONObject(result)); }
});

Output in headless Chrome 155, with the page on port 9931 and the endpoint on port 8931, which sends no CORS header at all:

JSONP onSuccess: {"total":25}

It worked where RequestBuilder and fetch were blocked (next section), and that is its danger. The reply is executed as JavaScript in your page with your page’s privileges: a provider that is compromised, or that is wrong about escaping, runs code in the Bookshelf session. JSONP can only issue GET requests, cannot send headers, and cannot tell a 404 from a network failure; the same call against a server that had no such path (a 404) produced:

JSONP onFailure: com.google.gwt.jsonp.client.TimeoutException: Timeout while calling http://localhost:8931/jsonp?x=1

Use it only when the provider is trusted and offers nothing else. The old "JavaScript hijacking" weakness of JSON over GET (CVE-2007-2378) is a related design issue, and the XSRF, RPC & Server Security page covers it.

RequestFactory

RequestFactory is the Developer Guide’s data-oriented alternative to RPC: the client works with proxy interfaces (EntityProxy, ValueProxy) through a RequestContext, only the changes of a modified object travel to the server, and the server side is bound to your domain classes with @ProxyFor, @Service and optional Locator classes. The Maven coordinates changed when GWT moved to Maven Central: the artifacts are requestfactory-client, requestfactory-server, requestfactory-server-jakarta and requestfactory-apt under the group org.gwtproject.web.bindery, all at 2.13.1 (2026-06-19) on Maven Central on 2026-10-09, and the 2.13.1 BOM manages the requestfactory dependency. The servlet exchanges application/json documents, so cross-origin use is preflighted like RPC. No official page that I checked declares RequestFactory deprecated or in maintenance mode, and none recommends a replacement; the RequestFactory & AutoBeans page treats it fully, including when not to start new code with it.

REST libraries and Elemental2 fetch

A JSON API is the neutral choice, and the GWT ecosystem offers three levels of convenience for calling one. The lowest is the browser’s Fetch API through Elemental2 (com.google.elemental2:elemental2-dom, which also brings elemental2-core and elemental2-promise; the artifact is not managed by the GWT BOM, so you pin the version, 1.3.2 being the latest stable release at the date above; module elemental2.dom.Dom):

Probe.java: fetch with Elemental2
DomGlobal.fetch(url).then(r -> {
  log("fetch resolved: ok=" + r.ok + " status=" + r.status);
  return r.text().then(body -> { log("fetch body length=" + body.length()); return null; });
}, err -> {
  log("fetch rejected: " + err);
  return null;
});
fetch resolved: ok=true status=200
fetch body length=124

Above fetch are libraries that generate the client from an annotated Java interface. Their status on Maven Central and GitHub, checked on 2026-10-09, is uneven:

Table 4. Status of the REST libraries (Maven Central and GitHub, 2026-10-09)
Library Latest release found Repository activity

DominoKit domino-rest (org.dominokit:domino-rest-shared) with domino-jackson (org.dominokit:domino-jackson)

domino-rest-shared 2.0.2 (2026-09-24), domino-jackson 1.0.6 (2026-07-27)

DominoKit/domino-rest last pushed 2026-09-24

RestyGWT (org.fusesource.restygwt:restygwt)

2.2.7 (2020-05-17)

resty-gwt/resty-gwt last pushed 2023-01-02

gwt-jackson (com.github.nmorel.gwtjackson:gwt-jackson)

0.15.4 (2018-09-16)

nmorel/gwt-jackson last pushed 2024-03-15

AutoREST (intendia-oss/autorest)

Not found on Maven Central under com.intendia.gwt.autorest

Last pushed 2023-11-24

The dated numbers state what the registries show; they do not say whether a library works with GWT 2.13.1, which the REST & JSON Libraries page tests where it makes a claim. For the server half of such an API see REST APIs with Spring Boot.

Server push: WebSocket, server-sent events and polling

A request/response style cannot tell the browser that something happened. There are three answers, and none of them is GWT-RPC (the RPC protocol is request/response and its format is not a message stream):

  • Polling: ask again every few seconds with a Timer. It is the most compatible and the most wasteful option, and the simplest to secure, because every poll is an ordinary authenticated request.

  • Server-sent events (SSE): one HTTP response that stays open and carries text/event-stream events; the browser’s EventSource reconnects by itself. One direction only, plain HTTP GET, subject to CORS like fetch (the withCredentials field of EventSource sends cookies cross-origin).

  • WebSocket: a full-duplex channel started by an HTTP upgrade. Messages are strings or binary, so the format (usually JSON) is yours.

Elemental2 provides EventSource and WebSocket (module elemental2.dom.Dom); GWT-RPC serialisation is not involved:

Probe.java: polling, SSE and WebSocket
Timer poll = new Timer() {
  int ticks;
  @Override public void run() {
    log("poll tick " + (++ticks));
    if (ticks == 2) { cancel(); }
  }
};
poll.scheduleRepeating(300);

EventSource es = new EventSource(api + "/events");
int[] n = {0};
es.onmessage = e -> { log("SSE message: " + e.data); if (++n[0] == 3) { es.close(); log("SSE closed"); } };
es.onerror = e -> log("SSE error readyState=" + es.readyState);

String wsUrl = api.replaceFirst("^http", "ws") + "/ws";
WebSocket ws = new WebSocket(wsUrl);
ws.onopen = e -> { log("WebSocket open"); ws.send("hello"); };
ws.onmessage = e -> { log("WebSocket message: " + e.data.asString()); ws.close(); };
ws.onclose = e -> log("WebSocket close code=" + e.code);
ws.onerror = e -> log("WebSocket error");

Against the scripted server (a stream of "loan N" events and an echo endpoint, with CORS enabled for the page’s origin), the lines of these three pieces appeared in this order (lines of the other tests omitted):

SSE message: loan 1
SSE message: loan 2
SSE message: loan 3
SSE closed
WebSocket open
WebSocket message: echo: hello
WebSocket close code=1000
poll tick 1
poll tick 2

The server side is outside GWT: a Jakarta WebSocket endpoint, an asynchronous servlet that streams text/event-stream, or the push support of your framework. Other sections of this site show the idea on other stacks: server-sent events and WebSockets with Thymeleaf and WebSocket push in JSF. The open issue #264 of this documentation repository (chat, videoconference and websockets) is where a fuller treatment of real-time features is tracked; this page only shows how a GWT client connects.

The book that this section consulted predates all three browser APIs: its server-push material relies on blocking loops in servlets and on polling, and it describes GWT 1.3/1.4 in which EventSource and WebSocket did not exist. Today the choice is between the three options above.

The same-origin policy and CORS

The browser lets a script read a response only if it comes from the same origin as the page: the same scheme, host and port. The GWT FAQ (GWT FAQ — Server) states it plainly, and also states that this is not a GWT problem: it applies to any Ajax framework. It affects GWT in three places: the host page and the compiled scripts, the calls the application makes, and development (the code server on one port, the application server on another). Cross-Origin Resource Sharing is the server’s way of opting specific other origins in; the site’s CORS page explains the headers and the preflight protocol, and this section shows how each GWT style meets them.

What each style does when the origins differ

The page below was loaded from http://localhost:9931, and the Bookshelf API runs on http://localhost:8931, which is another origin because of the port. The server sent no CORS headers.

Cross-origin results with no CORS headers on the server (headless Chrome 155, GWT 2.13.1)
RequestBuilder onResponseReceived: status=0 statusText='' length=0
fetch rejected: TypeError: Failed to fetch
SSE error readyState=2
JSONP onSuccess: {"total":25}
WebSocket open
WebSocket message: echo: hello

The browser console explains the first, third and fourth lines:

Access to XMLHttpRequest at 'http://localhost:8931/api/books?start=0&length=1' from origin 'http://localhost:9931' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Access to fetch at 'http://localhost:8931/api/books?start=0&length=1' from origin 'http://localhost:9931' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Access to resource at 'http://localhost:8931/events' from origin 'http://localhost:9931' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Read the first line twice: RequestBuilder did not call onError. The blocked request arrived in onResponseReceived with the status code 0 and an empty body, because the browser hides the details of a failed cross-origin request from the script. Code that treats every non-200 status as "the server said no" will show a misleading message; test for status 0 and report a network or CORS problem.

Table 5. How the styles meet the policy
Style Behaviour

RequestBuilder, REST libraries

Subject to the policy; a blocked request produces onResponseReceived with status 0. They honour CORS, and setIncludeCredentials(true) sends cookies (see below)

fetch

Subject to the policy; a blocked request rejects the promise with a TypeError

EventSource

Subject to the policy; the stream is refused without a matching Access-Control-Allow-Origin

JSONP

Not subject, because it is a <script> element; this is also why it is dangerous

WebSocket

Not subject to CORS: the connection succeeded without a single CORS header, and the scripted server saw the page’s Origin: http://localhost:9931 request header. The server must decide whether that origin may connect, otherwise any web page the user visits can open a socket with the user’s cookies (cross-site WebSocket hijacking)

GWT-RPC, RequestFactory

Subject to the policy, and always preflighted, because the content types (text/x-gwt-rpc, application/json) are not CORS-safelisted and RPC adds X-GWT-Permutation and X-GWT-Module-Base headers

Preflight against an RPC servlet

A servlet that knows nothing about CORS does not answer the preflight in a way the browser accepts. This is the Bookshelf RPC servlet, asked the question the browser asks before it sends a cross-origin RPC call:

$ curl -s -i -X OPTIONS \
    -H 'Origin: http://localhost:9931' \
    -H 'Access-Control-Request-Method: POST' \
    -H 'Access-Control-Request-Headers: content-type,x-gwt-permutation,x-gwt-module-base' \
    http://localhost:8931/bookshelf/catalog
HTTP/1.1 200 OK
Allow: POST, TRACE, OPTIONS
Content-Length: 0

The status is 200 and there are no Access-Control-Allow-* headers, so the browser cancels the actual call. The servlet stack shipped in GWT has no CORS support of its own; you add it in front, or you avoid the problem.

Avoiding CORS, or answering it correctly

The first choice is not to cross origins at all. The Developer Guide’s deployment section already shows the pattern: serve the static files from one server and configure it to proxy only the servlet paths to the application server (it uses Apache HTTPD with ProxyPass and ProxyPassReverse), so that the browser sees a single origin. The same idea works with any reverse proxy, with Spring Boot serving the compiled module and the API from one application, and in development with the proxy of your build tool or code server. No header has to be configured, and cookies stay first-party.

When the API must be on another origin, answer CORS on the server, with an explicit list of allowed origins and never a reflected Origin header. Jetty 12 ships a CrossOriginHandler (module cross-origin), Tomcat a CorsFilter (org.apache.catalina.filters.CorsFilter), and Spring has its own configuration; a filter of a few lines is enough for the Bookshelf servlets and shows the moving parts:

AllowListCorsFilter.java: CORS for an allow-list of origins (server)
public class AllowListCorsFilter implements Filter {
  private Set<String> allowed = Set.of();

  @Override
  public void init(FilterConfig config) {
    String origins = config.getInitParameter("allowedOrigins");
    if (origins != null && !origins.isBlank()) {
      allowed = Set.of(origins.split("\\s*,\\s*"));
    }
  }

  @Override
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) resp;
    String origin = request.getHeader("Origin");
    if (origin != null) {
      response.addHeader("Vary", "Origin");
      if (allowed.contains(origin)) {
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        boolean preflight = "OPTIONS".equals(request.getMethod())
            && request.getHeader("Access-Control-Request-Method") != null;
        if (preflight) {
          response.setHeader("Access-Control-Allow-Methods", "GET, POST");
          response.setHeader("Access-Control-Allow-Headers",
              "Content-Type, X-GWT-Permutation, X-GWT-Module-Base, X-Bookshelf-Client");
          response.setHeader("Access-Control-Max-Age", "600");
          response.setStatus(HttpServletResponse.SC_NO_CONTENT);
          return;
        }
      }
    }
    chain.doFilter(req, resp);
  }
}

Mapped in web.xml to /api/*, /bookshelf/catalog and /bookshelf/xsrf with the single allowed origin http://localhost:9931, the same preflight now succeeds for that origin and is not granted to any other:

$ curl -s -i -X OPTIONS -H 'Origin: http://localhost:9931' -H 'Access-Control-Request-Method: POST' \
    -H 'Access-Control-Request-Headers: content-type,x-gwt-permutation' http://localhost:8931/bookshelf/catalog
HTTP/1.1 204 No Content
Vary: Origin
Access-Control-Allow-Origin: http://localhost:9931
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, X-GWT-Permutation, X-GWT-Module-Base, X-Bookshelf-Client
Access-Control-Max-Age: 600

$ curl -s -i -X OPTIONS -H 'Origin: http://evil.example' -H 'Access-Control-Request-Method: POST' \
    http://localhost:8931/bookshelf/catalog
HTTP/1.1 200 OK
Vary: Origin
Allow: POST, TRACE, OPTIONS
Content-Length: 0

A RequestBuilder call from the page on port 9931 with a custom header and cookies then succeeds:

RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, api + "/api/books?start=0&length=1");
rb.setHeader("X-Bookshelf-Client", "probe");   // forces a preflight
rb.setIncludeCredentials(true);                // send cookies cross-origin
status=200 X-Content-Type-Options=null body={"total":25,"books":

RequestBuilder.setIncludeCredentials sets withCredentials on the underlying request. With credentials, the server must answer with the exact origin (the wildcard is not accepted) and Access-Control-Allow-Credentials: true. The last output line shows a detail that surprises people: the server did send X-Content-Type-Options: nosniff, yet response.getHeader("X-Content-Type-Options") returned null, because a cross-origin script can read only the CORS-safelisted response headers unless the server lists others in Access-Control-Expose-Headers.

Two things CORS does not do. It does not authenticate anyone: any non-browser client ignores it, so the authorisation checks stay on the server, for every call, whatever the origin (the CORS page of this site makes the same point). And it does not replace protection against cross-site request forgery for cookie-authenticated calls; the XSRF, RPC & Server Security page covers the token flow that the Bookshelf RPC service uses.

A worked example: the Bookshelf catalogue

The Bookshelf scenario of this section reaches its server in four of these ways, so the same screen (one page of the catalogue) can be compared:

Table 6. Bookshelf endpoints used in this section
Style Server side Client side

GWT-RPC

CatalogServiceImpl extends com.google.gwt.user.server.rpc.jakarta.XsrfProtectedServiceServlet, mapped to /bookshelf/catalog

CatalogServiceAsync.getBooks(start, length, AsyncCallback<BookPage>)

JSON over HTTP

BooksRestServlet, GET /api/books?start=0&length=10, answering {"total":25,"books":[...]} with X-Content-Type-Options: nosniff

RequestBuilder or fetch, then JSONParser.parseStrict

Events

A stream of text/event-stream messages for loan events (scripted in this page’s tests)

EventSource with onmessage

Cross-origin variant

AllowListCorsFilter in front of the two servlets

RequestBuilder with setIncludeCredentials(true)

The RPC servlet needs a session for its XSRF token, so a session filter creates one on every request; the REST endpoint is read-only and needs none. The catalogue returns data transfer objects (Book, BookPage), not persistent entities, and every write goes through a method that checks, on the server, whether the caller may change that book. Nothing the client sends is trusted, and no credentials appear in the client code or in the examples of this page.

Pitfalls

Table 7. Pitfalls and their symptoms (messages reproduced with GWT 2.13.1, Jetty 12.1.10, Chrome 155)
Symptom Cause Fix

RequestBuilder reports onResponseReceived with status 0 and empty text instead of an error

A cross-origin request blocked by the browser: has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Check status 0 explicitly; serve the page and the API from one origin or add CORS headers on the server

fetch rejects with TypeError: Failed to fetch

A network failure or a CORS block (the script cannot tell them apart); a 404 does not reject, it resolves with ok false

Open the console for the CORS message; test r.ok for HTTP errors. Error responses must carry the CORS headers too, otherwise a 404 or 500 looks like a network failure

curl or a browser address bar gets 405 Method Not Allowed from the RPC URL

RPC servlets accept only POST

Call it through the generated proxy; this is also the quick check that the servlet mapping exists

Server log: Content-Type was 'text/plain'. Expected 'text/x-gwt-rpc'. and the client gets status 500 with The call failed on the server; see server log for details

A request that did not come from the GWT proxy, or a proxy in front of the servlet that rewrites the content type

Send text/x-gwt-rpc; charset=utf-8, or leave the call to the generated proxy

Server log: Blocked request without GWT permutation header (XSRF attack?)

A client that does not send X-GWT-Permutation (not a GWT-compiled client, or a proxy that strips the header)

Keep the header end to end; override checkPermutationStrongName only knowingly

A cross-origin RPC call never reaches the service

The preflight answers 200 with Allow: POST, TRACE, OPTIONS and no Access-Control-Allow-* headers

Add CORS in front of the servlet, with X-GWT-Permutation and X-GWT-Module-Base in Access-Control-Allow-Headers, or proxy to one origin

response.getHeader(...) returns null for a header the server sent

Cross-origin scripts see only safelisted headers

Add the header name to Access-Control-Expose-Headers

JSONP onFailure with TimeoutException: Timeout while calling ... for an endpoint that returns 404

A script that fails to load is reported as a timeout

Check the URL and the server log; do not expect an HTTP status

An exception thrown in onSuccess is "swallowed"

It is routed to GWT.UncaughtExceptionHandler, not to the caller

Register a handler that logs to the server (see the Logging page)

The list shows older data after a fast succession of clicks

Answers arrive in any order

Number the requests and ignore stale answers, as in CatalogLoader

A WebSocket connects from any site

WebSockets are not subject to CORS

Compare the Origin request header with an allow-list in the handshake

The callback runs on a widget that has been removed

The call outlived the screen

Cancel the Request, or test isAttached() first

Recent changes and where the book is outdated

Table 8. What changed, with versions
Version Change

2.10.1 and 2.11.0 (2024-01-09)

Enhanced JPA and JDO classes are no longer serialisable by default in GWT-RPC; the Jakarta artifacts for GWT-RPC and RequestFactory appear (2.11.0)

2.12.0

Content-security-policy workarounds for GWT-RPC payloads and a fix for the linkers

Java 17 and later

Serialising a server-side exception fails without --add-opens java.base/java.lang=ALL-UNNAMED (gwtproject/gwt#9793, open)

The book consulted for this section (Hanson and Tacy, 2007, GWT 1.3/1.4) predates most of what this page uses: it builds RPC clients with ServiceDefTarget.setServiceEntryPoint and GWT.getModuleBaseURL() where today a @RemoteServiceRelativePath annotation does the job, it uses Java 1.4 collections and @gwt.typeArgs comments where today generics are used, it loads JSON with JSONParser.parse, which relied on eval, where parseStrict is the safe call, it relies on server push by blocking in servlets where EventSource and WebSocket exist, and its example services (Google and Yahoo search endpoints) no longer exist. The concepts it teaches are still valid: calls must be asynchronous, the same-origin policy decides what the client may reach, and the client must treat every answer as untrusted input.