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
| Style | What travels | What you write | Cross-origin |
|---|---|---|---|
GWT-RPC |
A POST with the content type |
A |
Always preflighted by the browser, because the content type and the |
|
Any text: JSON, XML, form data, plain text |
An HTTP request in Java, a parser for the answer ( |
Subject to the same-origin policy (it is |
JSONP |
A |
|
Works across origins without CORS, at the price of running the server’s reply as code |
RequestFactory |
JSON ( |
|
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 |
Subject to the same-origin policy; works with CORS |
Elemental2 |
Whatever the Fetch API can send, typically JSON |
|
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 ( |
Elemental2 |
WebSocket is not subject to CORS, so the server must check the |
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.
| 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 |
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 |
|
Nothing is generated for you, and nothing needs to be installed on the server |
The service is on another origin and sends CORS headers |
|
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:
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.
| Style | A failure is… |
|---|---|
|
An exception declared in the |
|
Only a request that could not be sent or timed out ( |
|
Only a network-level problem (the promise is rejected with a |
|
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.
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");
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 theIllegalStateException. -
A
RequestBuildertimeout set withsetTimeoutMilliscallsonErrorwith aRequestTimeoutException, 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:
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:
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:
@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:
$ 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:
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:
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).
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):
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:
| Library | Latest release found | Repository activity |
|---|---|---|
DominoKit |
|
DominoKit/domino-rest last pushed 2026-09-24 |
RestyGWT ( |
2.2.7 (2020-05-17) |
resty-gwt/resty-gwt last pushed 2023-01-02 |
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 |
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-streamevents; the browser’sEventSourcereconnects by itself. One direction only, plain HTTP GET, subject to CORS likefetch(thewithCredentialsfield ofEventSourcesends 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:
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.
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.
| Style | Behaviour |
|---|---|
|
Subject to the policy; a blocked request produces |
|
Subject to the policy; a blocked request rejects the promise with a |
|
Subject to the policy; the stream is refused without a matching |
JSONP |
Not subject, because it is a |
WebSocket |
Not subject to CORS: the connection succeeded without a single CORS header, and the scripted server saw the page’s |
GWT-RPC, RequestFactory |
Subject to the policy, and always preflighted, because the content types ( |
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
Source: WHATWG Fetch — the CORS protocol
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:
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:
| Style | Server side | Client side |
|---|---|---|
GWT-RPC |
|
|
JSON over HTTP |
|
|
Events |
A stream of |
|
Cross-origin variant |
|
|
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
| Symptom | Cause | Fix |
|---|---|---|
|
A cross-origin request blocked by the browser: |
Check status 0 explicitly; serve the page and the API from one origin or add CORS headers on the server |
|
A network failure or a CORS block (the script cannot tell them apart); a 404 does not reject, it resolves with |
Open the console for the CORS message; test |
|
RPC servlets accept only POST |
Call it through the generated proxy; this is also the quick check that the servlet mapping exists |
Server log: |
A request that did not come from the GWT proxy, or a proxy in front of the servlet that rewrites the content type |
Send |
Server log: |
A client that does not send |
Keep the header end to end; override |
A cross-origin RPC call never reaches the service |
The preflight answers 200 with |
Add CORS in front of the servlet, with |
|
Cross-origin scripts see only safelisted headers |
Add the header name to |
JSONP |
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 |
It is routed to |
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 |
A WebSocket connects from any site |
WebSockets are not subject to CORS |
Compare the |
The callback runs on a widget that has been removed |
The call outlived the screen |
Cancel the |
Recent changes and where the book is outdated
| 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 |
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.