OpenLiberty / open-liberty

Open Liberty is a highly composable, fast to start, dynamic application server runtime environment
https://openliberty.io
Eclipse Public License 2.0
1.15k stars 592 forks source link

Jakarta EE9:JAX-RS: Content-Type is not set causing Wink client to fail #18462

Closed jagraj closed 3 years ago

jagraj commented 3 years ago

Describe the bug JAX-RS application is failing after migrating from EE8 runtime to Jakarta EE9 with the RESTEASY runtime. My application endpoint defined with @Produces(MediaType.APPLICATION_XML) and @Consumes(MediaType.APPLICATION_XML) and at the client side I am getting entity as XML data type but we are getting content type as null.

Here is the exception from this failure.

[INFO    ] CWWJP9990I: [eclipselink] EclipseLink, version: Eclipse Persistence Services - 3.0.0.v202012081010
[INFO    ] registering MBean org.apache.cxf:bus.id=GarageSaleEJB-Server-Bus,type=Performance.Counter.Server,service="{http://session.gsdb.gs.svt.websphere.ibm.com/}GarageSaleStoreManagerService",port="GarageSaleStoreManagerPort",operation="getInventory": org.apache.cxf.management.counters.ResponseTimeCounter@1f68c759
[INFO    ] registering MBean org.apache.cxf:bus.id=GSjsf20LibertyWeb-Client-Bus,type=Performance.Counter.Client,service="{http://session.gsdb.gs.svt.websphere.ibm.com/}GarageSaleStoreManagerService",port="GarageSaleStoreManagerPort",operation="getInventory": org.apache.cxf.management.counters.ResponseTimeCounter@24ca7227
[err] java.lang.RuntimeException: A javax.ws.rs.ext.MessageBodyReader implementation was not found for class com.ibm.websphere.svt.gs.tax.entity.ShipRate type and application/octet-stream media type.  Verify that all entity providers are correctly registered.  Add a custom javax.ws.rs.ext.MessageBodyReader provider to handle the type and media type if a JAX-RS entity provider does not currently exist.
[err]   at org.apache.wink.client.internal.handlers.ClientResponseImpl.readEntity(ClientResponseImpl.java:134)
[err]   at org.apache.wink.client.internal.handlers.ClientResponseImpl.getEntity(ClientResponseImpl.java:76)
[err]   at org.apache.wink.client.internal.handlers.ClientResponseImpl.getEntity(ClientResponseImpl.java:67)
[err]   at org.apache.wink.client.internal.ResourceImpl.invoke(ResourceImpl.java:209)
[err]   at org.apache.wink.client.internal.ResourceImpl.get(ResourceImpl.java:306)
[err]   at com.ibm.websphere.svt.gs.gsjsfweb.utils.GarageSaleManagedBeanUtil.getShipRateByItemId(GarageSaleManagedBeanUtil.java:373)
[err]   at com.ibm.websphere.svt.gs.gsjsfweb.utils.GarageSaleManagedBeanUtil.calculateSubTotal(GarageSaleManagedBeanUtil.java:133)
[err]   at com.ibm.websphere.svt.gs.gsjsfweb.GarageSaleJSFActions.addToCart(GarageSaleJSFActions.java:428)
[err]   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[err]   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[err]   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[err]   at java.lang.reflect.Method.invoke(Method.java:498)
[err]   at org.apache.el.parser.AstValue.invoke(AstValue.java:247)
[err]   at [internal classes]
[err]   at jakarta.faces.component.UICommand.broadcast(UICommand.java:122)
[err]   at jakarta.faces.component.UIViewRoot._broadcastAll(UIViewRoot.java:1255)
[err]   at jakarta.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:420)
[err]   at jakarta.faces.component.UIViewRoot._process(UIViewRoot.java:1741)
[err]   at jakarta.faces.component.UIViewRoot.processApplication(UIViewRoot.java:935)
[err]   at org.apache.myfaces.lifecycle.InvokeApplicationExecutor.execute(InvokeApplicationExecutor.java:42)
[err]   at [internal classes]
[err]   at jakarta.faces.webapp.FacesServlet.service(FacesServlet.java:204)
[err]   at com.ibm.ws.webcontainer.servlet.ServletWrapper.service(ServletWrapper.java:1258)
[err]   at [internal classes]
[err]   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[err]   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[err]   at java.lang.Thread.run(Thread.java:748)

Steps to Reproduce

  1. Transform EE8 application to Jakarta EE9 and the app has valid annotations defined for both producer and consumer content type annotations.
  2. RESTEASY returning null content type and this is causing my application to fail after JEE9 transformation.

Expected behavior Content type header must be set based on the annotation defined for the JAX-RS resource.

Diagnostic information:

Additional context Add any other context about the problem here.

WhiteCat22 commented 3 years ago

The error message:

java.lang.RuntimeException: A javax.ws.rs.ext.MessageBodyReader implementation was not found for class com.ibm.websphere.svt.gs.tax.entity.ShipRate type and application/octet-stream media type.  Verify that all entity providers are correctly registered.  Add a custom javax.ws.rs.ext.MessageBodyReader provider to handle the type and media type if a JAX-RS entity provider does not currently exist.

tells me that the Wink client, which is sending a request from com.ibm.websphere.svt.gs.gsjsfweb.utils.GarageSaleManagedBeanUtil.getShipRateByItemId(GarageSaleManagedBeanUtil.java:373)

Doesn't have a MessageBodyReader that can de-serialize the ShipRate object returned by the JAX-RS endpoint.

I have reproduced this with jaxrs-2.0, jaxrs-2.1 and restfulWs-3.0.

The solution is to register a MessageBodyReader that can de-serialize ShipRate with the client.

WhiteCat22 commented 3 years ago

I manually forced my client to specify application/xml for both Content-Type and Accept headers, and the server side does not have a MessageBodyWriter (without enabling the EE9 JAX-B feature) as well.

jhanders34 commented 3 years ago

Adam he has a MessageBodyReader that can de-serialized ShipRate when receiving application/xml. When I looked at the wink code it does not look at the Consumes state if the Content-Type header is not set. CXF does look at the Consumes MediaType if Content-Type is not set. It appears that CXF does set the Content-Type header when sending back the response, but RestEasy does not. At least that is how I understood the problem when working with Jag on it.

WhiteCat22 commented 3 years ago

Ok, that's good to know, thanks Jared.

jhanders34 commented 3 years ago

So from what I can tell for this issue, the RestEasy code would need to be updated to return a Content-Type depending on the produced content. Either that or the application needs to be updated to set the Content-Type and we would need to document that as a limitation with restulfulWS-3.0. Jag, did the workaround to set the Content-Type work for you?

jagraj commented 3 years ago

I did try workaround and that did not help. Some how we are still not getting headers from the response. Here is the info that we got from the trace.

EE9:

[9/14/21, 15:34:18:889 CDT] 00000037 id=00000000 org.apache.wink.client.internal.log.Responses                1 handle The received response headers:
Content-Language              en-US
Content-Length                0

EE8:

[9/14/21, 16:05:16:354 CDT] 00000035 id=00000000 org.apache.wink.client.internal.log.Responses                1 handle The received response headers:
Content-Language              en-US
Content-Length                186
Content-Type                  application/xml
Date                          Tue, 14 Sep 2021 21:05:16 GMT
X-Powered-By                  Servlet/4.0
WhiteCat22 commented 3 years ago
[9/14/21, 16:21:54:698 CDT] 00000039 id=9e7c3cb6 iberty.restfulWS.internal.globalhandler.RESTfulWSHandlerImpl < execute Exit 
[9/14/21, 16:21:54:698 CDT] 00000039 id=9e7c3cb6 iberty.restfulWS.internal.globalhandler.RESTfulWSHandlerImpl < filter Exit 
[9/14/21, 16:21:54:698 CDT] 00000039 id=00000000 com.ibm.ws.webcontainer.srt.SRTServletResponse               > setStatus ENTRY  status --> 204 [com.ibm.ws.webcontainer40.srt.SRTServletResponse40@14d6f273]

it looks like after invoking the global handler's filter method, the status is set to 204 (no content) so I think the global handler might be eating the response object somehow.

I've been working on a solution to introspect Resteasy classes for tracing and I'm hoping to get more trace to narrow down the problem.

WhiteCat22 commented 3 years ago
public void validateReturnValue(HttpRequest request, Object object, Method method, Object returnValue, Class<?>... groups)
[9/16/21, 11:14:16:526 CDT] 00000039 id=3b587fa6 org.jboss.resteasy.plugins.validation.GeneralValidatorImpl   > validateReturnValue Entry  
                                                                                                               org.jboss.resteasy.plugins.server.servlet.Servlet3AsyncHttpRequest@120b1fa
                                                                                                               Proxy for com.ibm.websphere.svt.gs.tax.session.ProductTaxShipSessionBeanLocal$ProductTaxShipSessionBeanRemote$1425262761$Proxy$_$$_Weld$EnterpriseProxy$@b1904dca
                                                                                                               public com.ibm.websphere.svt.gs.tax.entity.ShipRate com.ibm.websphere.svt.gs.tax.session.ProductTaxShipSessionBean.getShipRateByItemID(java.lang.String) throws java.lang.Exception
                                                                                                               null

Interesting, it looks like the return value for getShipRateByItemID is null, which would explain why the server is sending back an HTTP 204 with no entity.

WhiteCat22 commented 3 years ago

I modified the application to print out the ShipRate being returned by the server and found that it is indeed null

ProductTaxShipSessionBean.java

    @GET
    @Path("getShipRateByItemID")
    @Produces(MediaType.APPLICATION_XML)
    @Consumes(MediaType.APPLICATION_XML)
    public ShipRate getShipRateByItemID(String itemID) throws Exception {
        String methodName ="getShipRateByItemID";
        ShipRate shipRate=null;
        try{
            logger.logp(Level.FINE, className, methodName, "Calling getShipRateByItemID");
            shipRate= taxAndShipRateSingletonBean.getShipRateByItemID(itemID);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println("Adam: shipRate=" + shipRate);
        return shipRate;
    }
[9/16/21, 11:49:20:600 CDT] 00000039 SystemOut                                                    O Adam: shipRate=null

So, the REAL problem is that taxAndShipRateSingletonBean.getShipRateByItemID(itemID); is returning null in the application.

WhiteCat22 commented 3 years ago

ShipRateSession.findShipRateById():

        ShipRate shipRate = null;
        EntityManager em = getEntityManager();
        try {
            shipRate = (ShipRate) em.find(ShipRate.class,
                    itemId);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return shipRate;

The EntityManager is returning null because the entity doesn't exist. https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html#find-java.lang.Class-java.lang.Object-

jagraj commented 3 years ago

@WhiteCat22 After debugging the application I found out that String itemID QueryParam is not passed to the resource method. I think this is related to this issue (https://github.com/OpenLiberty/open-liberty/issues/18210) where we need to set all annotations in bean class. After changing method signature with the QueryParam public ShipRate getShipRateByItemID(@QueryParam("itemID")String itemID) then I no longer see this problem. This issue will be resolved once fix is delivered for issue #18210.

Thanks for your help in debugging this problem.

WhiteCat22 commented 3 years ago

You're welcome!