Discussion:
[jetty-users] Embedded Jetty and Angular 6: only rewrite URLs that don`t match any servlet or files
Nicolas Therrien
2018-09-10 22:00:51 UTC
Permalink
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:
http://localhost:8080/mypage
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:
https://angular.io/guide/router
https://angular.io/guide/deployment

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

*[image:
Loading Image...]*

*o*: +1.819.931.2053
Joakim Erdfelt
2018-09-11 01:34:38 UTC
Permalink
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.

See
https://docs.oracle.com/javaee/7/api/javax/servlet/RequestDispatcher.html
and all of the ERROR_* constants.

Joakim Erdfelt / ***@webtide.com


On Mon, Sep 10, 2018 at 4:59 PM Nicolas Therrien <
Post by 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.
http://localhost:8080/mypage
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.
https://angular.io/guide/router
https://angular.io/guide/deployment
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.
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"
// 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
ctx.addValve(new RewriteValve());
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
To change your delivery options, retrieve your password, or unsubscribe
from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users
Nicolas Therrien
2018-09-15 14:07:29 UTC
Permalink
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

*[image:
https://www.motorolasolutions.com/content/dam/msi/images/logos/corporate/msiemailsignature.png]*

*o*: +1.819.931.2053
Post by Joakim Erdfelt
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.
See
https://docs.oracle.com/javaee/7/api/javax/servlet/RequestDispatcher.html
<https://urldefense.proofpoint.com/v2/url?u=https-3A__docs.oracle.com_javaee_7_api_javax_servlet_RequestDispatcher.html&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=D1kpDqrzg_vuhuc9UcfywK6jA4XmCli5HfL6vrf7zpA&e=>
and all of the ERROR_* constants.
On Mon, Sep 10, 2018 at 4:59 PM Nicolas Therrien <
Post by 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.
http://localhost:8080/mypage
<https://urldefense.proofpoint.com/v2/url?u=http-3A__localhost-3A8080_mypage&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=wxv6U6jkfXLrlygr0oi68Qj844IUh4t_kCIlXYiFZSs&e=>
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
<https://urldefense.proofpoint.com/v2/url?u=http-3A__localhost-3A8080_-23_mypage&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=RrPWclANmuoUpHv9BxIl1Nnydr1UeY7mKFe5HsNQjpE&e=>
in my browser.
If I type http://localhost:8080/mypage
<https://urldefense.proofpoint.com/v2/url?u=http-3A__localhost-3A8080_mypage&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=wxv6U6jkfXLrlygr0oi68Qj844IUh4t_kCIlXYiFZSs&e=>,
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.
https://angular.io/guide/router
<https://urldefense.proofpoint.com/v2/url?u=https-3A__angular.io_guide_router&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=2ZoHdLbWQVoE6WohfEESuLTskCxsNAoD54Iff_A7hvw&e=>
https://angular.io/guide/deployment
<https://urldefense.proofpoint.com/v2/url?u=https-3A__angular.io_guide_deployment&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=3F90xhwQtG5RGsFF66CruKKPvTZKCxlhV9wPq-5SM_o&e=>
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.
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"
// 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
ctx.addValve(new RewriteValve());
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
To change your delivery options, retrieve your password, or unsubscribe
from this list, visit
https://dev.eclipse.org/mailman/listinfo/jetty-users
<https://urldefense.proofpoint.com/v2/url?u=https-3A__dev.eclipse.org_mailman_listinfo_jetty-2Dusers&d=DwMFaQ&c=q3cDpHe1hF8lXU5EFjNM_A&r=P3_1pTtMQK06fFymYIWbyyzVU6nc0CcwfuZhLhexammvaiCaU0ieHeI7BWvfbbjE&m=5VHcVIOmMAA3s1Gy9YhxJU_BQjnRsqmR5bwFHo6k_pU&s=XBMqXERb3CMzEhB3CxjIVZjiYbvYbH9drgozwYmWKh4&e=>
_______________________________________________
jetty-users mailing list
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=
Loading...