Search code examples
xmlconfigurationreverse-proxyjetty-9

Jetty 9.x ProxyServlet - how to set up ServletContext correctly in XML


Aiming to start Jetty locally with a webapp servlet and a proxy servlet both running, and other tools like deploy and console logging. All Jetty configuration is in XML files.

The proxy servlet will reverse-proxy GET requests prefixed /media/* to an external site https://example-server/. So http://localhost:8080/media/image.jpg will pass through to https://media-server/image.jpg.

Here's an extract from my jetty.xml:

<Set name="handler">
  <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
    <Set name="handlers">
     <Array type="org.eclipse.jetty.server.Handler">
       <Item>
         <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
       </Item>
       <Item>
         <New id="context" class="org.eclipse.jetty.servlet.ServletContextHandler">
           <Arg><Get class="org.eclipse.jetty.servlet.ServletContextHandler" name="SESSIONS"/></Arg>
           <Call name="setContextPath" arg="/"/>
           <Set name="servletHandler">
             <New id="handler" class="org.eclipse.jetty.servlet.ServletHandler">
               <Call id="holder" name="addServletWithMapping" arg="org.eclipse.jetty.proxy.ProxyServlet$Transparent,/media/*">
                 <Call name="setInitParameter" arg="proxyTo,https://media-server"/>
                 <Call name="setInitParameter" arg="prefix,/media"/>
               </Call>
             </New>
           </Set>
         </New>
       </Item>
       <Item>
         <New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/>
       </Item>
     </Array>
    </Set>
  </New>
</Set>

The above XML should be equivalent to this Java code.

ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
ServletHandler handler = new ServletHandler();
ServletHolder holder = handler.addServletWithMapping(ProxyServlet$Transparent.class, "/media/*");
holder.setInitParameter("proxyTo", "https://media-server");
holder.setInitParameter("prefix", "/media");
context.setServletHandler(handler);

That is adapted from the default jetty.xml and https://dzone.com/articles/configuring-jetty-servlet-proxy

From the DZone guide I've updated the class names for Jetty 9.x. So org.eclipse.jetty.servlets.ProxyServlet is now org.eclipse.jetty.proxy.ProxyServlet and the proxyTo and prefix parameters must start with a lowercase p.

Checked that jetty-proxy-9.4.12.v20180830.jar is included as a library in Jetty's startup configuration.

For logging, the Jetty command line includes -Dorg.eclipse.jetty.proxy.LEVEL=DEBUG (I recommend this for anyone else troubleshooting ProxyServlet.)

The problem: nothing's happening. The ProxyServlet is not activating on GET requests to `http://localhost:8080/media/image.jpg'.

Here's the log line showing the ServletContextHandler is starting.

2018-09-28 15:26:46.045:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@1e028a9{"",null,AVAILABLE}

I guess there's simple solution to this, like setting the ServletContext correctly, but I can't figure out how to do that in XML, and would really appreciate some help. Jetty documentation on this is thin.

Now if I change the jetty.xml to this below then the proxy does activate on GET requests to `http://localhost:8080/media/image.jpg'.

<Set name="handler">
  <New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
    <Set name="handlers">
     <Array type="org.eclipse.jetty.server.Handler">
       <Item>
         <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
       </Item>
       <Item>
         <New id="handler" class="org.eclipse.jetty.servlet.ServletHandler">
           <Call id="holder" name="addServletWithMapping" arg="org.eclipse.jetty.proxy.ProxyServlet$Transparent,/media/*">
             <Call name="setInitParameter" arg="proxyTo,https://media-server"/>
             <Call name="setInitParameter" arg="prefix,/media"/>
           </Call>
         </New>
       </Item>
       <Item>
         <New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/>
       </Item>
     </Array>
    </Set>
  </New>
</Set>

The log shows the ProxyServlet activating like this:

2018-09-28 14:22:14.904:DBUG:oejpP.194a1b5:qtp22374712-13: org.eclipse.jetty.proxy.ProxyServlet$Transparent-194a1b5 @ null/media to https://media-server
2018-09-28 14:22:14.904:DBUG:oejpP.194a1b5:qtp22374712-13: 21964987 rewriting: http://localhost:8080/media/image.jpg -> null

But here the proxy fails because it has a null context. So the prefix has been set (source code) to null/media is because the ServletContext.getContextPath() is null. And that causes a redirect to null because of this source code. With _prefix set to null/media, nothing's going to match that. Possibly that's a bug, I've opened an issue.


Solution

  • Here's an example of using a ProxyServlet from a ${jetty.base} directory.

    $ mkdir proxy-example-base
    $ cd proxy-example-base/
    $ java -jar ../../jetty-home-9.4.12.v20180830/start.jar --add-to-start=http,deploy,proxy
    INFO  : webapp          transitively enabled, ini template available with --add-to-start=webapp
    INFO  : server          transitively enabled, ini template available with --add-to-start=server
    INFO  : proxy           initialized in ${jetty.base}/start.ini
    INFO  : security        transitively enabled
    INFO  : servlet         transitively enabled
    INFO  : http            initialized in ${jetty.base}/start.ini
    INFO  : client          transitively enabled
    INFO  : threadpool      transitively enabled, ini template available with --add-to-start=threadpool
    INFO  : deploy          initialized in ${jetty.base}/start.ini
    MKDIR : ${jetty.base}/webapps
    INFO  : Base directory was modified
    $ ls -la
    total 8
    drwxr-xr-x   4 joakim  staff   136 Oct  1 14:10 ./
    drwxr-xr-x  17 joakim  staff   578 Oct  1 14:10 ../
    -rw-r--r--   1 joakim  staff  2146 Oct  1 14:10 start.ini
    drwxr-xr-x   3 joakim  staff   102 Oct  1 14:11 webapps/
    $ cp ~/Downloads/media-proxy.xml webapps/
    $ cat webapps/media-proxy.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
    <Configure class="org.eclipse.jetty.servlet.ServletContextHandler">
      <Set name="contextPath">/</Set>
      <Call name="addServlet">
        <Arg>org.eclipse.jetty.proxy.ProxyServlet$Transparent.class</Arg>
        <Arg>/media/*</Arg>
        <Set name="InitOrder">1</Set>
        <Call name="setInitParameter">
          <Arg>proxyTo</Arg>
          <Arg>https://media-server/</Arg>
        </Call>
        <Call name="setInitParameter">
          <Arg>prefix</Arg>
          <Arg>/media</Arg>
        </Call>
      </Call>
    </Configure>
    

    The way this works is that ${jetty.base} is configured to have the proxy jetty module enabled, which puts the proxy classes on the server classpath.

    Then the deploy jetty module is enabled to find configurations of webapps in the ${jetty.base}/webapps/ directory and deploy them.

    Finally, an XML deployable is setup to have a javax.servlet.ServletContext, at the / contextPath, and one defined servlet, the ProxyServlet$Transparent with some init-parameters.