RequestBuilder, JSON & XML

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 with -sourceLevel 17, the Jakarta Servlet 6 server of the Bookshelf scenario run by Jetty 12.1.10, Chrome 155 as the browser, and the gwt-user 2.13.1 sources, checked on 2026-10-09. Almost everything here is client code: RequestBuilder, the JSON and XML readers and JsonpRequestBuilder are Java compiled to JavaScript, and under the hood they drive the browser’s XMLHttpRequest, JSON and DOMParser objects. The server appears as ordinary servlets that return JSON, JSONP, XML or an event stream, and the build needs only a few <inherits> lines in the module XML. Nothing is shared between client and server in this style; the contract between them is a text format, which is exactly why it is useful when the back end is not Java, and exactly why it needs more care than GWT-RPC. Every output block on this page was produced by a lab module compiled with the 2.13.1 compiler, served from a private copy of the Bookshelf scenario by Jetty on port 8933 (a second origin on port 8943 played the part of "another site"), and driven in headless Chrome, which posted its log back to the server; the curl outputs came from the same server, and the AutoBean details marked as probes from a small JVM program using the 2.13.1 jars. Messages are quoted as they were printed; where a statement comes from the Developer Guide or the 2.13.1 source only, the text says so.

GWT-RPC (see the GWT-RPC page) is the Java-to-Java answer to "how do I call the server". This page covers the other half of the Developer Guide chapter on server communication: the HTTP client classes, with JSON and XML as the data formats, plus the cross-site tricks around them. Use it when the server is not a GWT-RPC servlet: a REST API written in any language, a third-party service, a legacy endpoint, a file or feed that is only a URL.

What this page covers

Table 1. The tools of this page and where each one fits
Tool What it does Use it for

RequestBuilder (com.google.gwt.http.HTTP)

Sends any HTTP request through XMLHttpRequest and gives the response text, status and headers to a callback

Every same-origin or CORS call to a JSON, XML or text endpoint

JsonUtils, JSONParser (com.google.gwt.json.JSON)

Turn response text into JavaScript objects, an overlay type or a tree of JSONValue objects

Reading JSON without extra libraries

JsInterop native types plus the browser’s JSON.parse

A typed Java view of the parsed object, no copy and no parser code in the output

Reading JSON in new code

AutoBeanFactory and AutoBeanCodex (com.google.web.bindery.autobean.AutoBean)

Maps JSON to Java interfaces and back, on the client and on a JVM

Real Java beans on both sides of a JSON contract

JsonpRequestBuilder (com.google.gwt.jsonp.Jsonp)

Loads JSON through a dynamically inserted <script> tag, which is not blocked by the same-origin policy

Legacy cross-site data; the trade-offs are listed below

XMLParser (com.google.gwt.xml.XML)

Parses and builds a W3C DOM of an XML document

XML feeds and legacy services

Timer, EventSource, WebSocket

Polling, Server-Sent Events and bidirectional sockets

Getting data from the server without a user action

The decision between all of these and GWT-RPC, RequestFactory and the REST libraries is made on the Client–Server Communication page and on the REST & JSON Libraries page; this page stays at the level of the classes GWT itself ships.

RequestBuilder

The module and the classes

RequestBuilder lives in com.google.gwt.http.client. The module com.google.gwt.http.HTTP is inherited by com.google.gwt.user.User, so a normal application already has it; a library module that only inherits com.google.gwt.core.Core must inherit it explicitly.

<inherits name="com.google.gwt.http.HTTP" />
Table 2. The types of the HTTP module
Type Role

RequestBuilder

Collects method, URL, headers, user, password, timeout, body and callback, then sends. Constants GET, POST, PUT, DELETE and HEAD (type RequestBuilder.Method)

RequestCallback

Two methods: onResponseReceived(Request, Response) and onError(Request, Throwable)

Request

The handle returned by sendRequest: cancel() and isPending()

Response

Abstract: getStatusCode(), getStatusText(), getText(), getHeader(String), getHeaders(), getHeadersAsString(), plus the SC_* constants

RequestException

Checked exception thrown synchronously when the request cannot be started

RequestPermissionException

Subclass of RequestException: the browser refused to open the URL

RequestTimeoutException

Subclass of RequestException, delivered to onError when the timeout expires

URL, UrlBuilder

Encoding and decoding of URLs and components

A first request

The Bookshelf catalogue is published by a servlet as JSON at /api/books?start=0&length=2. This is the complete request, with the response read in the callback.

RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, BASE + "api/books?start=0&length=2");
builder.setHeader("Accept", "application/json");
builder.setTimeoutMillis(5000);
try {
  builder.sendRequest(null, new RequestCallback() {
    @Override
    public void onResponseReceived(Request request, Response response) {
      log("GET status=" + response.getStatusCode() + " text=" + response.getStatusText()
          + " type=" + response.getHeader("Content-Type"));
      String body = response.getText();
      // parse body: see "Reading JSON"
    }

    @Override
    public void onError(Request request, Throwable exception) {
      log("onError " + exception.getClass().getName() + ": " + exception.getMessage());
    }
  });
} catch (RequestException e) {
  log("sendRequest threw " + e.getClass().getName() + ": " + e.getMessage());
}

Measured output of the callback:

GET status=200 text=OK type=application/json;charset=utf-8

Three things in that example are easy to get wrong, and the rest of this section is about them.

  • The call returns at once. The code after sendRequest runs before the response arrives, and GWT offers no synchronous variant (the Developer Guide explains that a synchronous call would freeze the single browser thread).

  • onResponseReceived is called for every response the browser produces, including 404 and 500. onError is not "the call failed"; it is "the timeout expired" (and, as shown below, almost nothing else).

  • getText() returns a String. Choosing and running a parser is your job.

Methods

GET, POST, PUT, DELETE and HEAD are constants of RequestBuilder. The release notes of 2.0.1 list the fix "RequestBuilder restricted to GET and POST", so the older statement that only those two work is obsolete. For a method without a constant, such as PATCH, the class has a second constructor that takes the method as a String, but it is protected: write a small subclass of RequestBuilder and call super("PATCH", url). The 2.13.1 source documents the constructor but the Developer Guide does not mention it, and the lab did not exercise it, so check your server’s handling of the method in a test before relying on it.

Pass the request body to sendRequest(String requestData, RequestCallback callback), or call setRequestData(String) and setCallback(RequestCallback) and then send(); sendRequest does not remember the data or callback it was given, while send() uses the stored ones. A body is a String: serialise JSON or XML yourself, and see below for the encoders.

Building the URL

RequestBuilder takes a URL that is already encoded. The URL class has the encoders: URL.encode(String) for a whole URL (it leaves the reserved characters of URL syntax alone), URL.encodeQueryString(String) for the value of a query parameter, and URL.encodePathSegment(String) for a path segment. UrlBuilder assembles a URL from parts and encodes each one.

String url = BASE + "api/books?author=" + URL.encodeQueryString(author) + "&start=" + start;

Concatenating user input into a URL without encoding it is the client-side half of query injection (an & in the author name adds a parameter). The server must still validate every parameter, because anyone can send any URL to it without your client.

Headers

setHeader(name, value) sets one request header and replaces an earlier value of the same name; both arguments must be non-empty. There is one trap in the source of setHeaders: if you set no header at all, GWT adds Content-Type: text/plain; charset=utf-8; if you set at least one header, it adds nothing, and the browser chooses the content type from the body, which for a String is text/plain;charset=UTF-8. So a POST with a JSON body needs Content-Type: application/json set explicitly the moment you add any other header, such as a CSRF token. The lab made this visible: with only the token header set, the Bookshelf servlet, which insists on JSON, answered 415 and said what it had seen (full listing in the CSRF section below).

POST token but no Content-Type -> 415 Unsupported Media Type ; server says: application/json required, got text/plain;charset=utf-8

The browser keeps some headers for itself (the "forbidden request-header" list of the Fetch standard: Host, Content-Length, Cookie, Origin and others), so do not expect setHeader to control them. Custom headers on a cross-origin request turn it into a preflighted request, as measured in the CORS section.

Timeouts

setTimeoutMillis(int) sets a timer; 0 (the default) means no timeout, and a negative value throws IllegalArgumentException("Timeouts cannot be negative"). When the timer fires, GWT aborts the XMLHttpRequest and calls onError with a RequestTimeoutException, whose message and getters are:

timeout onError com.google.gwt.http.client.RequestTimeoutException: A request timeout has expired after 300 ms

(The lab asked the server to wait 1500 ms with a 300 ms timeout.) The request is aborted in the browser, but the server may keep working on it; a non-idempotent request that timed out may still have been applied, so do not blindly repeat it. RequestTimeoutException.getRequest() and getTimeoutMillis() give the details.

Status codes are not errors

The lab asked for a 404 and a 500. Both arrived in onResponseReceived:

404 reached onResponseReceived status=404 text=Not Found
500 reached onResponseReceived status=500 content-type=text/html;charset=iso-8859-1

Notice the second line: the error body of a servlet container is HTML, not your JSON. Check getStatusCode() (and ideally the Content-Type) before parsing, as the wrapper in the worked example does. Response has SC_* constants for the usual codes, so write Response.SC_OK rather than 200, and remember that success is the whole 2xx range (a 201 Created or 204 No Content is not SC_OK).

Status 0: "no response"

When the browser produces no HTTP response at all (the server is down, DNS fails, the connection is reset, or a cross-origin request is blocked), the XMLHttpRequest still reaches its DONE state with status 0 and an empty statusText. GWT delivers that to onResponseReceived, not to onError, with an empty body. This is the single most surprising behaviour of the class, and the Developer Guide’s comment in the sample (the onError branch is "could be timeout, SOP violation, etc.") does not match it. Measured with a request to another origin that sends no CORS headers:

cross-origin onResponseReceived status=0 text='' body.length=0

So the success test in a real application is "status in 200..299", and status 0 is a distinct, user-visible case: "cannot reach the server or the request was blocked". The browser console says which, but the page cannot read it: for security reasons JavaScript is deliberately not told why a cross-origin request failed.

Failing before the request starts

sendRequest can throw instead of calling back. RequestException covers failures to start; the subclass RequestPermissionException is thrown when XMLHttpRequest.open itself fails, which is what a URL that the browser cannot parse does:

malformed url -> com.google.gwt.http.client.RequestPermissionException: The URL http://[bad is invalid or violates the same-origin security restriction

The message mentions the same-origin policy because GWT cannot tell the cases apart. Handle both channels: a catch around sendRequest and the two callback methods.

Cancelling

sendRequest returns a Request. isPending() is true while the browser is working on it; cancel() clears the state-change handler, aborts the XMLHttpRequest and stops the timeout timer, without calling either callback. Use it when the user navigates away from the screen that asked, or to drop a stale search. Measured: the lab started a request that the server would answer after 500 ms, cancelled it, and waited 900 ms.

cancel: pending before=true
cancel: pending after=false
cancel: 900 ms later no callback was invoked

The server may still process a cancelled request, so cancelling is a client-side courtesy and not a way to undo a state-changing call.

User, password and credentials

setUser(String) and setPassword(String) pass HTTP basic credentials to XMLHttpRequest.open; putting a password in client code means every visitor can read it, so they are only useful with credentials typed by the user at run time. Cookies of the same origin are always sent. For a cross-origin request, setIncludeCredentials(true) sets withCredentials so that the browser also sends cookies and accepts Set-Cookie; it needs the server’s Access-Control-Allow-Credentials: true and a specific (not *) Access-Control-Allow-Origin. (The 2.5.1 release notes call this method withCredentials(); the method in 2.13.1 is setIncludeCredentials.)

CORS in practice

The same-origin policy blocks a script from reading a response from another scheme, host or port. CORS (Cross-Origin Resource Sharing) is the opt-in: the server answers with Access-Control-Allow-Origin, and for requests that are not "simple" the browser first sends an OPTIONS preflight. Nothing in RequestBuilder is CORS-specific, because the browser does all of it. The lab ran three cross-origin calls from the page on port 8933 to a server on port 8943:

Table 3. Cross-origin behaviour of RequestBuilder in Chrome (measured)
Request Server Result in the callback

GET, no custom header

No CORS headers at all

onResponseReceived, status 0, empty text

GET, no custom header

Access-Control-Allow-Origin: http://localhost:8933

status 200 OK

GET with X-CSRF-Token (preflighted)

Answers OPTIONS with Access-Control-Allow-Headers: X-CSRF-Token

status 200 OK

GET with X-CSRF-Token (preflighted)

OPTIONS answers 405

onResponseReceived, status 0, empty text

cross-origin onResponseReceived status=0 text='' body.length=0
CORS simple GET -> 200 OK
CORS preflighted GET (custom header) -> 200 OK
CORS preflight rejected by server -> 0 ''

CORS is configured on the server (see CORS for the headers, and REST APIs with Spring Boot for the Spring side). Never answer Access-Control-Allow-Origin: * together with credentials, and never reflect the request’s Origin header without checking it against a list. Two simpler designs avoid CORS altogether: serve the API from the same origin as the page (a reverse proxy in front of both), or call your own server, which fetches the remote data (the "proxy on your own server" of the Cross-site Requests tutorial).

A wrapper that treats non-2xx as failure

Most applications wrap RequestBuilder once, so that callers see one AsyncCallback like the one GWT-RPC uses and never touch status codes. This is the Bookshelf wrapper; it keeps the "status 0" case distinct and lets parse errors reach the same onFailure.

public final class JsonClient {
  public interface Parser<T> {
    T parse(String text);
  }

  private static final int TIMEOUT_MILLIS = 10_000;

  private JsonClient() {}

  public static <T> Request get(String url, Parser<T> parser, AsyncCallback<T> callback) {
    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
    builder.setHeader("Accept", "application/json");
    builder.setTimeoutMillis(TIMEOUT_MILLIS);
    try {
      return builder.sendRequest(null, new RequestCallback() {
        @Override
        public void onResponseReceived(Request request, Response response) {
          int status = response.getStatusCode();
          if (status == 0) {
            callback.onFailure(new HttpStatusException(0, "No response: network error, CORS or aborted"));
          } else if (status < 200 || status >= 300) {
            callback.onFailure(new HttpStatusException(status, status + " " + response.getStatusText()));
          } else {
            T value;
            try {
              value = parser.parse(response.getText());
            } catch (RuntimeException e) {
              callback.onFailure(e); // malformed body
              return;
            }
            callback.onSuccess(value);
          }
        }

        @Override
        public void onError(Request request, Throwable exception) {
          callback.onFailure(exception); // timeout
        }
      });
    } catch (RequestException e) {
      callback.onFailure(e); // could not even start
      return null;
    }
  }
}

Measured with Parser being text -> (BookPageDto) NativeJson.parse(text):

JsonClient success total=25 first=The Pragmatic Programmer
JsonClient 404 -> HttpStatusException: 404 Not Found
JsonClient cross-origin -> HttpStatusException: No response: network error, CORS or aborted network=true
JsonClient bad body -> JavaScriptException: (SyntaxError) : Unexpected end of JSON input

The callback.onSuccess call is outside the try on purpose: an exception thrown by the caller’s success code must not be reported as a parse failure. Exceptions that escape a callback are not caught by RequestBuilder: the browser event handler is wrapped with GWT’s $entry (visible in the XMLHttpRequest source), so they reach the handler installed with GWT.setUncaughtExceptionHandler.

Reading JSON

JSON is the data format of this page: strings, numbers, booleans, null, arrays and objects (RFC 8259). Because it is a subset of the JavaScript literal syntax, the browser has a native parser, and every GWT technique below is a way of calling it and giving the result a Java type. The Developer Guide calls them "JSON types", "overlay types" and, for the cross-site case, "mashups with JSON and JSNI".

The module for the JSON classes is not part of User; inherit it where you use JSONParser and friends. JsonUtils lives in Core and needs no extra module.

<module rename-to="rblab">
  <inherits name="com.google.gwt.user.User" />
  <inherits name="com.google.gwt.http.HTTP" />
  <inherits name="com.google.gwt.json.JSON" />
  <inherits name="com.google.gwt.xml.XML" />
  <inherits name="com.google.gwt.jsonp.Jsonp" />
  <inherits name="com.google.web.bindery.autobean.AutoBean" />

  <source path="client/http" />
  <entry-point class="com.example.bookshelf.client.http.RbLab" />
</module>
flowchart TD start["Response text from RequestBuilder"] trust{"Who produces the text?"} bad["Do not use eval-based readers: parseLenient, unsafeEval, JSONP from a stranger"] shape{"What do you need?"} tree["JSONParser.parseStrict: JSONValue tree, no types needed"] overlay["JsonUtils.safeEval: JavaScriptObject overlay with JSNI getters, legacy"] native["JsInterop native type plus JSON.parse: typed fields, new code"] beans["AutoBeanCodex.decode: Java interfaces, also on a JVM"] start --> trust trust -- "my server, or any server" --> shape trust -- "text I would pass to eval" --> bad shape -- "explore unknown or varying structure" --> tree shape -- "old code, official tutorials" --> overlay shape -- "fixed contract, smallest output" --> native shape -- "Java beans on client and server, shared tests" --> beans

The diagram’s left branch repeats the rule that governs this whole section: the text of a response is untrusted input, even when it comes from your own server, because the server may be compromised, a proxy may rewrite it, or a stored value may have been written by an attacker. Only parsers that cannot execute code are acceptable.

JsonUtils

com.google.gwt.core.client.JsonUtils is a class of static helpers.

Table 4. JsonUtils methods
Method Behaviour in the 2.13.1 source

<T extends JavaScriptObject> T safeEval(String json)

Calls the browser’s JSON.parse and returns the result as an overlay type; throws IllegalArgumentException("Error parsing JSON: ...") followed by the payload when the text is invalid. The Javadoc requires the payload to be an object or an array, not a primitive or a string

unsafeEval(String json)

Escapes a list of problematic characters and calls eval('(' + ... + ')'); "should only be used on JSON from trusted sources"

boolean safeToEval(String text)

An RFC 4627 regular-expression check; the Javadoc itself says that safeEval is "safer, faster" and works on large texts, and that this one may fail on large texts in Chrome

String stringify(JavaScriptObject) and stringify(JavaScriptObject, String space)

Calls JSON.stringify

String escapeValue(String)

Returns a quoted, escaped JSON string; use it when you must build JSON by hand

String escapeJsonForEval(String)

Escapes characters that eval treats as line terminators; used by the eval-based methods

The Developer Guide (and, copied from it, much third-party material) describes safeEval as "evaluates a JSON expression" with the comment "JSON String that you trust", which suggests eval. In the 2.13.1 source it is JSON.parse inside a try, which is the safe choice; the stale wording is repeated as an outdated item at the end.

JSONParser and the JSONValue tree

com.google.gwt.json.client.JSONParser produces a tree of JSONValue objects: JSONObject, JSONArray, JSONString, JSONNumber, JSONBoolean and JSONNull. Each JSONValue has isObject(), isArray(), isString(), isNumber(), isBoolean() and isNull(), which return the typed view or null when the value is of another kind, so a chain of them is also your validation.

Table 5. JSONParser entry points
Method Behaviour

parseStrict(String)

Uses the browser’s JSON.parse; use this one

parse(String)

@Deprecated; calls parseLenient

parseLenient(String)

@Deprecated; escapes and then calls eval, which "can execute arbitrary script"

null input throws NullPointerException, an empty string IllegalArgumentException("empty argument"), and bad syntax a JSONException whose message starts with Error parsing JSON: (the lab lines are in the pitfalls table).

JSONObject obj = JSONParser.parseStrict(body).isObject();
JSONArray arr = obj.get("books").isArray();
log("parseStrict total=" + (int) obj.get("total").isNumber().doubleValue() + " second="
    + arr.get(1).isObject().get("title").isString().stringValue());
parseStrict total=25 second=Effective Java

JSONNumber holds a double, as JavaScript does: a 64-bit integer above 2^53 loses precision, and a database bigint key should travel as a JSON string. JSONObject.get(key) returns null for an absent key; calling a method on it throws NullPointerException, so the chain above is only safe for a payload whose shape you have validated.

The real cost of the tree is not speed but code: every field access is a method call, a null test and an unboxing. It pays when the structure varies (a generic viewer, a debugging tool), and in a pinch for one or two fields of a payload you otherwise ignore.

The eval parser really does execute code

The Security article warns that JSONParser.parse ends in eval. The lab proves it harmlessly: the "attacker" JSON assigns a global flag instead of stealing anything.

String hostile = "{\"total\":(window.__pwned=1)}";
try {
  JsonUtils.safeEval(hostile);
} catch (IllegalArgumentException e) {
  log("safeEval(hostile) -> rejected, pwned=" + pwned());
}
try {
  JSONParser.parseStrict(hostile);
} catch (RuntimeException e) {
  log("parseStrict(hostile) -> rejected, pwned=" + pwned());
}
@SuppressWarnings("deprecation")
Object lenient = JSONParser.parseLenient(hostile);
log("parseLenient(hostile) -> parsed " + lenient + " pwned=" + pwned());
safeEval(hostile) -> rejected, pwned=false
parseStrict(hostile) -> rejected, pwned=false
parseLenient(hostile) -> parsed {"total":1} pwned=true

The lenient parser evaluated the assignment and happily returned an object; the strict ones refused the text. Replace every JSONParser.parse and parseLenient in an old code base with parseStrict; the only thing you lose is the tolerance for non-JSON such as single quotes, comments or unquoted keys, which your server should not be producing in the first place.

Overlay types: the legacy typed view

An overlay type is a Java class that extends JavaScriptObject and whose methods are JSNI one-liners reading properties of the JavaScript object underneath. It is what the Retrieving JSON tutorial teaches, and the JSNI & Overlay Types page describes the rules in detail: a protected no-argument constructor, final methods, no instance fields.

public class BookJso extends JavaScriptObject {
  protected BookJso() {}

  public final native String getTitle() /*-{ return this.title; }-*/;

  public final native int getYear() /*-{ return this.year; }-*/;
}

public class BookPageJso extends JavaScriptObject {
  protected BookPageJso() {}

  public final native int getTotal() /*-{ return this.total; }-*/;

  public final native JsArray<BookJso> getBooks() /*-{ return this.books; }-*/;
}
BookPageJso jso = JsonUtils.safeEval(body);
log("safeEval overlay total=" + jso.getTotal() + " first=" + jso.getBooks().get(0).getTitle());
safeEval overlay total=25 first=The Pragmatic Programmer

The compiler turns the getters into direct property reads (the overlay’s methods can be inlined, as the tutorial notes), so there is no per-object copy and no parser in the output beyond JSON.parse. JsArray<T>, JsArrayString, JsArrayInteger, JsArrayNumber and JsArrayBoolean wrap arrays. The drawbacks are the reason the tutorials' style is now legacy: it relies on JSNI, which the JsInterop page replaces; it is a GWT-only mechanism rather than part of the portable JsInterop specification; and nothing checks that the JSON really has a title.

JsInterop native types and JSON.parse

In new code, bind the JSON shape with a native JsInterop type. A native type has no implementation in Java: the compiler emits plain property accesses, so a DTO costs nothing at run time, and the JSON object itself is bound the same way.

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "JSON")
public final class NativeJson {
  private NativeJson() {}

  public static native <T> T parse(String text);

  public static native String stringify(Object value);
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookDto {
  public double id;
  public String title;
  public String author;
  public int year;
  public int copies;
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookPageDto {
  public int total;
  public BookDto[] books;
}
BookPageDto dto = NativeJson.parse(body);
log("JSON.parse dto total=" + dto.total + " year0=" + dto.books[0].year + " author0=" + dto.books[0].author);
log("JSON.stringify(dto.books[0]) = " + NativeJson.stringify(dto.books[0]));
JSON.parse dto total=25 year0=1999 author0=Andrew Hunt
JSON.stringify(dto.books[0]) = {"id":1,"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"copies":0}

Elemental2 ships the same binding (elemental2.core.Global.JSON, covered with fetch on the DOM & Elemental2 page), so a project that already depends on it can write Global.JSON.parse(text) and skip the hand-written NativeJson. Writing the three-line binding avoids a dependency for one function.

Three properties of this approach decide how you use it.

  • There is no validation. NativeJson.parse gives back whatever the server sent, cast to your type unchecked. A missing field is JavaScript undefined: a missing object field reads as null in Java, but a missing primitive does not read as 0. Measured with NativeJson.parse("{}") as a BookDto:

    missing primitives: year=undefined (== 0 ? false) title==null ? true id=undefined

    Validate the fields you depend on (Js.isTruthy, Js.isUndefined of jsinterop-base, or a range check) before using them in arithmetic.

  • Numbers are doubles. JavaScript has one number type, the double, and a GWT long is an emulated type rather than a JavaScript number, so declare ids as double (as BookDto.id is) or carry them as strings.

  • The text must be JSON. JSON.parse rejects anything else. A syntax error surfaces as a JavaScriptException whose message is the browser’s: (SyntaxError) : Unexpected end of JSON input.

Source: GWT Developer Guide — JavaScript: JsInterop (the JsInterop page of this section explains native types)

AutoBeans: Java interfaces for the JSON

When you want ordinary Java beans (getters and setters, List properties, no JavaScript types), the AutoBean framework generates the implementation and AutoBeanCodex converts to and from a JSON payload. It is the JSON layer that RequestFactory is built on, and it also runs on a plain JVM, so the same interfaces can be unit-tested on the server side. The RequestFactory & AutoBeans page covers the framework itself; here is its codec used on a RequestBuilder response.

public interface BookBean {
  long getId();
  void setId(long id);
  String getTitle();
  void setTitle(String title);
  int getYear();
  void setYear(int year);
}

public interface BookPageBean {
  int getTotal();
  void setTotal(int total);
  List<BookBean> getBooks();
  void setBooks(List<BookBean> books);
}

public interface BookshelfBeans extends AutoBeanFactory {
  AutoBean<BookBean> book();

  AutoBean<BookPageBean> page();
}
BookshelfBeans f = GWT.create(BookshelfBeans.class);
BookPageBean bean = AutoBeanCodex.decode(f, BookPageBean.class, body).as();
log("AutoBean total=" + bean.getTotal() + " first=" + bean.getBooks().get(0).getTitle()
    + " id1=" + bean.getBooks().get(1).getId());

AutoBean<BookBean> nb = f.book();
nb.as().setId(99);
nb.as().setTitle("Bookshelf \"quoted\"");
log("AutoBean encode = " + AutoBeanCodex.encode(nb).getPayload());
AutoBean total=25 first=The Pragmatic Programmer id1=2
AutoBean encode = {"title":"Bookshelf \"quoted\"","id":"99"}

The module is com.google.web.bindery.autobean.AutoBean; the factory is created with GWT.create on the client and AutoBeanFactorySource.create on a JVM (which needs gwt-user and gwt-dev on the class path). @AutoBean.PropertyName("book_title") maps a JSON name that is not a Java identifier, and it is asymmetric: put it on the getter and on the setter. Probed on a JVM with a bean that had it only on the getter, encode and decode worked but calling the unannotated setter threw java.lang.IllegalArgumentException: jsonTitle; with both annotated the payload was {"book_title":"X","id":"42"}. The same probe showed how the decoder treats an imperfect payload: a JSON property that is not in the interface is ignored, a missing int becomes 0, a missing List becomes null, and a long is accepted as a number ("id":42) or as a string ("id":"12345678901234567"). A syntactically invalid payload threw java.lang.RuntimeException: Could not parse payload on the JVM; the client-side message was not probed.

Look at the second output line: AutoBeanCodex encoded the long id as the string "99" (the 2.13.1 ValueCodex writes Long as a string and reads either a number or a string), and int as a number. That protects 64-bit ids from the double precision loss, and it is a contract detail your non-GWT server must honour; if the server sends and expects numbers, use int, double or String in the interface instead. Date is written as the millisecond value, as a string.

Sending JSON

Build the body with a JSON encoder, never by string concatenation: a title containing a quote or a backslash breaks the document, and a title containing ","admin":true,"x":" changes its meaning.

String viaDto = NativeJson.stringify(dto.books[0]);          // JsInterop object
String viaBeans = AutoBeanCodex.encode(nb).getPayload();      // AutoBean
String viaTree = new JSONObject().toString();                 // JSONObject tree
String quoted = JsonUtils.escapeValue("Bookshelf \"quoted\""); // one string literal

Send it with an explicit Content-Type: application/json; charset=utf-8 (see Headers above, and the CSRF example below), and have the server reject other types, which is also a CSRF defence.

JSONP

JSON with Padding is a workaround for the same-origin policy that predates CORS. The browser may load a <script> from any origin, so instead of XMLHttpRequest the client adds <script src="https://other.example/data?callback=handler">. The server answers with executable JavaScript, handler({...});, and the browser runs it, which calls the function the client registered. The Cross-site Requests tutorial (T Xsite) and the JSON and Mashups article (A JSON mash-ups) both build on it.

JsonpRequestBuilder

com.google.gwt.jsonp.client.JsonpRequestBuilder (module com.google.gwt.jsonp.Jsonp) hides the bookkeeping that the 2009 article spends most of its length on: unique callback names, the global registry, script tags that must be removed again. In the lab module JsonpRequestBuilder resolved even without an explicit <inherits> (the ClientBundle Resources module inherits Jsonp for external resources), but do not rely on that; inherit com.google.gwt.jsonp.Jsonp where you use it.

Table 6. JsonpRequestBuilder in the 2.13.1 source
Member Behaviour

requestObject(url, AsyncCallback<T extends JavaScriptObject>)

The JSON value must be an object or array; returns a JsonpRequest<T>

requestString, requestInteger, requestDouble, requestBoolean

The same for primitives

send(url) and send(url, AsyncCallback<Void>)

Fire-and-forget, optionally told when the script ran

setCallbackParam(String)

Name of the query parameter that carries the function name; default callback

setFailureCallbackParam(String)

Name of a second parameter carrying an error function; default none

setTimeout(int)

Milliseconds before a TimeoutException (default 10000)

setPredeterminedId(String)

Fixed id for the callback name, so the same resource loaded twice is shared (the callback is named __gwt_jsonp__.P<id>.onSuccess)

The parameters are annotated @IsTrustedResourceUri, so the SafeHtml checks described on the SafeHtml & XSS page flag a URL that was built from user input. JsonpRequest.cancel() removes a pending request (not with preset ids). Measured, with a servlet on the same origin and a plain Java server on another port:

JsonpRequestBuilder jp = new JsonpRequestBuilder();
jp.setTimeout(1500);
jp.requestObject(BASE + "api/books.js", new AsyncCallback<BookJso>() {
  @Override
  public void onFailure(Throwable caught) {
    log("jsonp same-origin failure " + caught);
  }

  @Override
  public void onSuccess(BookJso result) {
    log("jsonp same-origin ok title=" + result.getTitle());
  }
});
jsonp same-origin ok title=The Pragmatic Programmer
jsonp cross-origin ok title=From the other origin

The server side of JSONP

The server must wrap the JSON in a call to a function whose name comes from the request. That name is attacker-controlled text that you write into a script, so it is a code-injection point. The lab servlet accepts only a dotted identifier, prefixes the body with /**/ (a common hardening against content-sniffing abuse of JSONP endpoints), and sets X-Content-Type-Options: nosniff.

public class BooksJsonpServlet extends HttpServlet {
  private static final Pattern CALLBACK = Pattern.compile("[A-Za-z_$][A-Za-z0-9_$.]{0,63}");

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String callback = req.getParameter("callback");
    if (callback == null || !CALLBACK.matcher(callback).matches()) {
      resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "invalid callback");
      return;
    }
    Book first = CatalogRepository.INSTANCE.page(0, 1).getBooks().get(0);
    String json = "{\"id\":" + first.getId() + ",\"title\":" + BooksRestServlet.quote(first.getTitle()) + "}";
    resp.setContentType("text/javascript");
    resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
    resp.setHeader("X-Content-Type-Options", "nosniff");
    resp.getWriter().write("/**/" + callback + "(" + json + ");");
  }
}
curl -s -i 'localhost:8933/api/books.js?callback=showBook'
HTTP/1.1 200 OK
Content-Type: text/javascript;charset=utf-8
X-Content-Type-Options: nosniff
Content-Length: 58

/**/showBook({"id":1,"title":"The Pragmatic Programmer"});
curl -s -i 'localhost:8933/api/books.js?callback=alert(document.cookie)//'
HTTP/1.1 400 Bad Request
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html;charset=iso-8859-1

The Cross-site Requests tutorial’s PHP script does echo $callback; for the unchecked callback parameter, and the Retrieving JSON tutorial’s servlet prints the stock symbols into the JSON without escaping them. Both are useful for learning the mechanism and are injection holes in production: do not copy them.

When the request is rejected, the client is not told why. A <script> whose load fails fires no usable error to GWT, so a JSONP failure shows up as the timeout (the lab asked for a callback name the servlet refuses, with an 800 ms timeout):

jsonp rejected -> com.google.gwt.jsonp.client.TimeoutException: Timeout while calling http://localhost:8933/api/books.js

Pass setFailureCallbackParam if the service supports an error callback, and always set a short setTimeout (the default is ten seconds).

The trade-offs

Table 7. JSONP against CORS and a same-origin proxy
Aspect JSONP CORS with RequestBuilder Server-side proxy

Methods

GET only

Any

Any

Request headers

None (no CSRF header, no Authorization)

Any, with a preflight

Any, on your own origin

Error information

None: a timeout

Status 0 (still opaque), or the real status when the server allows

Real status

Trust placed in the remote site

Total: its response runs as your script

Data only: you parse it

Data only, and you can filter it

Cookies

Always sent to the remote site

Only with setIncludeCredentials(true) and server consent

Your own session

Works without the remote server’s cooperation

No, it must produce JavaScript

No, it must send CORS headers

Yes

Content Security Policy

Needs the remote host in script-src

Needs it in connect-src

Same origin

The security consequences follow from the first line of "trust":

  • A site you load JSONP from can run any code in your page: read the DOM, steal the session, rewrite the UI. The Security article says it plainly: with a <script> tag "you’re trusting their site". If the page handles anything private, do not use JSONP from a third party; use a server-side proxy.

  • Your own JSONP endpoint is readable by any page, so it must not return private data to a cookie-authenticated request. This is the "JSON hijacking" family (CVE-2007-2378: NVD describes it as "JavaScript Hijacking" of JSON exchanged without a protection scheme, a design issue with no fixed version): a hostile page includes your URL in a <script> and gets your data. Public data is fine; per-user data is not.

  • Never build the script URL from user input (@IsTrustedResourceUri exists to make the mistake visible).

New code should prefer CORS, or a same-origin API behind a reverse proxy, and JSONP should survive only for services that offer nothing else.

XML with XMLParser

XML is the older format and still the only one a feed, a SOAP-era service or an Atom endpoint offers. The Developer Guide page "Working with XML" documents the W3C DOM API in com.google.gwt.xml.client; the module is com.google.gwt.xml.XML (not part of User).

Table 8. The XML types
Type Role

XMLParser

parse(String) returns a Document; createDocument() makes an empty one; removeWhitespace(Node) removes whitespace-only text nodes; supportsCDATASection() is deprecated and always true

Document, Element, Node, NodeList, Attr, Text, CDATASection, Comment, ProcessingInstruction

The DOM tree, as in the W3C DOM Level 2

DOMException

Thrown by DOM operations

The lab parses the XML feed that a servlet returns from GET /api/books.xml?length=2.

<?xml version="1.0" encoding="UTF-8"?>
<catalog total="25">
  <book id="1" year="1999">
    <title>The Pragmatic Programmer</title>
    <author>Andrew Hunt</author>
  </book>
</catalog>
Document doc = XMLParser.parse(response.getText());
Element root = doc.getDocumentElement();
log("xml root=" + root.getTagName() + " total=" + root.getAttribute("total"));
NodeList books = doc.getElementsByTagName("book");
for (int i = 0; i < books.getLength(); i++) {
  Element book = (Element) books.item(i);
  Node title = book.getElementsByTagName("title").item(0).getFirstChild();
  log("xml book id=" + book.getAttribute("id") + " year=" + book.getAttribute("year")
      + " title=" + title.getNodeValue());
}

(Document, Element, Node and NodeList here are the com.google.gwt.xml.client types, which clash by name with com.google.gwt.dom.client; import only one set.)

xml root=catalog total=25
xml book id=1 year=1999 title=The Pragmatic Programmer
xml book id=2 year=2001 title=Effective Java

Navigation uses getDocumentElement(), getChildNodes(), getFirstChild(), getNextSibling(), getParentNode(), and the lookups getElementsByTagName(String) (in the browser implementation the name is matched in any namespace) and getElementById(String). getElementById only finds elements whose ID attribute is declared as such in a DTD; an attribute called id is not enough, as the Developer Guide points out. Text content is getNodeValue() on the Text child, or getData() on a Text node.

A malformed document does not throw in Chrome

XMLParser.parse is documented to throw when the text is not well-formed, and the Developer Guide’s example wraps it in catch (DOMException e). In the 2.13.1 source the browser implementation (XMLParserImplStandard) calls DOMParser.parseFromString and raises an error only when the root element is a parsererror in the Mozilla namespace. Chrome reports a parse failure by returning a document that contains an XHTML-namespaced parsererror element, so the check does not fire. Measured with <catalog><book></catalog>:

xml malformed -> NO exception; root=catalog parsererror elements=1

So after XMLParser.parse on text that did not come from your own validated code, test for the error element yourself.

Document doc = XMLParser.parse(text);
if (doc.getElementsByTagName("parsererror").getLength() > 0) {
  throw new IllegalArgumentException("Malformed XML");
}

(Keep the catch (DOMException) too: other implementations and some inputs do throw.)

Building and serialising XML

XMLParser.createDocument() gives an empty Document; build it with createElement, createTextNode and setAttribute, attach with appendChild, and serialise with toString(). The serialiser escapes the markup characters, which is the reason to build XML with the DOM and not with string concatenation.

Document out = XMLParser.createDocument();
Element order = out.createElement("order");
out.appendChild(order);
Element title = out.createElement("title");
title.appendChild(out.createTextNode("A & B <draft>"));
order.appendChild(title);
order.setAttribute("isbn", "x\"y");
log("xml built = " + out.toString());
xml built = <order isbn="x&quot;y"><title>A &amp; B &lt;draft&gt;</title></order>

XML security

The browser’s DOMParser does not fetch external entities or DTDs for the page’s benefit, so the client side has no XXE exposure comparable to a server-side parser; the server that consumes the XML your client posts still needs a hardened parser. The client-side risk is the same as for JSON: the values you read are untrusted text. Show them with setText or a Label, never setHTML, and use SafeHtml when markup is unavoidable. XML is also larger than JSON for the same data; unless a service forces it on you, prefer JSON.

State-changing requests: CSRF/XSRF

A request that changes data must prove that the page that sent it is your page. Cross-site request forgery (CSRF, called XSRF in the GWT documents) makes the victim’s browser send a request with the victim’s cookies from a hostile site; the server cannot tell it from a genuine one unless something an attacker cannot know or set travels with it. GWT-RPC has built-in protection in RpcToken and XsrfProtectedService, described on the GWT-RPC page and on the XSRF, RPC & Server Security page; for RequestBuilder and REST endpoints you apply the same idea by hand.

The defences, in the order to apply them:

  1. Do not change state on GET (nor on HEAD), and make the other methods the only way in.

  2. Send a token in a custom header that the server compares with a per-session secret. A custom header cannot be set by a cross-site form or <script>, and for a cross-origin XMLHttpRequest it triggers a preflight that only an origin you allowed can pass.

  3. Require Content-Type: application/json on JSON endpoints; a cross-site HTML form can only send application/x-www-form-urlencoded, multipart/form-data or text/plain.

  4. Use SameSite cookies (Lax or Strict) for the session cookie, as defence in depth.

  5. Authorise on the server for every call. A token proves the origin of a request, not the user’s right to make it.

How the token reaches the client matters. Give it to the page in the dynamic host page (the server renders the HTML, so it can write the token into a <meta> tag), or have the page fetch it once from an endpoint that only the same origin can read. Never put it in a URL, and never hard-code one in the source of the module. The lab uses the first way; the host page is produced by a servlet and the token is a random value stored in the session:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="CSRF_TOKEN_PLACEHOLDER">
    <title>Bookshelf</title>
    <script src="../rblab/rblab.nocache.js"></script>
  </head>
  <body><pre id="out"></pre></body>
</html>

(CSRF_TOKEN_PLACEHOLDER stands for the random value the servlet writes in each response.)

private static String metaContent(String name) {
  NodeList<Element> metas = Document.get().getElementsByTagName("meta");
  for (int i = 0; i < metas.getLength(); i++) {
    if (name.equals(metas.getItem(i).getAttribute("name"))) {
      return metas.getItem(i).getAttribute("content");
    }
  }
  return "";
}
String body = "{\"title\":\"Refactoring\",\"year\":1999}";
RequestBuilder full = new RequestBuilder(RequestBuilder.POST, BASE + "api/book");
full.setHeader("X-CSRF-Token", csrfToken);
full.setHeader("Content-Type", "application/json; charset=utf-8");
full.sendRequest(body, callback);

The servlet compares the header with the session token in constant time, checks the content type, and only then reads the body:

public class BooksWriteServlet extends HttpServlet {
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String sent = req.getHeader("X-CSRF-Token");
    String expected = tokenFor(req.getSession(true));
    boolean ok = sent != null && MessageDigest.isEqual(
        sent.getBytes(StandardCharsets.UTF_8), expected.getBytes(StandardCharsets.UTF_8));
    if (!ok) {
      resp.sendError(HttpServletResponse.SC_FORBIDDEN, "missing or invalid CSRF token");
      return;
    }
    String type = req.getContentType();
    if (type == null || !type.toLowerCase().startsWith("application/json")) {
      resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "application/json required, got " + type);
      return;
    }
    String body = new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
    // validate the JSON, authorise the user, then change the data
    resp.setStatus(HttpServletResponse.SC_CREATED);
    resp.setContentType("application/json");
    resp.getWriter().write("{\"saved\":true,\"bytes\":" + body.length() + "}");
  }
}

The three calls the lab made, in order: no header, token only, token and JSON content type.

POST no headers -> 403 Forbidden
POST token but no Content-Type -> 415 Unsupported Media Type ; server says: application/json required, got text/plain;charset=utf-8
POST token + JSON content type -> 201 Created {"saved":true,"bytes":35}

The second line is the headers trap again: setting the token header switched off GWT’s default Content-Type, so the browser sent text/plain.

Frameworks give you the server half. Spring Security’s CookieCsrfTokenRepository writes a cookie named XSRF-TOKEN and reads the token back from a request header named X-XSRF-TOKEN (or the parameter _csrf) by default; its default HttpSessionCsrfTokenRepository keeps the token in the session instead, and the default request handler also accepts X-CSRF-TOKEN. To use the cookie variant from GWT, the cookie must be readable by script (CookieCsrfTokenRepository.withHttpOnlyFalse() in the Spring documentation), and the client reads it with Cookies.getCookie("XSRF-TOKEN") (com.google.gwt.user.client.Cookies) and copies it into the header. Spring Security 6 and later add BREACH protection to the token value, so use the documentation for the version you run when you wire the two together. The Spring side is covered in Spring Security, and the OWASP cheat sheet below lists the other options.

Server push without GWT-RPC

A browser page cannot be called by the server; the client must open the channel. The GWT documents of the 2007-2013 era describe polling and a long-held request ("Comet"); today the alternatives are, in the order of cost:

Table 9. Ways to get server data without a user action
Technique How Strength Cost

Polling

A Timer sends a RequestBuilder request every n seconds

Works everywhere, trivial, cacheable

Latency up to n seconds, load on the server even when nothing changed

Long polling

The server holds the request until it has data

Low latency without new protocols

A held server thread (or async servlet) per client; reconnect logic

Server-Sent Events

EventSource receives a one-way text stream; the browser reconnects by itself

Simple, plain HTTP, auto-reconnect, event ids

One direction, no custom headers (cookies only), connection limits on HTTP/1.1

WebSocket

A bidirectional message channel

Lowest latency, both directions

A different protocol to secure (origin check, authentication, no CSRF header on the handshake) and to scale; proxies and load balancers must support it

The Delayed Logic & Scheduler page covers Timer and RepeatingCommand; the lab uses Timer.scheduleRepeating for polling:

final int[] count = {0};
new Timer() {
  @Override
  public void run() {
    RequestBuilder b = new RequestBuilder(RequestBuilder.GET, BASE + "api/books?start=0&length=1");
    try {
      b.sendRequest(null, new RequestCallback() {
        @Override
        public void onResponseReceived(Request request, Response response) {
          log("poll #" + (count[0] + 1) + " status=" + response.getStatusCode());
          if (++count[0] >= 3) {
            cancel(); // stops the timer
          }
        }

        @Override
        public void onError(Request request, Throwable exception) {
          log("poll error " + exception);
        }
      });
    } catch (RequestException e) {
      log("poll threw " + e);
    }
  }
}.scheduleRepeating(250);
poll #1 status=200
poll #2 status=200
poll #3 status=200

Good polling is gentle: do not start a new request while the previous one is pending (check Request.isPending()), stop when the page is hidden, add exponential back-off on status 0 or 5xx, and send If-None-Match or a "changed since" parameter so that an unchanged answer is a cheap 304.

Server-Sent Events need no library, only a JsInterop binding for the browser’s EventSource (no class in gwt-user wraps it; elemental2-dom 1.3.2 ships elemental2.dom.EventSource, and the hand-written binding below avoids the dependency):

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "EventSource")
public class EventSourceJs {
  @JsFunction
  public interface Listener {
    void onEvent(MessageEventJs event);
  }

  public EventSourceJs(String url) {}

  public native void addEventListener(String type, Listener listener);

  public native void close();
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "MessageEvent")
public class MessageEventJs {
  public String data;
  public String lastEventId;
}
EventSourceJs es = new EventSourceJs(BASE + "lab/events");
final int[] got = {0};
es.addEventListener("stock", ev -> {
  log("sse id=" + ev.lastEventId + " data=" + ev.data);
  if (++got[0] == 3) {
    es.close();
  }
});

The servlet writes text/event-stream and flushes after each event; here it sends three and closes:

resp.setContentType("text/event-stream");
resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
resp.setHeader("Cache-Control", "no-cache");
for (int i = 1; i <= 3; i++) {
  resp.getWriter().write("id: " + i + "\nevent: stock\ndata: {\"bookId\":" + i + ",\"copies\":" + (10 - i) + "}\n\n");
  resp.getWriter().flush();
  Thread.sleep(200);
}
sse id=1 data={"bookId":1,"copies":9}
sse id=2 data={"bookId":2,"copies":8}
sse id=3 data={"bookId":3,"copies":7}

Two details that the lab confirms and the specification explains: events with an event: name are delivered only to listeners registered for that name with addEventListener (the generic onmessage sees only unnamed events), and the data: payload is text, so parse it with one of the JSON readers above. If the stream ends, the browser reconnects after a delay unless you call close() (the specification’s rule); the lab closes the source in the third callback so that it does not start over. A held-open stream occupies a server thread in a classic blocking servlet, so a real endpoint uses asynchronous servlet processing or a framework that streams.

WebSockets use the browser WebSocket class. Elemental2 binds it as elemental2.dom.WebSocket and the DOM & Elemental2 page has a worked example, so it is not repeated here. Chat, video-conference signalling and similar bidirectional applications are the subject of the planned follow-up issue #264 in this documentation project, which treats WebSockets in depth; this page stays with the one-way and polling techniques that need nothing beyond plain HTTP. Secure a socket the way you secure any endpoint: check the Origin header during the handshake (the handshake carries cookies but you cannot add a CSRF header to it), authenticate the connection, authorise each message, and use wss://.

Worked example: the Bookshelf HTTP lab

The scenario for this section is the Bookshelf catalogue. Its server publishes JSON, JSONP, XML, an event stream and a protected write endpoint; its client is one entry point that runs every technique of this page in turn and prints a line per result, so that the page can quote real output. This is the whole picture of one call through the wrapper, with the failure paths that the page measured.

sequenceDiagram participant P as Presenter participant J as JsonClient participant RB as RequestBuilder participant X as XMLHttpRequest participant S as Bookshelf server P->>J: get(url, parser, callback) J->>RB: new RequestBuilder(GET, url) + Accept + timeout RB->>X: open, setRequestHeader, send alt malformed URL RB-->>J: RequestPermissionException thrown by sendRequest J-->>P: onFailure else the server answers X->>S: GET /api/books?start=0&length=2 S-->>X: 200 and JSON, or 404 and HTML, or no CORS headers X-->>RB: readyState DONE RB-->>J: onResponseReceived(status) alt status 0 J-->>P: onFailure(HttpStatusException 0) else status outside 200 to 299 J-->>P: onFailure(HttpStatusException status) else 2xx J->>J: parser.parse(text) J-->>P: onSuccess(dto) or onFailure(parse error) end else the timeout expires first RB-->>J: onError(RequestTimeoutException) J-->>P: onFailure end

The pieces

The lab is a private copy of the Bookshelf Maven project with one extra GWT module and a handful of servlets.

Table 10. Files of the lab
File Purpose

bookshelf-client/src/main/resources/com/example/bookshelf/RbLab.gwt.xml

The module shown above, rename-to="rblab"

client/http/RbLab.java

The entry point: runs the scenarios in sequence and posts the log back

client/http/JsonClient.java, HttpStatusException.java

The wrapper

client/http/NativeJson.java, BookDto.java, BookPageDto.java

JsInterop native types

client/http/BookJso.java, BookPageJso.java

Legacy overlay types

client/http/BookBean.java, BookPageBean.java, BookshelfBeans.java

AutoBeans

client/http/EventSourceJs.java, MessageEventJs.java

Server-Sent Events binding

bookshelf-server/.../BooksRestServlet.java (from the RPC scenario)

GET /api/books returns {"total":25,"books":[...]} with X-Content-Type-Options: nosniff

BooksJsonpServlet.java, BooksXmlServlet.java, BooksWriteServlet.java, LabServlet.java

JSONP, XML, the CSRF-protected POST /api/book, and the helper endpoints (/lab/slow, /lab/status, /lab/badjson, /lab/events, the dynamic host page /lab/page)

The servlet mappings are plain web.xml entries:

<servlet>
  <servlet-name>booksJsonp</servlet-name>
  <servlet-class>com.example.bookshelf.server.BooksJsonpServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>booksJsonp</servlet-name>
  <url-pattern>/api/books.js</url-pattern>
</servlet-mapping>
<servlet>
  <servlet-name>booksXml</servlet-name>
  <servlet-class>com.example.bookshelf.server.BooksXmlServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>booksXml</servlet-name>
  <url-pattern>/api/books.xml</url-pattern>
</servlet-mapping>
<servlet>
  <servlet-name>booksWrite</servlet-name>
  <servlet-class>com.example.bookshelf.server.BooksWriteServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>booksWrite</servlet-name>
  <url-pattern>/api/book</url-pattern>
</servlet-mapping>

Running it

mvn -B package -DskipTests
mvn -B -pl bookshelf-server org.eclipse.jetty.ee10:jetty-ee10-maven-plugin:12.1.10:run-war -Djetty.http.port=8933

Open http://localhost:8933/lab/page. The lab module needs the second origin only for the cross-origin cases (a 30-line com.sun.net.httpserver program on port 8943 that serves JSONP, a JSON endpoint without CORS headers, and two endpoints with CORS headers). For development, the same module runs in Super Dev Mode with mvn gwt:codeserver as described on the Development Mode & Debugging page; the lab used the production build because it is what a real deployment serves.

The complete log of the final run (51 lines, abbreviated here to the sequence of scenarios):

1 host page base = http://localhost:8933/lab/ module base = http://localhost:8933/rblab/
2 GET status=200 text=OK type=application/json;charset=utf-8
...
9 JsonClient success total=25 first=The Pragmatic Programmer
10 JsonClient 404 -> HttpStatusException: 404 Not Found
11 JsonClient cross-origin -> HttpStatusException: No response: network error, CORS or aborted network=true
...
22 404 reached onResponseReceived status=404 text=Not Found
...
26 CORS simple GET -> 200 OK
...
51 DONE

The Bookshelf wrapper is what the presenters call; nothing outside it looks at a status code.

The tutorials and the article, one by one

The Developer Guide’s three tutorials and the 2009 article build one story: serve JSON locally, read it, then fetch it from another site. They are still worth reading for the shape of the problem; this table says what to change when you follow them with 2.13.1.

Table 11. What each official document teaches and what has changed
Document Teaches Read it with these corrections

T JSON, "Retrieving JSON"

A servlet that returns an array of stock prices; JsonUtils.safeEval into an overlay type StockData extends JavaScriptObject; RequestBuilder with URL.encode; an error label for non-200 responses

It extends HttpServlet from javax.servlet and a web.xml with the Servlet 2.3 DTD: use jakarta.servlet and the Servlet 6 schema. Its servlet prints the symbols into the JSON unescaped: escape them. Its URLs on port 8888 belong to the old Dev Mode server; the 2.13 DevMode serves only static files, so call your application server on its own port. The overlay can be a JsInterop native type (above)

T Xsite, "Cross-site Requests"

The same-origin policy, the proxy alternative, and JsonpRequestBuilder against a PHP or Python JSONP server

The PHP script echoes the callback parameter without validation: never copy it (use the servlet above). CORS has replaced JSONP for new work

T JSONphp, "JSON php"

The JSON endpoint written as a PHP script instead of a servlet

The same lesson as the servlet: any server that returns JSON works; the script prints the symbols unescaped and uses header('Content-Type: text/javascript'), where JSON should be application/json

A "Using GWT for JSON mash-ups" (2009)

The three ways to consume JSON, the $wnd versus window pitfall of the module iframe, a hand-rolled script-tag loader

JsonpRequestBuilder is the library form of its loader. The $wnd/window bug is real for hand-written JSNI loaders (an array created in the parent window fails instanceof Array in the module frame), which is one more reason not to write them. Its example data sources are a 2009 API: do not reuse the URLs

Security notes

  • The response is untrusted input. Parse with JSON.parse-based readers (safeEval, parseStrict, NativeJson.parse), never with eval; validate fields before use; display text with setText, not setHTML; use SafeHtml for markup.

  • Third-party scripts are your code. JSONP, and any ScriptInjector call, give the other site full control of your page. Prefer CORS or a proxy that returns only data.

  • State-changing calls need a token and a content type. The header-token pattern above, with SameSite cookies and server-side authorisation on every call. Tokens come from the server at run time; none belongs in source code or in a URL.

  • Cross-origin is the server’s decision. Allow specific origins, never * with credentials, and answer the preflight only for the headers and methods you want.

  • Do not send secrets in the URL. Query strings are logged by servers and proxies and stored in browser history; use headers or the body, over HTTPS.

  • Server-side validation is the only validation. Anything the client checks can be bypassed with curl; the lab’s servlets validate the callback name, the content type and the token themselves.

  • Do not return private data in a JSONP or GET JSON endpoint that cookies alone protect. See the hijacking discussion in the JSONP section.

  • Errors may expose internals. The 500 page measured above is a container HTML page; configure error pages so that stack traces are not sent to the browser.

Pitfalls

Messages marked lab come from the Bookshelf lab above (Chrome 155, GWT 2.13.1, Jetty 12.1.10), probe from compiling or running a small program with the 2.13.1 jars, and source from the GWT 2.13.1 sources.

Table 12. Common problems with RequestBuilder, JSON, JSONP and XML
Symptom Cause Fix

No source code is available for type com.google.gwt.json.client.JSONParser; did you forget to inherit a required module? (and the same for JSONObject, JSONArray, com.google.gwt.xml.client.XMLParser, Document, com.google.web.bindery.autobean.shared.AutoBeanCodex) (probe)

The module inherits User and HTTP only; JSON, XML and AutoBean are separate modules

Add <inherits name="com.google.gwt.json.JSON" />, com.google.gwt.xml.XML, com.google.web.bindery.autobean.AutoBean

Error parsing JSON: SyntaxError: Unexpected end of JSON input followed on the next line by the payload, as IllegalArgumentException from JsonUtils.safeEval (lab)

The text is not complete or not JSON (truncated response, HTML error page, trailing comma)

Check status and Content-Type first; log the payload; fix the server

com.google.gwt.json.client.JSONException: Error parsing JSON: SyntaxError: Unexpected end of JSON input (lab)

The same text given to JSONParser.parseStrict

As above; catch JSONException

java.lang.IllegalArgumentException: empty argument (lab); NullPointerException for null (source)

JSONParser.parseStrict("") on an empty body, such as a 204

Test for a body before parsing

com.google.gwt.core.client.JavaScriptException: (SyntaxError) : Unexpected end of JSON input (lab)

NativeJson.parse (the browser JSON.parse) on invalid text

Catch RuntimeException around the parse, as JsonClient does

A DTO field is undefined; dto.year == 0 is false (lab: year=undefined (== 0 ? false) title==null ? true)

JSON without the property, read through a native type: no defaults are applied

Validate required fields, or use AutoBeans (missing int becomes 0, probe)

onError is never called for a 404 or 500; onResponseReceived is, with status=404 text=Not Found (lab)

onError is only for the timeout; HTTP statuses are responses

Branch on getStatusCode(), accept the 2xx range

onResponseReceived status=0 text='' body.length=0 for a call that "worked in curl" (lab)

No response: server down, DNS, TLS, or a cross-origin response without CORS headers or with a failed preflight

Open the browser console for the real reason; configure CORS on the server; treat status 0 as a distinct failure

com.google.gwt.http.client.RequestPermissionException: The URL http://[bad is invalid or violates the same-origin security restriction thrown by sendRequest (lab)

A URL that XMLHttpRequest.open rejects

Encode and validate the URL; catch RequestException around sendRequest

com.google.gwt.http.client.RequestTimeoutException: A request timeout has expired after 300 ms in onError (lab)

setTimeoutMillis(300) and a slow server

Raise the timeout or fix the server; do not repeat a non-idempotent request blindly

415 Unsupported Media Type and application/json required, got text/plain;charset=utf-8 (lab)

A header was set, so GWT did not add its default Content-Type, and the browser sent text/plain for the string body

Always set Content-Type: application/json; charset=utf-8 yourself on JSON bodies

403 Forbidden with missing or invalid CSRF token (lab)

The request has no token header, or a token from another session

Read the token from the dynamic host page and send it in a custom header

com.google.gwt.jsonp.client.TimeoutException: Timeout while calling http://localhost:8933/api/books.js (lab)

A JSONP request that the server refused (here 400 invalid callback) or that returned something other than a callback call

Check the server log; set a short setTimeout; JSONP gives no status

parsererror elements=1 and no exception after XMLParser.parse of malformed XML in Chrome (lab)

The 2.13.1 implementation only recognises the Mozilla parse-error namespace

Look for a parsererror element in the result

"id":"99" where the server expects a number (lab: AutoBeanCodex.encode output)

AutoBeanCodex writes a long as a JSON string

Use int, double or String in the interface, or accept strings on the server

java.lang.IllegalArgumentException: jsonTitle when calling a setter (probe, JVM)

@AutoBean.PropertyName is on the getter only; it is asymmetric

Annotate the getter and the setter

parseLenient ran attacker script: pwned=true (lab)

The eval-based parser executed an expression inside the "JSON"

Use parseStrict; remove JSONParser.parse and unsafeEval

A write that was cancelled or timed out was applied anyway (source)

Request.cancel() and the timeout abort the XMLHttpRequest in the browser; the server may already have processed the request

Make writes idempotent; send a request id the server can de-duplicate

Where older material is outdated

  • The Developer Guide’s JSON page describes JsonUtils.safeEval as a method that "evals" its input and shows only the overlay and JSONParser styles. In the 2.13.1 source safeEval is JSON.parse. The page does not mention parseStrict or the native-type style.

  • JSONParser.parse is @Deprecated; the guide and the 2007 book use it as the normal reader, and the book itself notes that it evaluates the text instead of parsing it. Use parseStrict.

  • The Developer Guide’s RequestBuilder example says onError handles "SOP violation": in the browser it is onResponseReceived with status 0 (measured above).

  • The XML page says XMLParser.parse throws DOMException for malformed input; in Chrome it returns a document with a parsererror element (measured above).

  • The tutorials use javax.servlet, Servlet 2.3 DTDs, Eclipse wizards, the removed Jetty-based development mode server and Google Data feeds. The structure is right and the code needs the changes in the table above.

  • The 2007 book (chapters 12 and 13) teaches RequestBuilder with only GET and POST, XMLParser, FormPanel and a JSON search component that parses with the eval-based JSONParser.parse, backed by a third-party search service and server proxies in Java, Perl and Ruby. Its explanation of the HTTP methods and of the proxy idea still holds; the parser, the GET/POST limit (lifted in GWT 2.0.1) and the example service do not. File uploads with FormPanel are covered on the widgets page and, for the server half, on the server integration page of this section.

  • The 2.5.1 release notes name a RequestBuilder.withCredentials() method; the method is setIncludeCredentials(boolean).

What changed recently

  • 2.0.1. RequestBuilder accepts methods other than GET and POST (release notes).

  • 2.5.1. RequestBuilder gained the credentials option for CORS (listed as withCredentials(), now setIncludeCredentials).

  • JSON. JSONParser.parse is deprecated in favour of parseStrict; JsonUtils.safeEval is JSON.parse in 2.13.1.

  • 2.13.x. The release notes fetched on 2026-10-08 list no change to com.google.gwt.http, json, jsonp or xml. These packages in 2.13.1 are what the 2010 documents describe, apart from the points above.

  • Beyond 2.13. The roadmap fetched on 2026-10-08 names no replacement for them; for new code, the browser’s own fetch through Elemental2 or one of the libraries on the REST & JSON Libraries page are the options when you do not want RequestBuilder.