Search code examples
javamime-typescontent-typerestletmedia-type

Restlet Content Type Negotiation


It's common for APIs to support various forms of content negotiation: Accept HTTP header on the Request, Content-Type HTTP header on the Request (although I don't think conforms to standards), and deriving the expected type from the file extension. I'd like my Restlet REST API to support all of these, and I'm looking for help to figure out how to do that.

My first effort was to see if Restlet supported it out of the box. I'm using the Jackson extension, so I created a simple app which routed /foo to a ServerResource that returned a Map<String,Boolean>.

When I simply do an GET HTTP URL connection (a la https://stackoverflow.com/a/953697/27561) to /foo, I got back XML. I'd like this default to JSON. That's problem #1. How can I make the default type JSON?

When I make that same HTTP URL connection call to /foo.xml or /foo.json, I get a 404. That's problem #2. How can I use the file extension to denote the expected media type?

Since I'm running within a Servlet container, my current approach is to wrap the HttpServletRequest, and to set the Accept HTTP header to application/json if there was no file extension. That's a hack that is currently solving problem #1. However, I have been unable to extend this approach in a way that gets rid of the 404 in problem #2.

Restlet makes so many other things easy that I'm assuming I'm missing something — there's problem some configuration that I can tweak somewhere to make it Do The Right Thing. I've seen hints about that in the API documentation, but nothing obvious. So what's that configuration?


Solution

  • The simplest way to do that is to add a custom filter in front of the router of your application to do the following things:

    • Set a default media type if the list of accepted media types is empty (request.clientInfo.acceptedMediaTypes). The values of the header Accept are set here. It's important to set that before executing the server resource so this can be take into account during the conversion from bean to representation.

      Filter preferencesFilter = new Filter(getContext()) {
          protected int beforeHandle(Request request, Response response) {
              if (request.getClientInfo().getAcceptedMediaTypes().isEmpty()) {
                  request.getClientInfo().accept(MediaType.APPLICATION_JSON);
              } else if ((request.getClientInfo().getAcceptedMediaTypes().size() == 1)
                  && (request.getClientInfo().getAcceptedMediaTypes().get(0).getMetadata().equals(MediaType.ALL))) {
                  request.getClientInfo().accept(MediaType.APPLICATION_JSON);
              }
              return super.beforeHandle(request, response);
          }            
      }
      

      For information, when specifying no accept media type, Restlet uses the first converter of the registered ones to actually build the response content. It seems that it's the XML one in your case.

    • Detect provided extension to deduce in your case the corresponding accepted media types. The TunnelService of Restlet allows to preprocess the request to support features like using extensions for content negotiation. You can simply configure this as described below:

      public class MyApplication extends Application {
          public MyApplication() {
              getTunnelService().setExtensionsTunnel(true);
          }
      
          @Override
          public Restlet createInboundRoot() {
              (...)
          }
      }
      

    Otherwise, you shouldn't rely to the servlet API when using Restlet. The servlet extension should only be seen as an adapter to embed a Restlet application within a servlet container...

    It added a sample project for your use case at the following address: https://github.com/templth/restlet-stackoverflow/tree/master/restlet/test-restlet-conneg.

    Hope it helps you, Thierry