Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to get ContextHandlerCollection from server #12653

Open
HemanthKarniyana opened this issue Dec 18, 2024 · 14 comments
Open

Unable to get ContextHandlerCollection from server #12653

HemanthKarniyana opened this issue Dec 18, 2024 · 14 comments
Labels

Comments

@HemanthKarniyana
Copy link

HemanthKarniyana commented Dec 18, 2024

Jetty Version
12.0.14

Jetty Environment
EE8

Java Version
JDK17

Question

Hi, i have deployed a webapplication in jetty12 ee8. /aaa return 301 status code. Am trying to redirect it to 302 due to some requirements. I followed the below approach and got stuck, if some can help it would be great!

I have written a customContextHandler.java and set this handlers to server with /aaa contextPath.

public class CustomContextHandler extends ContextHandler {

    private static final Logger LOG = LoggerFactory.getLogger(CustomContextHandler2.class);

    @Override
    public boolean handle(Request request, Response response, Callback callback) throws Exception {
           // if path starts with /aaa then set status code to 302
    }
}

It return 302 as expected but Now the problem is even if the webAppContext is not loaded it return 302 which should be 503.
To achieve this am trying to get the contextHandlerCollection from server and check if there is a contextHandler for /aaa then i can confirm the webappcontext is loaded. So the condition should be

// if path starts with /aaa and /aaa Contextexists then set status code to 302

Am trying to get handlers from server.getHandler() and its returning a monitoring handler (statisticsHandler) instead of handler collection/tree.

Am i missing something? How can i achieve this.

@sbordet
Copy link
Contributor

sbordet commented Dec 18, 2024

Have you tried to use RewriteHandler with a RedirectPatternRule?

See documentation:

Don't try to modify Jetty classes, very likely there is a better approach -- you are not the first one that is asking to redirect with a different status code.

It's not clear if you use Jetty embedded or as a standalone server, but the above documentation should cover both cases.

If still does not work, you have to be more precise to describe what the problem is.

@HemanthKarniyana
Copy link
Author

HemanthKarniyana commented Dec 18, 2024

@sbordet Thanks for the response.

I have tried RewriteHandler with a RedirectPatternRule and able to redirect but here also i face the same problem. if the /aaa context failed to startup still its returning to 302 instead of 503 which is a false positive. The rewrite handler redirecting /aaa request independent of the webappcontext

Am using standalone jetty server.

Details about the approach.

I have written a CustomContextHandler extends ContextHandler. Within this handler,

If the path equals /aaa and if webapp loaded then set status code to 302
If the path equals /aaa and if webapp not loaded then super.handle()....

Now to check if webapp loaded or not, i'm trying to check if there is any ContextHandler associated with "/aaa" contextPath. To get the server handlers am using server.getHandlers().

server.getHandlers() return a List<Handler> = [oejsh.StatisticsHandler@5cdd09b1{STARTED}]. But am expecting it should also return WebAppContextHandlers. Is there any better approach ? or am i missing something here ?

package com.oracle.multitenant.jetty;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.slf4j.LoggerFactory;
import org.eclipse.jetty.server.Server;
import org.slf4j.Logger;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.ee8.webapp.WebAppContext;
import java.util.List;

public class CustomContextHandler extends ContextHandler {
    private static final Logger LOG = LoggerFactory.getLogger(CustomContextHandler.class);
    @Override
    public boolean handle(Request request, Response response, Callback callback) throws Exception {
        // Custom logic before calling the parent method
        String path = request.getHttpURI().getCanonicalPath();
        String contextPath = getContextPath();

        if (path.equals("/aaa")) {
            List<Handler> handlers = getServer().getHandlers();
            for (Handler handler : handlers) {
                if (handler instanceof WebAppContext) {
                    WebAppContext webAppContext = (WebAppContext) handler;
                    if ("/aaa".equals(webAppContext.getContextPath()) && webAppContext.isAvailable()) {
                        response.setStatus(302);
                        callback.succeeded();
                        return true;
                    }
                }
            }
        }
        return super.handle(request, response, callback);
    }
}

Note : I want to return 503 incase /aaa webappcontext failed to load. and 302 incase it loaded.

@sbordet
Copy link
Contributor

sbordet commented Dec 18, 2024

How do you deploy the web application?

Is it a *.war you drop in $JETTY_BASE/webapps?

Do you have a custom Jetty Context XML file?

Is /hello the context path of that web application?

And the redirect is to /hello/ (with the trailing slash)?

Look at ContextHandler.handleMovedPermanently(), which is where the 301 is sent if what I wrote above is your case.

It should be enough for you to override that method, although we can certainly make it more easily configurable.

If your web application is a *.war file, it should be enough to override handleMovedPermanently() and add a Jetty Context XML file to specify your WebAppContext subclass.

@janbartel
Copy link
Contributor

janbartel commented Dec 18, 2024

@HemanthKarniyana in addition to the questions from @sbordet can you please explain how you have configured your custom ContextHandler class onto the Server, ie what is the ordering of your Handler tree?

BTW your code which is looking for WebAppContexts looks incorrect, and will always fall through to super.handle() which might be why you always see the 301. Your code is only receiving WebAppContexts that are the direct children of the server, but you have a more complex handler structure than that. You want to do something like:

List<WebAppContext> webapps = getServer().getDescendants(WebAppContext.class);

I am somewhat perplexed that you've used a ContextHandler for this redirect. I would have expected that you would insert a Handler somewhere into your handler tree rooted at the Server - say just before the ContextHandlerCollection and then used it to filter on the request path and perform the redirect. If you don't want to do that, and you want to use a ContextHandler that is added to the ContextHandlerCollection, then you could look at using a standard org.eclipse.jetty.ee9.nested.ContextHandler, and extend the org.eclipse.jetty.server.ConditionalHandler as it's handler to do the redirect.

@HemanthKarniyana
Copy link
Author

HemanthKarniyana commented Dec 19, 2024

@sbordet
We deploy A.war in $JETTY_BASE/webapps. We also have a Jetty Context XML file as A.xml under webapps. Yes, /aaa is the contextpath and redirected to /aaa/

A.xml

<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="A" class="org.eclipse.jetty.ee8.webapp.WebAppContext">
    <Set name="contextPath">/aaa</Set>
    <Set name="war"><Property name="jetty.webapps" default="." />/A.war</Set>
    <Set name="overrideDescriptor"><Property name="jetty.webapps" default="."/>/A.d/A-override.xml</Set>
    <Set name="extractWAR">true</Set>
    <Call name="setAttribute">
      <Arg>org.eclipse.jetty.webapp.basetempdir</Arg>
      <Arg><Property name="jetty.base" default="." />/work</Arg>
    </Call>

<!-- Some matcher config -->
</Configure>

As you mentioned ContextHandler.handleMovedPermanently() return 301. But am not able to override this method from a CustomWebAppContext. for example, the below is not possible as method does not override or implement a method from a supertype. (its an ee8.webapp.WebAppContext

public class CustomWebAppContext extends WebAppContext {
    @Override
    protected void handleMovedPermanently(Request request, Response response, Callback callback) {
......

I tried doing as below

public class CustomContextHandler4 extends ContextHandler {
    private static final Logger LOG = LoggerFactory.getLogger(CustomContextHandler4.class);

    @Override
    protected void handleMovedPermanently(Request request, Response response, Callback callback) {
        // TODO: should this be a fully qualified URI? (with scheme and host?)
        String location = getContextPath() + "/";
        if (request.getHttpURI().getParam() != null)
            location += ";" + request.getHttpURI().getParam();
        if (request.getHttpURI().getQuery() != null)
            location += "?" + request.getHttpURI().getQuery();

        response.setStatus(HttpStatus.FOUND_302);
        response.getHeaders().add(new HttpField(HttpHeader.LOCATION, location));
        callback.succeeded();
    }
}

Now i have added this ContextHandler to server in jetty-home/jetty.xml. But this is not able to override the webAppContext's contextHandler and still returning 301.

    <!-- =========================================================== -->
    <!-- Set the handler structure for the Server                    -->
    <!-- =========================================================== -->
    <Set name="defaultHandler">
      <New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler">
        <Arg name="serveFavIcon" type="boolean"><Property name="jetty.server.default.serveFavIcon" default="true"/></Arg>
        <Arg name="showContexts" type="boolean"><Property name="jetty.server.default.showContexts" default="true"/></Arg>
      </New>
    </Set>
    <Set name="handler">
      <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection">
        <Call name="addHandler">
          <Arg>
              <New class="com.oracle.multitenant.jetty.CustomContextHandler">
                <Set name="contextPath">/aaa</Set>
              </New>
          </Arg>
        </Call>
      </New>
    </Set>

@sbordet
Copy link
Contributor

sbordet commented Dec 19, 2024

Ok, let's try a simpler solution first, i.e. disabling the redirect.

In hello.xml call setAllowNullPathInfo(true):

<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="Hello" class="org.eclipse.jetty.ee8.webapp.WebAppContext">
    <Set name="contextPath">/hello</Set>
    ...
    <Set name="allowNullPathInfo">true</Set>
    ...
</Configure>

Let us know if this is enough.

@HemanthKarniyana
Copy link
Author

HemanthKarniyana commented Dec 19, 2024

@sbordet
<Set name="allowNullPathInfo">true</Set> this is breaking the webappcontext load.
java.lang.NullPointerException: Cannot invoke "String.split(String)" because the return value of "javax.servlet.http.HttpServletRequest.getHeader(String)" is null

Quick Update : Am able to achieve the requirement. I followed the below approach.

  1. Added a CustomContextHandler in jetty.xml
    <Set name="handler">
      <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection">
        <Call name="addHandler">
          <Arg>
              <New class="com.oracle.multitenant.jetty.CustomContextHandler">
                <Set name="contextPath">/aaa</Set>
              </New>
          </Arg>
        </Call>
        <Set name="dynamic" property="jetty.server.contexts.dynamic"/>
      </New>
    </Set>
  1. CustomContextHandler.java
package com.example.jetty;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.slf4j.LoggerFactory;
import org.eclipse.jetty.server.Server;
import org.slf4j.Logger;
import org.eclipse.jetty.server.Handler;
import java.util.List;
import java.util.ArrayList; 

public class CustomContextHandler extends ContextHandler {
    private static final Logger LOG = LoggerFactory.getLogger(CustomContextHandler7.class);

    @Override
    public boolean handle(Request request, Response response, Callback callback) throws Exception {
        String path = request.getHttpURI().getPath();

        if (path.equals("/aaa")) {
            // Since it is leaf i have to get descendants from the server....
            List<ContextHandler> descendants = getServer().getDescendants(ContextHandler.class);

            for (ContextHandler contextHandler : descendants) {
                if(contextHandler.getContextPath().equals("/aaa") &&
                    contextHandler.isAvailable() &&
                    contextHandler.getBaseResource() != null){
                    
                    response.setStatus(302);
                    callback.succeeded();
                    
                    return true;
                }
            }
            // it is a leaf so no point of super.handle()
            return false;
        }
        // it is a leaf so no point of super.handle()
        return false;
    }
}

I have three conditions here -
contextHandler.getContextPath().equals("/aaa") --> The contextPath should be /aaa
contextHandler.isAvailable() --> This is Available if /aaa loaded successfully or Unavailable
contextHandler.getBaseResource() != null --> This is to differentiate between webappcontexthandler and customcontexthandler.

With this setup. I'm able get 302 for /aaa (loaded) and 503 /aaa (not loaded).

But i have some performance concern here. Lets say a normal request comes to the server and it reaches CustomContextHandler and return false. Then the server start the cycle again ?

Do you see any performance issues or any improvements.

There are much simplier approach but those i found doesn't check if webappcontext is loaded or not. Simply return 302 even if the context load failed which is a false positive.

@sbordet
Copy link
Contributor

sbordet commented Dec 19, 2024

@janbartel I think we should just have a setter with the redirect code, i.e. WebAppContext.setRedirectCode(302).

The approach taken by @HemanthKarniyana may work, but seems way too complicated to just change a redirect code.

Thoughts?

@janbartel
Copy link
Contributor

@HemanthKarniyana your examples are very confusing because you keep swapping the relevant context paths. Please explain precisely what it is you are trying to do. What I have understood from your previous postings is this:

  • if you receive a request for a particular context - let's call it A - you want to check if a different context - let's call it B - is deployed, and if so, you want to redirect to it with a 302. So for example, receiving a request to /aaa, you wish to 302 redirect to /bbb, only iff the webapp at /bbb is deployed.

Is this correct? If not, please state clearly the situation you are trying to achieve using the above /aaa and /bbb terminology so we can be sure we are on the same page. There are many other possibilities for what you might be trying to achieve and it's impossible to advise you correctly unless we have a correct understanding.

@sbordet until we achieve clarity of what @HemanthKarniyana is wanting to achieve I do not see any reason to change any implementation of any class.

@HemanthKarniyana
Copy link
Author

HemanthKarniyana commented Dec 20, 2024

@janbartel Sorry my bad posted with a confusing example before. I have updated all the code mentioned with right example. Any way am explaining here again. Webapp A is deployed and its context path is /aaa. If we receive a request to /aaa it returns 301 currently. I want to redirect to 302 instead. But the constraint here is i want to redirect only if the webapp A is successfully loaded.

If webapp A successfully loaded then /aaa status code is 302
If webapp A not loaded then /aaa status code is 503.

RewriteHandler and other methods return 302 irrespective of webapp loaded or not. This is the main reason i have to go through all this complex handler approach.

I achieved this using two methods,

Method-1 : Add a leaf handler (CustomContextHandler) Mentioned above. (Updated with webapp A contextpath /aaa for better understanding.)

Method-2 : Add a root handler (CustomHandlerWrapper) and every request goes through it.

package com.example.jetty;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Callback;
import org.slf4j.LoggerFactory;
import org.eclipse.jetty.server.Server;
import org.slf4j.Logger;
import org.eclipse.jetty.server.Handler;
import java.util.List;
import java.util.ArrayList; 
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;

public class CustomHandlerWrapper extends Handler.Wrapper {
    private static final Logger LOG = LoggerFactory.getLogger(CustomHandlerWrapper.class);

    public CustomHandlerWrapper() {
        super(); 
        LOG.info("CustomHandlerWrapper initialized with default constructor");
    }

    public CustomHandlerWrapper(Handler handler) {
        super(); 
        setHandler(handler);
        LOG.info("CustomHandlerWrapper initialised with a handler: {}", handler);
    }

    @Override
    public boolean handle(Request request, Response response, Callback callback) throws Exception {
        String path = request.getHttpURI().getPath();

        if (path.equals("/aaa")) {
            List<ContextHandler> descendants = getDescendants(ContextHandler.class);

            for (ContextHandler contextHandler : descendants) {
                if(contextHandler.getContextPath().equals("/aaa") &&
                    contextHandler.isAvailable()){

                    String location = request.getHttpURI() + "/";
                    response.setStatus(HttpStatus.FOUND_302);
                    response.getHeaders().add(new HttpField(HttpHeader.LOCATION, location));

                    callback.succeeded();
                    return true;
                }
            }
            // forwarding to next handler in the chain. 
            return super.handle(request, response, callback);
        }
        // forwarding to next handler in the chain. 
        return super.handle(request, response, callback);
    }
}

jetty.xml

<Set name="handler">
      <New class="com.example.jetty.CustomHandlerWrapper">
        <Set name="handler">
          <New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection">
            <Set name="dynamic" property="jetty.server.contexts.dynamic"/>
          </New>
        </Set>
      </New>  
</Set>

Both methods are same the difference is one is sitting at the root level and other is sitting at the leaf level.

@gregw
Copy link
Contributor

gregw commented Dec 20, 2024

@HemanthKarniyana I've only just tuned into this issue and to be honest I've not read anything but your most recent post.
I'm just wondering why you go to the effort of a custom handler wrapper to check availability when there is already an availability mechanism on ContextHandler? You can call ContextHandler.setAvailabile(boolean) and if you set it to false, then the context will respond with a 503 (or more precisely by calling ContextHandler.handleUnavailable(Request request, Response response, Callback callback))

As for the 301 vs 302, why not simply extend the ContextHandler and override the handleMovedPermanently to send 302?

@gregw
Copy link
Contributor

gregw commented Dec 20, 2024

Ah - I see you may be using EE8 or EE9, where we hide the core context handler. Hmmm perhaps we need to expose those methods better... investigating...

@gregw
Copy link
Contributor

gregw commented Dec 20, 2024

There is indeed more we could to to make the EE8/EE9 ContextHandler more extensible.

However, there is still an alternative that might be simpler. Install two contexts at '/aaa', with the first being a simple extension of the core ContextHandler. If the path is not "/aaa" it returns false and lets the request fall through to the next '/aaa' context. If the path is "/aaa" it either sends a 503 or a 302 redirect depending on the availability of the other context. The other context can thus be normal (or not even deployed) and you will get the behaviour you want.

@HemanthKarniyana
Copy link
Author

HemanthKarniyana commented Dec 20, 2024

Hi @gregw , yeah i mentioned the same in method-1. extends ContextHandler and returns 302 if matched else return false the ContextHandlerCollection will take care of 503. If you see any simpler approach or issues in the method-1,2 mentioned above, please suggest thank you for the response.

Also if you maintain something like setRedirectCode, setRedirectHeaders in WebAppContext that will be much helpful...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants