Fussel
Fussel

Reputation: 1865

Embedded Jetty with Angular URL rewrite

I'm using Jetty to deploy a WebSocket and an Angular application. On development it's all working but on production I have the issue that when refreshing the frontend or typing an url I get a 404 from the server that the given resource does not exist. Now I'm trying to create a rewrite rule to redirect requests to my index.html. Initializing my server looks like this:

server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(config.getServer().getPort());
server.addConnector(connector);

RewriteHandler rewriteHandler = new RewriteHandler();
rewriteHandler.setRewriteRequestURI(true);
rewriteHandler.setRewritePathInfo(false);
rewriteHandler.setOriginalPathAttribute("requestedPath");

/* 
RedirectRegexRule rule1 = new RedirectRegexRule();
rule1.setRegex("/(.+)");
rule1.setLocation("/index.html");

rewriteHandler.addRule(rule1);
*/

URL webRootLocation = this.getClass().getResource("/frontend/index.html");
if (webRootLocation == null)
    throw new IllegalStateException("Unable to determine webroot URL location");

URI webRootUri = URI.create(webRootLocation.toURI().toASCIIString().replaceFirst("/index.html$","/"));
logger.debug("Web Root URI: {}",webRootUri);

ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
contextHandler.setBaseResource(Resource.newResource(webRootUri));
contextHandler.setWelcomeFiles(new String[]{ "index.html" });

rewriteHandler.setHandler(contextHandler);

ServerContainer container = WebSocketServerContainerInitializer.initialize(contextHandler);

List<Class<? extends Encoder>> encoders = new ArrayList<>();
encoders.add(MessageEncoder.class);
List<Class<? extends Decoder>> decoders = new ArrayList<>();
decoders.add(MessageDecoder.class);

ServerEndpointConfig endpointConfig = ServerEndpointConfig.Builder
        .create(AppEndpoint.class, "/wss-test")
        .encoders(encoders)
        .decoders(decoders)
        .configurator(new AppEndpointConfig(config, factory))
        .build();
container.addEndpoint(endpointConfig);

//server.setHandler(contextHandler);
HandlerList handlerList = new HandlerList();
handlerList.setHandlers(new Handler[]{rewriteHandler, contextHandler});
server.setHandler(handlerList);

contextHandler.addServlet(DefaultServlet.class, "/");

server.start();
// server.dump(System.err);
server.join();

My Angular frontend is compiled in a resource folder of the application which is served by the server on - for example: localhost:8080/. If my app routs to localhost:8080/some/path everything is fine, but when refreshing the page I get a 404 that some/path is unknown. Using the rewirte rule (commented out) I get ERR_TOO_MANY_REDIRECTIONS also lazy-loaded resources / modules are not found. Any suggestions on how to use Jetty rewrite for an Angular application as I have no glue on how to continue here or does Jetty not support something like the apache rewrite that is suggested by angular deployment

RewriteEngine On
# If an existing asset or directory is requested go to it as it is
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]

# If the requested resource doesn't exist, use index.html
RewriteRule ^ /index.html

Upvotes: 2

Views: 1955

Answers (2)

Fussel
Fussel

Reputation: 1865

For anyone looking for a solution for this I made it work with Jettys RewriteHandler. My servers setup now looks like the following:

server = new Server(config.getServer().getPort());

RewriteHandler rewriteHandler = new RewriteHandler();
rewriteHandler.setRewriteRequestURI(true);
rewriteHandler.setRewritePathInfo(false);
rewriteHandler.setOriginalPathAttribute("requestedPath");

RewriteRegexRule rule1 = new RewriteRegexRule();
rule1.setRegex("^((?!"+wsPath+"|\\.js|\\.css|\\.jpe?g|\\.png).)*$");
rule1.setReplacement("/index.html");
rewriteHandler.addRule(rule1);

URL webRootLocation = this.getClass().getResource("/frontend/index.html");
if (webRootLocation == null)
    throw new IllegalStateException("Unable to determine Web-Root URL location");

URI webRootUri = URI.create(webRootLocation.toURI().toASCIIString().replaceFirst("/index.html$","/"));
logger.debug("Web-Root URI: {}",webRootUri);

ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
contextHandler.setBaseResource(Resource.newResource(webRootUri));
contextHandler.setWelcomeFiles(new String[]{ "index.html" });

rewriteHandler.setHandler(contextHandler);

// Initialize WebSocket
ServerContainer container = WebSocketServerContainerInitializer.initialize(contextHandler);

List<Class<? extends Encoder>> encoders = new ArrayList<>();
encoders.add(MessageEncoder.class);
List<Class<? extends Decoder>> decoders = new ArrayList<>();
decoders.add(MessageDecoder.class);

ServerEndpointConfig endpointConfig = ServerEndpointConfig.Builder
        .create(AppEndpoint.class, "/" + wsPath)
        .encoders(encoders)
        .decoders(decoders)
        .configurator(new AppEndpointConfig(config, factory))
        .build();
container.addEndpoint(endpointConfig);

server.setHandler(rewriteHandler);

contextHandler.addServlet(DefaultServlet.class, "/");

server.start();
server.join();

It's not ideal as only specific file endings match the regex but it does work with a builded and deployed Angular application inside of Jetty. If a RESTful API is used some regex must be defined to match a path of the API like the wsPath (a simple string) in my example.

Upvotes: 2

Joakim Erdfelt
Joakim Erdfelt

Reputation: 49462

The only way to accomplish this in the Servlet world is to have a error page handling the 404 status code and issuing the redirect yourself.

This can only be done within a specific webapp, and not from a generic rewrite handling routine.

Why?

Well, the "If the requested resource doesn't exist, use index.html" is the key.

What happens.

  1. The webapp was entered, and no url-pattern matched, so the default url-pattern (of "/") is used.
  2. The default url-pattern is mapped to what's known as "The Default Servlet"
  3. The Default Servlet is responsible for looking in the webapp's base resource for a matching resource and returning that content as a static resource request.
  4. If the static resource doesn't exist, and the request was for a directory (eg: /js/ then use the list of welcome files to look up a welcome file. (this list is configured in the WEB-INF/web.xml)
<welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>
  1. If the static resource still doesn't exist, process it as a 404 response.

Meanwhile, the error page handling routines of the servlet spec kick in. This will result in a lookup for a path declared for 404 in your WEB-INF/web.xml.

<servlet>
  <servletname>404Handler</servlet-name>
  <servlet-class>com.acme.My404Servlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>404Handler</servlet-name>
  <url-pattern>/404status</url-pattern>
</servlet-mapping>
<!-- ... then later ... -->
<error-page>
    <error-code>404</error-code>
    <location>/404status</location>
</error-page>

That path can be a servlet, a static resource, a jsp, etc. Pretty much anything that you can reference with a path.

If it's a servlet (or jsp) you can interrogate the original request via the request attributes to know why you are handling this error.

See: https://stackoverflow.com/a/32910916/775715

An example of this in embedded-jetty would be ...

package jetty.errors;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.PathResource;

public class EmbeddedWelcomeErrorDemo
{
    public static void main(String[] args) throws Exception
    {
        Server server = new Server(8080);

        String baseDir = System.getProperty("user.home");
        if (args.length > 0)
            baseDir = args[0];

        Path basePath = Paths.get(baseDir);

        if (!Files.exists(basePath) || !Files.isDirectory(basePath))
        {
            throw new IOException("Not a valid directory: " + basePath);
        }

        ServletContextHandler context = new ServletContextHandler();
        context.setContextPath("/");
        context.setBaseResource(new PathResource(basePath));
        context.setWelcomeFiles(new String[]{
            "index.html"
        });

        // Add error page mapping for context
        context.addServlet(ErrorHandling.class, "/errorpage");
        ErrorPageErrorHandler errorMapper = new ErrorPageErrorHandler();
        errorMapper.addErrorPage(404, "/errorpage");
        context.setErrorHandler(errorMapper);

        // to handle static resources against base resource (always last)
        // always named "default" (per spec)
        ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class);
        // assigned to default url-pattern of "/" (per spec)
        context.addServlet(defaultHolder, "/");

        HandlerList handlers = new HandlerList();
        handlers.addHandler(context);
        handlers.addHandler(new DefaultHandler()); // for non-context errors

        server.setHandler(handlers);
        server.start();
        server.join();
    }

    public static class ErrorHandling extends HttpServlet
    {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
        {
            if (req.getDispatcherType() != DispatcherType.ERROR)
            {
                // we didn't get here from a error dispatch.
                // somebody attempted to use this servlet directly.
                resp.setStatus(404);
                return;
            }

            String requestedResource = (String)req.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
            log("[ErrorHandling] Requested resource was " + requestedResource);
            int statusCode = (int)req.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
            switch (statusCode)
            {
                case 404:
                    // let handle it by a redirect
                    resp.sendRedirect("/");
                    break;
                default:
                    // pass the other errors through
                    resp.setStatus(statusCode);
                    break;
            }
        }
    }
}

Some example of what happens.

$ mkdir $HOME/tmp-base
$ mdkir css
$ echo "this is the index.html" > index.html
$ echo "this is my other html" > myother.html
$ echo "this is my fancy css" > css/main.css

Then run the server example with the command line to this directory

$ java ... jetty.errors.EmbeddedWelcomeErrorDemo $HOME/tmp-base
2019-09-24 14:17:55.540:INFO::main: Logging initialized @190ms to org.eclipse.jetty.util.log.StdErrLog
2019-09-24 14:17:55.621:INFO:oejs.Server:main: jetty-9.4.20.v20190813; built: 2019-08-13T21:28:18.144Z; git: 84700530e645e812b336747464d6fbbf370c9a20; jvm 1.8.0_202-b08
2019-09-24 14:17:55.661:INFO:oejsh.ContextHandler:main: Started o.e.j.s.ServletContextHandler@7921b0a2{/,file:///home/joakim/tmp-base/,AVAILABLE}
2019-09-24 14:17:55.674:INFO:oejs.AbstractConnector:main: Started ServerConnector@7cef4e59{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
2019-09-24 14:17:55.674:INFO:oejs.Server:main: Started @325ms

And then make a few requests ...

$ curl -L -vv http://localhost:8080/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 24 Sep 2019 19:26:28 GMT
< Last-Modified: Tue, 24 Sep 2019 19:12:21 GMT
< Content-Type: text/html
< Accept-Ranges: bytes
< Content-Length: 23
< Server: Jetty(9.4.20.v20190813)
< 
this is the index.html
* Connection #0 to host localhost left intact

this was welcome file handling

$ curl -L -vv http://localhost:8080/myother.html
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /myother.html HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 24 Sep 2019 19:21:10 GMT
< Last-Modified: Tue, 24 Sep 2019 19:13:46 GMT
< Content-Type: text/html
< Accept-Ranges: bytes
< Content-Length: 22
< Server: Jetty(9.4.20.v20190813)
< 
This is my other html
* Connection #0 to host localhost left intact

this was normal static file serving

$ curl -L -vv http://localhost:8080/css/main.css
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /css/main.css HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 24 Sep 2019 19:22:22 GMT
< Last-Modified: Tue, 24 Sep 2019 19:22:16 GMT
< Content-Type: text/css
< Accept-Ranges: bytes
< Content-Length: 21
< Server: Jetty(9.4.20.v20190813)
< 
this is my fancy css
* Connection #0 to host localhost left intact

This was normal static file serving

If I make some requests to non-existent resources or directories ....

$ curl -L -vv http://localhost:8080/css/bogus.css
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /css/bogus.css HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Date: Tue, 24 Sep 2019 19:22:46 GMT
< Location: http://localhost:8080/
< Content-Length: 0
< Server: Jetty(9.4.20.v20190813)
< 
* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://localhost:8080/'
* Found bundle for host localhost: 0x5647e1581a50 [can pipeline]
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 24 Sep 2019 19:22:46 GMT
< Last-Modified: Tue, 24 Sep 2019 19:12:21 GMT
< Content-Type: text/html
< Accept-Ranges: bytes
< Content-Length: 23
< Server: Jetty(9.4.20.v20190813)
< 
this is the index.html
* Connection #0 to host localhost left intact

This was handled by the ErrorHandling servlet

$ curl -L -vv http://localhost:8080/this/directory/does/not/exist
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /this/directory/does/not/exist HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Date: Tue, 24 Sep 2019 19:23:02 GMT
< Location: http://localhost:8080/
< Content-Length: 0
< Server: Jetty(9.4.20.v20190813)
< 
* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://localhost:8080/'
* Found bundle for host localhost: 0x561eefa8b020 [can pipeline]
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 24 Sep 2019 19:23:02 GMT
< Last-Modified: Tue, 24 Sep 2019 19:12:21 GMT
< Content-Type: text/html
< Accept-Ranges: bytes
< Content-Length: 23
< Server: Jetty(9.4.20.v20190813)
< 
this is the index.html
* Connection #0 to host localhost left intact

This was handled by the ErrorHandling servlet

[joakim@hyperion tmp]$ curl -L -vv http://localhost:8080/non-existant.jpeg
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /non-existant.jpeg HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Date: Tue, 24 Sep 2019 19:21:18 GMT
< Location: http://localhost:8080/
< Content-Length: 0
< Server: Jetty(9.4.20.v20190813)
< 
* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://localhost:8080/'
* Found bundle for host localhost: 0x563f476b6a50 [can pipeline]
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 24 Sep 2019 19:21:18 GMT
< Last-Modified: Tue, 24 Sep 2019 19:12:21 GMT
< Content-Type: text/html
< Accept-Ranges: bytes
< Content-Length: 23
< Server: Jetty(9.4.20.v20190813)
< 
this is the index.html
* Connection #0 to host localhost left intact

This was handled by the ErrorHandling servlet

Upvotes: 2

Related Questions