Mantis - Hessian
Viewing Issue Advanced Details
4080 major always 06-16-10 01:12 06-16-10 01:12
dilbert  
 
normal  
new 4.0.6  
open  
none    
none  
0004080: SecurityException thrown when serializing Throwable on Google App Engine
When an exception is thrown on GAE (a servlet that extends HessianServlet) it is not forwarded to the client, instead a SecurityException is thrown. Here is an example. First the exception declaration:

public class TestException extends RuntimeException {
}

Next the service declaration:
public interface IService {
    void testException();
}

And finally the service implementation:
public class Service extends HessianServlet implements IService {
    public void testException() {
        throw new TestException();
    }
}
As You can see this is a trivial implementation to test the exception. When executed on Google servers it dies like this:

java.lang.SecurityException: java.lang.IllegalAccessException: Reflection is not allowed on private java.lang.Throwable java.lang.Throwable.cause
    at com.google.appengine.runtime.Request.process-9880ff155b30e983(Request.java)
    at java.lang.reflect.Field.setAccessible(Field.java:166)
    at com.caucho.hessian.io.JavaSerializer.introspect(JavaSerializer.java:122)
    at com.caucho.hessian.io.JavaSerializer.<init>(JavaSerializer.java:81)
    at com.caucho.hessian.io.ThrowableSerializer.<init> ThrowableSerializer.java:59)
    at com.caucho.hessian.io.SerializerFactory.loadSerializer(SerializerFactory.java:301)
    at com.caucho.hessian.io.SerializerFactory.getSerializer(SerializerFactory.java:224)
    at com.caucho.hessian.io.SerializerFactory.getObjectSerializer(SerializerFactory.java:197)
    at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:418)
    at com.caucho.hessian.io.Hessian2Output.writeFault(Hessian2Output.java:400)
    at com.caucho.hessian.server.HessianSkeleton.invoke(HessianSkeleton.java:314)
    at com.caucho.hessian.server.HessianSkeleton.invoke(HessianSkeleton.java:202)
    at com.caucho.hessian.server.HessianServlet.invoke(HessianServlet.java:389)
    at com.caucho.hessian.server.HessianServlet.service(HessianServlet.java:369)
    at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
    at com.google.apphosting.utils.servlet.ParseBlobUploadFilter.doFilter(ParseBlobUploadFilter.java:97)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.google.apphosting.runtime.jetty.SaveSessionFilter.doFilter(SaveSessionFilter.java:35)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.handle(AppVersionHandlerMap.java:238)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.Server.handle(Server.java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
    at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:923)
    at com.google.apphosting.runtime.jetty.RpcRequestParser.parseAvailable(RpcRequestParser.java:76)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
    at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:135)
    at com.google.apphosting.runtime.JavaRuntime.handleRequest(JavaRuntime.java:250)
    at com.google.apphosting.base.RuntimePb$EvaluationRuntime$6.handleBlockingRequest(RuntimePb.java:5838)
    at com.google.apphosting.base.RuntimePb$EvaluationRuntime$6.handleBlockingRequest(RuntimePb.java:5836)
    at com.google.net.rpc.impl.BlockingApplicationHandler.handleRequest(BlockingApplicationHandler.java:24)
    at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:398)
    at com.google.net.rpc.impl.Server$2.run(Server.java:852)
    at com.google.tracing.LocalTraceSpanRunnable.run(LocalTraceSpanRunnable.java:56)
    at com.google.tracing.LocalTraceSpanBuilder.internalContinueSpan(LocalTraceSpanBuilder.java:576)
    at com.google.net.rpc.impl.Server.startRpc(Server.java:807)
    at com.google.net.rpc.impl.Server.processRequest(Server.java:369)
    at com.google.net.rpc.impl.ServerConnection.messageReceived(ServerConnection.java:442)
    at com.google.net.rpc.impl.RpcConnection.parseMessages(RpcConnection.java:319)
    at com.google.net.rpc.impl.RpcConnection.dataReceived(RpcConnection.java:290)
    at com.google.net.async.Connection.handleReadEvent(Connection.java:474)
    at com.google.net.async.EventDispatcher.processNetworkEvents(EventDispatcher.java:831)
    at com.google.net.async.EventDispatcher.internalLoop(EventDispatcher.java:207)
    at com.google.net.async.EventDispatcher.loop(EventDispatcher.java:103)
    at com.google.net.rpc.RpcService.runUntilServerShutdown(RpcService.java:251)
    at com.google.apphosting.runtime.JavaRuntime$RpcRunnable.run(JavaRuntime.java:413)
    at java.lang.Thread.run(Unknown Source)
Caused by: java.lang.IllegalAccessException: Reflection is not allowed on private java.lang.Throwable java.lang.Throwable.cause
    ... 54 more

On the client I get something like this:
java.lang.reflect.UndeclaredThrowableException
    at $Proxy0.testException(Unknown Source)
    at com.noveideje.testHessian.client.Main.main(Main.java:37)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:616)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:110)
Caused by: java.io.EOFException: readObject: unexpected end of file
    at com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2133)
    at com.caucho.hessian.io.MapDeserializer.readMap(MapDeserializer.java:114)
    at com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1653)
    at com.caucho.hessian.io.Hessian2Input.readReply(Hessian2Input.java:348)
    at com.caucho.hessian.client.HessianProxy.invoke(HessianProxy.java:194)
    ... 7 more

I have found a workaround by using a custom serializer. Here is how. First the Serializer:

public class ThrowableSerializer extends AbstractSerializer {
    @Override
    public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {
        if (obj != null) {
            final Class cl = obj.getClass();
            if (out.addRef(obj))
                return;
            int ref = out.writeObjectBegin(cl.getName());
            Throwable tr = (Throwable) obj;
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            try {
                oos.writeObject(tr);

                if (ref < -1) {
                    out.writeString("value");
                    out.writeBytes(bos.toByteArray());
                    out.writeMapEnd();
                } else {
                    if (ref == -1) {
                        out.writeInt(1);
                        out.writeString("value");
                        out.writeObjectBegin(cl.getName());
                    }
                    out.writeBytes(bos.toByteArray());
                }
            } finally {
                oos.close();
                bos.close();
            }
        } else
            out.writeNull();
    }
}
 
The other class we need is the Deserializer:
public class ThrowableDeserializer extends AbstractDeserializer {

    @Override
    public Class getType() {
        return Throwable.class;
    }

    @Override
    public Object readMap(AbstractHessianInput in) throws IOException {
        int ref = in.addRef(null);
        byte[] initValue = null;
        while (!in.isEnd()) {
            String key = in.readString();

            if (key.equals("value"))
                initValue = in.readBytes();
            else
                in.readString();
        }

        in.readMapEnd();
        ByteArrayInputStream bis = new ByteArrayInputStream(initValue);
        ObjectInputStream ois = new ObjectInputStream(bis);
        try {
            Object value = ois.readObject();
            in.setRef(ref, value);
            return value;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            ois.close();
            bis.close();
        }
    }

    @Override
    public Object readObject(AbstractHessianInput in, Object[] fieldNames)
            throws IOException {
        int ref = in.addRef(null);
        byte[] initValue = null;
        for (Object o : fieldNames) {
            if (o instanceof String) {
                final String key = (String) o;
                if (key.equals("value"))
                    initValue = in.readBytes();
                else
                    in.readObject();
            }
        }
        ByteArrayInputStream bis = new ByteArrayInputStream(initValue);
        ObjectInputStream ois = new ObjectInputStream(bis);
        try {
            Object value = ois.readObject();
            in.setRef(ref, value);
            return value;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            ois.close();
            bis.close();
        }
    }
}
I'm not sure if the readMap part is actually needed since I rearranged this code from another example. Also, a ThrowableSerializerFactory is needed:
public class ThrowableSerializerFactory extends AbstractSerializerFactory {
    @Override
    public Serializer getSerializer(Class cl) throws HessianProtocolException {
        if (Throwable.class.isAssignableFrom(cl)) {
            return new ThrowableSerializer();
        }
        return null;
    }

    @Override
    public Deserializer getDeserializer(Class cl) throws HessianProtocolException {
        if (Throwable.class.isAssignableFrom(cl)) {
            return new ThrowableDeserializer();
        }
        return null;
    }
}
What this code essentially does is take a Throwable (which implements Serializable), serializes it to a byte[] and pushes it over to the other side. This serialization does not use the problematic setAccessible method (like com.caucho.hessian.io.ThrowableSerializer) and works correctly on App engine (I tested it). The only part left to do is to plug all this into the servlet and the client. Here is how to do it on the servlet:
public class Service extends HessianServlet implements IService {
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        getSerializerFactory().addFactory(new ThrowableSerializerFactory());
    }
    // implement IService methods...
}

And here is how to do it on the client:
String url = "http://whatever.appspot.com/service"; [^]
HessianProxyFactory factory = new HessianProxyFactory();
factory.getSerializerFactory().addFactory(new ThrowableSerializerFactory());
IService service = (IService) factory.create(IService.class, url);

If you have any additional questions do not hesitate to ask. Thank you for your time. The attachment contains an example project in which the bug is reproduced.
This bug report obsoletes this bug: http://bugs.caucho.com/view.php?id=4061 [^]
Some forums where this bug was discussed:

http://groups.google.com/group/google-appengine-java/browse_thread/thread/0ccb9d0ff6b88545/ [^]
http://forum.caucho.com/showthread.php?t=9999 [^]
 testHessian.zip [^] (21,521 bytes) 06-16-10 01:12

There are no notes attached to this issue.