As we’re rolling out Resteasy 3.0, we have to pass the JAX-RS TCK. The good thing about this is that the TCK has grown massively is size and has a lot more test coverage for all old and new features of JAX-RS. It allowed me to uncover a few bugs I would not have found without the TCK. An unfortunate downside the TCK also got a lot stricter in some of the weak areas of the JAX-RS specification, particularly the request dispatching algorithm. I’ll be blunt, the algorithm is poor. IMO, the old spec leads made a huge mistake in introducing implementation details to the specification and now we have a poor algorithm we are stuck with. Us vendors cannot innovate and improve it because the TCK has backed us into a corner and the licensing fine print of Java EE makes it really hard for us to ship things that diverge from the spec. Here are a bunch of problems that used to work in Resteasy, but will no longer work because the TCK tests every fine detail of the JAX-RS matching algorithm.
- The @Path annotation at the class level is matched first before matching any resource methods. Only classes with the best and exact regular expressions are picked. Then the rest of the request is matched with remaining methods. So this won’t work anymore with a spec compliant algorithm:
Request: OPTIONS /foo @Path("/foo") public class Foo { @GET public String get() {...} } @Path("/{.*}") public class OptionsDefault { @OPTIONS public String options() {...} }
Earlier versions of Resteasy would match OptionsDefault.options(). Now, this method will not match according to the spec rules and you’ll get the default JAX-RS OPTIONS behavior.
- Locators are never resolved if there are resource methods that match the request. For example
PUT /foo/sub @Path("/foo") public class Foo { @GET @Path("sub") public String get() {...} @Path("{id}") public Locator locator() { return new Locator(); } } public class Locator{ @PUT public void put() {...} }
You’d think that the request would resolve to Locator.put() but you’d be wrong! Because there is a resource method whose path matches the request, but not the method you’d get a 405 response from the server. What’s interesting if you flip the expressions, a PUT request would work, but a GET request wouldn’t!
PUT /foo/sub @Path("/foo") public class Foo { @GET @Path("{id}") public String get() {...} @Path("sub") public Locator locator() { return new Locator(); } }
- It is possible to have poorer matches
GET /fart Accept: text/plain @GET @PATH("foo") @Produces("text/plain") public String get1() {} @GET @Path("{text}") @Produces("text/plain") public String get2() {} @GET @Path("f{text}") @Produces("text/*") public String get3() {}
You would think that GET /fart would match the get3() method because it is more specific path, but you’d be wrong. Because get3() has a less specific @Produces get2() would match. This is weird because the spec originally tells you to sort expressions on a best-match basis but then ditches this information to match Accept headers.
Another related note is the default returned media type.Right now the default is dependent on the deployment. If there is no Produce header, then the returned media type defaults to a union of the Accept header and explicit media types of all available MessageBodyWriters. There goes your portability! Instead, implementations should be allowed to specify their own default or even make it configurable. But, of course we can’t do that!
Granted some of these issues are edge cases, but IMO, some are not. The specification has 2 pages on english/pseudo-academic algorithm syntax to describe this very complex, but poor algorithm. Users will get frustrated trying to understand it. The experts themselves argued for days on interpretation of the specification. Users will scratch there head wondering why certain classes will match and some won’t and blame the vendor’s implementation. Resteasy had at least 4 user-reported regression tests that failed as a result of following the specfication matching algorithm religiously. I know these users will be back complaining that Resteasy 3.0 does not work for them when Resteasy 2.3.x did.
Resteasy 3.0-beta-6 Released – Closer to Compliance | Bill the Plumber
May 29, 2013 @ 23:42:31
May 30, 2013 @ 09:44:01
Hi Bill,
As much as I can understand that you do not like the JAX-RS algorithm, I have a few comments:
– The so-called “poor” algorithm is there since JAX-RS 1.0. Unlike me, you’ve been part of JAX-RS EG from it’s inception and you had a chance to influence it back than. (I know that the algorithm description is complex, but it has to be precise to ensure compatibility of implementations and you are considered to be an expert in the field, who should be able to deal with complex specification formulations.)
– The new TCK is merely testing that JAX-RS implementations complain with the original JAX-RS 1.0 algorithm. This is to ensure portability for the users, not to “back you into a corner”. If you say that things which used to work in RestEasy will not work anymore, you should at least admit that those things were NEVER compatible with JAX-RS specification.
– The RestEasy users who have developed their applications on RestEasy and relied on its incompatible matching algorithm implementation and now are reporting the regressions would be most likely very unpleasantly surprised if they tried to switch to a different JAX-RS runtime with an assumption that they are relying on portable JAX-RS features. The regressions reported to you are no because of we would do something wrong in JAX-RS 2.0. It’s because you have implemented an incompatible JAX-RS 1.0 matching algorithm in RestEasy. IOW it is a problem caused by a bug in your code, not in JAX-RS spec. Blaming JAX-RS here is not fair.
In fact, I do think that the new extended TCK did us a big favor. We have uncovered a previously grey area. That is a first step to starting a discussion in JAX-RS EG on a topic whether we need to improve the matching algorithm, and if, then how should we do it in a way that does not break backward compatibility.
May 30, 2013 @ 13:44:48
All fair comments. I was less vocal on JAX-RS 1.0 because, well, I came late, and because I was new to the whole area. After having maintained a JAX-RS implementation for 4 years now, written a book on JAX-RS, and used JAX-RS to implement various services, I have a different perspective and confidence now.
FWIW, these weren’t regressions reported now. These were actual reported user problems I fixed 1, 2, even 3 years ago. I diverged from JAX-RS spec on matching partially *BECAUSE* of user reported problems. But to be fair, I had already diverged a bit anyways to improve matching performance (particularly ignoring the initial class-level scan). But the thing I disagree with is that us vendors are unable to diverge from the algorithm to accommodate additional matching use cases because of the TCK and licensing agreements. We are not allowed to *IMPROVE* this poor algorithm. This is how you’ve boxed us into a corner. Most users could care less about portability. They just want their app to work. I’ve done a number of Vendor XXX to JBoss conversions over the years to know that portability in Java EE is a myth. There are only levels of portability. This is one area of the specification that should have been less specific to allow a wide range of matching possibilities and to allow implementations to optimize matching as much as possible. Something like:
“Requests should match the most explicit expression possible, with resource methods taking precedence over locators. More Explicit expressions are defined by the number of literal characters, matching groups, and matching expressions they have. A 405 exception should be thrown if a path matches but there is no support for the HTTP method. 415 should be thrown is there is a method match, but no Accept header matching. Implementations should provide default OPTIONS support. For unresolved HEAD requests, JAX-RS implementations should default to the applications GET method.”
Instead, you have 2-3 pages of algorithm that is tough to understand for developers and impossible for most users.
Another thing that should not have bee required was default media types. Right now the default is dependent on the deployment. If there is no Produce header, then the returned media type defaults to a union of the Accept header and explicit media types of all available MessageBodyWriters. There goes your portability! Instead, implementations should be allowed to specify their own default or even make it configurable. But, of course we can’t do that now!
Sep 26, 2013 @ 09:02:40
Thank you for you explanation. I agree with you that the specification’s routing algorithm is poor and upgrading to RestEasy 3 broke our application that relied on your improvements. Any chance that RestEasy 3 will provide some non-compatibility option where you make the routing behave in a sane way? That would be much appreciated since implementing our needs according to the spec will get very convoluted (we want to be able to have plugins that add methods to already existing resources, and in RestEasy 2.3 this was as easy as decorating them with the appropriate @Path; now I am not sure how to do it but probably it involves catch-all methods and some custom routing, when we added to framework to avoid writing custom routing…)
Sep 26, 2013 @ 11:39:04
Go read the Migration guide:
http://docs.jboss.org/resteasy/docs/3.0.4.Final/userguide/html/Migration_from_older_versions.html
If that doesn’t help, bring it to the lists.