Embedded Jetty and Angular 6: only rewrite URLs that don`t match any servlet or files

classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|

Embedded Jetty and Angular 6: only rewrite URLs that don`t match any servlet or files

Nicolas Therrien
Hi,

I want to add an Angular 6 application to an existing embedded Jetty web server (version 9.4.12.v20180830). I am having a problem getting client-side page routing to work properly.

For example:
Is a valid Angular route, but does not exist as a file.
I can navigate to it either via a link on the home page  (index.html) or by typing: http://localhost:8080/#/mypage in my browser.

If I type http://localhost:8080/mypage,  I get a 404.

That is because for client-side routing feature to work properly, HTML 5 PushState must be supported. From my understanding, what this means is that the server must rewrite URLs so that they reach the angular page on /index.html. Using the above example, "/mypage" would have to be rewritten to "/index.html" with an original path attribute set to the requested path.

Here is are links to Angular documentation: 

I've been working at it for a few days and am stuck. I have found how to do url rewriting with Jetty, using a RewriteHandler. The rewriting works, but is affecting all URLs, including valid ones. This causes script files and images to be replaced with contents of index.html.

I need URLs associated with other servlets (such as websocket servlets and rest apis) to be left untouched, and I need URLs for real files (such as images and other angular assets) to be left untouched.

I have been looking for something in the like of Ngnx:
try_files $uri $uri/ /index.html; 

Which translates into "try the URI as-is, if that doesn`t work, treat the URI as a folder, if that doesnt work then use index.html"

So I tried this with Jetty:

// Create the web app context
WebAppContext context = new WebAppContext();
context.setContextPath("/");
context.setWelcomeFiles(new String[] {"index.html"});
String resLocation = WebServer.class.getResource("/webapp").toString();
context.setResourceBase(resLocation);

// Enable URL Rewriting to support HTML 5 PushState
RewriteHandler rewrite = new RewriteHandler();
rewrite.setRewriteRequestURI(true);
rewrite.setRewritePathInfo(false);
rewrite.setOriginalPathAttribute("requestedPath");

RewriteRegexRule html5pushState = new RewriteRegexRule();
html5pushState.setRegex("/.*");
html5pushState.setReplacement("/index.html");
rewrite.addRule(html5pushState);

// Handler Structure
HandlerList handlers = new HandlerList();
rewrite.setHandler(context);
handlers.setHandlers(new Handler[] { context, rewrite}); 
server.setHandler(handlers);


I was hoping this would do the trick. My rationale was that using a HandlerList would cause Jetty to call each handler in the list in the order specified until a response is matched.  Calling context first would lookup all the existing webapps for a file or servlet.  Then calling rewrite would rewrite the URL and then because context is also a child of rewrite I was hoping this would cause Jetty to try again using the rewritten URL, in which case would be index.html.

Unfortunately, It doesn`t work because "context" returns a 404 to the client directly. The rewrite handler is never called.


I've been looking for Conditional statements in Jetty and couldn`t find any. In fact, I was able to do this kind of conditional rewrite with Tomcat using a RewriteValve:
ctx.addValve(new RewriteValve());

and a text file (rewrite.config):

RewriteCond %{REQUEST_URI} -f
RewriteRule ^(.*)$ - [L]

RewriteRule ^(.*)$ /index.html


Notice how the rewrite rule is conditional.   The rewrite occurs only if the URL cannot be matched to a file.



How can this kind of conditional rewriting be done under Jetty?

If a custom handler must be written, how would you guys query the server to figure out if a given path matches a servlet or a file?

Your help would be very much appreciated.

Nicolas Therrien

Senior Software Developer

https://www.motorolasolutions.com/content/dam/msi/images/logos/corporate/msiemailsignature.png

o: +1.819.931.2053  


_______________________________________________
jetty-users mailing list
[hidden email]
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users
Reply | Threaded
Open this post in threaded view
|

Re: Embedded Jetty and Angular 6: only rewrite URLs that don`t match any servlet or files

Joakim Erdfelt-8
ErrorPageErrorHandler errorPageMapper = new ErrorPageErrorHandler();
errorPageMapper.addErrorPage(404, "/myerror");
context.setErrorHandler(errorPageMapper);

context.addServlet(MyErrorServlet.class, "/myerror");

Now you can handle any 404 error with your custom MyErrorServlet.class

Pay attention to the DispatcherType, it should be ERROR.
Pay attention to the HttpServletRequest.getAttributes() that have meaning in this state.

and all of the ERROR_* constants.

Joakim Erdfelt / [hidden email]


On Mon, Sep 10, 2018 at 4:59 PM Nicolas Therrien <[hidden email]> wrote:
Hi,

I want to add an Angular 6 application to an existing embedded Jetty web server (version 9.4.12.v20180830). I am having a problem getting client-side page routing to work properly.

For example:
Is a valid Angular route, but does not exist as a file.
I can navigate to it either via a link on the home page  (index.html) or by typing: http://localhost:8080/#/mypage in my browser.

If I type http://localhost:8080/mypage,  I get a 404.

That is because for client-side routing feature to work properly, HTML 5 PushState must be supported. From my understanding, what this means is that the server must rewrite URLs so that they reach the angular page on /index.html. Using the above example, "/mypage" would have to be rewritten to "/index.html" with an original path attribute set to the requested path.

Here is are links to Angular documentation: 

I've been working at it for a few days and am stuck. I have found how to do url rewriting with Jetty, using a RewriteHandler. The rewriting works, but is affecting all URLs, including valid ones. This causes script files and images to be replaced with contents of index.html.

I need URLs associated with other servlets (such as websocket servlets and rest apis) to be left untouched, and I need URLs for real files (such as images and other angular assets) to be left untouched.

I have been looking for something in the like of Ngnx:
try_files $uri $uri/ /index.html; 

Which translates into "try the URI as-is, if that doesn`t work, treat the URI as a folder, if that doesnt work then use index.html"

So I tried this with Jetty:

// Create the web app context
WebAppContext context = new WebAppContext();
context.setContextPath("/");
context.setWelcomeFiles(new String[] {"index.html"});
String resLocation = WebServer.class.getResource("/webapp").toString();
context.setResourceBase(resLocation);

// Enable URL Rewriting to support HTML 5 PushState
RewriteHandler rewrite = new RewriteHandler();
rewrite.setRewriteRequestURI(true);
rewrite.setRewritePathInfo(false);
rewrite.setOriginalPathAttribute("requestedPath");

RewriteRegexRule html5pushState = new RewriteRegexRule();
html5pushState.setRegex("/.*");
html5pushState.setReplacement("/index.html");
rewrite.addRule(html5pushState);

// Handler Structure
HandlerList handlers = new HandlerList();
rewrite.setHandler(context);
handlers.setHandlers(new Handler[] { context, rewrite}); 
server.setHandler(handlers);


I was hoping this would do the trick. My rationale was that using a HandlerList would cause Jetty to call each handler in the list in the order specified until a response is matched.  Calling context first would lookup all the existing webapps for a file or servlet.  Then calling rewrite would rewrite the URL and then because context is also a child of rewrite I was hoping this would cause Jetty to try again using the rewritten URL, in which case would be index.html.

Unfortunately, It doesn`t work because "context" returns a 404 to the client directly. The rewrite handler is never called.


I've been looking for Conditional statements in Jetty and couldn`t find any. In fact, I was able to do this kind of conditional rewrite with Tomcat using a RewriteValve:
ctx.addValve(new RewriteValve());

and a text file (rewrite.config):

RewriteCond %{REQUEST_URI} -f
RewriteRule ^(.*)$ - [L]

RewriteRule ^(.*)$ /index.html


Notice how the rewrite rule is conditional.   The rewrite occurs only if the URL cannot be matched to a file.



How can this kind of conditional rewriting be done under Jetty?

If a custom handler must be written, how would you guys query the server to figure out if a given path matches a servlet or a file?

Your help would be very much appreciated.

Nicolas Therrien

Senior Software Developer

https://www.motorolasolutions.com/content/dam/msi/images/logos/corporate/msiemailsignature.png

o: +1.819.931.2053  

_______________________________________________
jetty-users mailing list
[hidden email]
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users

_______________________________________________
jetty-users mailing list
[hidden email]
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users
Reply | Threaded
Open this post in threaded view
|

Re: Embedded Jetty and Angular 6: only rewrite URLs that don`t match any servlet or files

Nicolas Therrien
Hi Joakim,

Thanks for trying to help. I appreciated it.

I tried using the error handler to trigger a rewrite of the URL but it didn`t work well. Thanks to this, however, I learned that part of my problem was that the WebappContext has an error handler enabled by default. This gave me better understanding of what I had to do.

I want to share the solution with everyone in case other people want to use Angular 6 applications on Jetty.

I created the following class which extends RewriteHandler and reads data from a WebappContext to determine if the incoming target URL corresponds to an existing file or api resource:

import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class Html5PushStateConditionalRewriteHandler extends RewriteHandler {


    private final WebAppContext webAppContext;

    public Html5PushStateConditionalRewriteHandler(WebAppContext webAppContext) {
        this.webAppContext = webAppContext;
    }

    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        if (isStarted()) {

            final MappedResource<ServletHolder> mappedServlet = webAppContext.getServletHandler().getMappedServlet(target);

            boolean fileExists = (mappedServlet.getResource().getServlet().getServletConfig().getServletContext().getResource(target) != null);


            boolean isDefaultServlet = mappedServlet.getResource().getName().equals("default");


            // Do not interfere with urls that either :
            //   1) Map to an existing file or resource
            //   2) Fall under other servlet scopes (only default servlet is targeted by this rewrite handler)
            if (isDefaultServlet && !fileExists) {

                // Apply rewrite rules
                super.handle(target, baseRequest, request, response);

            } else {

                // Pass along unchanged
                if (!baseRequest.isHandled()) {
                    Handler handler = _handler;
                    if (handler != null)
                        handler.handle(target, baseRequest, request, response);
                }
            }
        }

    }




}



And then here is how it can be hooked up when building the embedded server:

// Create the web server
        server = new Server();
        ServerConnector connector = new ServerConnector(server);
        connector.setPort(config.port);
        server.addConnector(connector);

        // Create the web app mainWebAppContext
        WebAppContext mainWebAppContext = new WebAppContext();
        mainWebAppContext.setContextPath("/");
        mainWebAppContext.setWelcomeFiles(new String[]{"index.html"});
        String resLocation = WebServer.class.getResource("/webapp").toString();
        mainWebAppContext.setResourceBase(resLocation);

        // Enable URL Rewriting to support HTML 5 PushState
        RewriteHandler urlRewriteHandler = new Html5PushStateConditionalRewriteHandler(mainWebAppContext);
        urlRewriteHandler.setRewriteRequestURI(true);
        urlRewriteHandler.setRewritePathInfo(false);
        urlRewriteHandler.setOriginalPathAttribute("requestedPath");

        RewriteRegexRule html5pushState = new RewriteRegexRule();
        html5pushState.setRegex("/.*");
        html5pushState.setReplacement("/index.html");
        urlRewriteHandler.addRule(html5pushState);

        [some code omitted]
mainWebAppContext.addServlet(websocketServletHolder, "/api/ws");

// Handler Structure: UrlRewriteHandler will filter URLs before they reach the webapp context.
        urlRewriteHandler.setHandler(mainWebAppContext);
        server.setHandler(urlRewriteHandler);



Using this method, I have had excellent results. Better even than what Tomcat and Ngnx do because this handler recognizes all servlet paths automatically and does not rewrite them. Under tomcat we have to enter all servlet paths manually in the rewrite.config file.

Hopefully this helps someone else.

Nicolas Therrien

Senior Software Developer

https://www.motorolasolutions.com/content/dam/msi/images/logos/corporate/msiemailsignature.png

o: +1.819.931.2053  



On Mon, Sep 10, 2018 at 9:35 PM Joakim Erdfelt <[hidden email]> wrote:
ErrorPageErrorHandler errorPageMapper = new ErrorPageErrorHandler();
errorPageMapper.addErrorPage(404, "/myerror");
context.setErrorHandler(errorPageMapper);

context.addServlet(MyErrorServlet.class, "/myerror");

Now you can handle any 404 error with your custom MyErrorServlet.class

Pay attention to the DispatcherType, it should be ERROR.
Pay attention to the HttpServletRequest.getAttributes() that have meaning in this state.

and all of the ERROR_* constants.

Joakim Erdfelt / [hidden email]


On Mon, Sep 10, 2018 at 4:59 PM Nicolas Therrien <[hidden email]> wrote:
Hi,

I want to add an Angular 6 application to an existing embedded Jetty web server (version 9.4.12.v20180830). I am having a problem getting client-side page routing to work properly.

For example:
Is a valid Angular route, but does not exist as a file.
I can navigate to it either via a link on the home page  (index.html) or by typing: http://localhost:8080/#/mypage in my browser.

If I type http://localhost:8080/mypage,  I get a 404.

That is because for client-side routing feature to work properly, HTML 5 PushState must be supported. From my understanding, what this means is that the server must rewrite URLs so that they reach the angular page on /index.html. Using the above example, "/mypage" would have to be rewritten to "/index.html" with an original path attribute set to the requested path.

Here is are links to Angular documentation: 

I've been working at it for a few days and am stuck. I have found how to do url rewriting with Jetty, using a RewriteHandler. The rewriting works, but is affecting all URLs, including valid ones. This causes script files and images to be replaced with contents of index.html.

I need URLs associated with other servlets (such as websocket servlets and rest apis) to be left untouched, and I need URLs for real files (such as images and other angular assets) to be left untouched.

I have been looking for something in the like of Ngnx:
try_files $uri $uri/ /index.html; 

Which translates into "try the URI as-is, if that doesn`t work, treat the URI as a folder, if that doesnt work then use index.html"

So I tried this with Jetty:

// Create the web app context
WebAppContext context = new WebAppContext();
context.setContextPath("/");
context.setWelcomeFiles(new String[] {"index.html"});
String resLocation = WebServer.class.getResource("/webapp").toString();
context.setResourceBase(resLocation);

// Enable URL Rewriting to support HTML 5 PushState
RewriteHandler rewrite = new RewriteHandler();
rewrite.setRewriteRequestURI(true);
rewrite.setRewritePathInfo(false);
rewrite.setOriginalPathAttribute("requestedPath");

RewriteRegexRule html5pushState = new RewriteRegexRule();
html5pushState.setRegex("/.*");
html5pushState.setReplacement("/index.html");
rewrite.addRule(html5pushState);

// Handler Structure
HandlerList handlers = new HandlerList();
rewrite.setHandler(context);
handlers.setHandlers(new Handler[] { context, rewrite}); 
server.setHandler(handlers);


I was hoping this would do the trick. My rationale was that using a HandlerList would cause Jetty to call each handler in the list in the order specified until a response is matched.  Calling context first would lookup all the existing webapps for a file or servlet.  Then calling rewrite would rewrite the URL and then because context is also a child of rewrite I was hoping this would cause Jetty to try again using the rewritten URL, in which case would be index.html.

Unfortunately, It doesn`t work because "context" returns a 404 to the client directly. The rewrite handler is never called.


I've been looking for Conditional statements in Jetty and couldn`t find any. In fact, I was able to do this kind of conditional rewrite with Tomcat using a RewriteValve:
ctx.addValve(new RewriteValve());

and a text file (rewrite.config):

RewriteCond %{REQUEST_URI} -f
RewriteRule ^(.*)$ - [L]

RewriteRule ^(.*)$ /index.html


Notice how the rewrite rule is conditional.   The rewrite occurs only if the URL cannot be matched to a file.



How can this kind of conditional rewriting be done under Jetty?

If a custom handler must be written, how would you guys query the server to figure out if a given path matches a servlet or a file?

Your help would be very much appreciated.

Nicolas Therrien

Senior Software Developer

https://www.motorolasolutions.com/content/dam/msi/images/logos/corporate/msiemailsignature.png

o: +1.819.931.2053  

_______________________________________________
jetty-users mailing list
[hidden email]
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users
_______________________________________________
jetty-users mailing list
[hidden email]
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://urldefense.proofpoint.com/v2/url?u=https-3A__dev.eclipse.org_mailman_listinfo_jetty-2Dusers&d=DwICAg&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=XBMqXERb3CMzEhB3CxjIVZjiYbvYbH9drgozwYmWKh4&e=

_______________________________________________
jetty-users mailing list
[hidden email]
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users