Working with the Simple Web Server

The Simple Web Server was added to the jdk.httpserver module in JDK 18. It is a minimal HTTP static file server, designed to be used for prototyping, testing, and debugging. This article explores some less obvious yet interesting programmatic applications of the Simple Web Server.

Introduction

The Simple Web Server is run with jwebserver on the command line. It serves static files in a single directory hierarchy over HTTP/1.1; dynamic content and other HTTP versions are not supported. In addition to a command-line tool, the Simple Web Server provides an API for programmatic creation and customization of the server and its components. This API extends the com.sun.net.httpserver package, which has been included in the JDK since 2006 and is officially supported.

This article focuses on the Simple Web Server API and describes several ways to work with the server and its components that go beyond the common usage of the jwebserver tool. In particular, the following applications are examined:

  • creating an in-memory file server,
  • serving a zip file system,
  • serving a JRT directory,
  • combining a file handler with a canned response handler.

Creating an In-Memory File Server

The Simple Web Server is commonly used to serve a directory hierarchy on the local default file system. For example, the command jwebserver serves the files of the current working directory. While this is suitable for certain uses cases (e.g. sharing and browsing files across the network), it can be a hinderance for others. Let’s take the case of API stubbing, where a mock directory structure is used to simulate expected response patterns. In this case it can be advantageous to steer clear of any file system manipulation and instead work with an in-memory file system, in order to avoid the tedious creation and subsequent deletion of test resources.

Luckily, the Simple Web Server, more precisely its file handler, supports non-default file system paths - the only requirement is that the path’s file system implements the java.nio.file API. Google’s Java in-memory file system Jimfs does just that, which means we can use it to create in-memory resources and serve them with the Simple Web Server. Here’s how to do that:

package org.example;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

/*
 * A Simple Web Server as in-memory server, using Jimfs.
 */
public class SWSJimFS {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);

    /* Creates an in-memory directory hierarchy and starts a Simple Web Server
     * to serve it.
     *
     * Upon receiving a GET request, the server sends a response with a status
     * of 200 if the relative URL matches /some/thing or /some/other/thing.
     * Query parameters are ignored. The body of the response will be a directory
     * listing in html and a Content-type header will be sent with a value of
     * "text/html; charset=UTF-8".
     */
    public static void main( String[] args ) throws Exception {
        Path root = createDirectoryHierarchy();
        var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
        server.start();
    }

    private static Path createDirectoryHierarchy() throws IOException {
        FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
        Path root = fs.getPath("/");

        /* Create directory hierarchy:
         *    |-- root
         *        |-- some
         *            |-- thing
         *            |-- other
         *                |-- thing
         */

        Files.createDirectories(root.resolve("some/thing"));
        Files.createDirectories(root.resolve("some/other/thing"));
        return root;
    }
}

The result is a neat run-time only solution without any actual file system manipulation. And while the example only creates a few test directories for the sake of brevity, one can easily use Files::write to create mock files with content, if required.

Serving a Zip File System

Another interesting use case is serving the contents of a zip file system. In this case, the path of its root entry is passed to the Simple Web Server.

package org.example;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import com.sun.net.httpserver.SimpleFileServer;

import static java.nio.file.StandardOpenOption.CREATE;

/*
 * A Simple Web Server that serves the contents of a zip file system.
 */
public class SWSZipFS {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);
    static final Path CWD = Path.of(".").toAbsolutePath();

    /* Creates a zip file system and starts a Simple Web Server to serve its
     * contents.
     *
     * Upon receiving a GET request, the server sends a response with a status
     * of 200 if the relative URL matches /someFile.txt, otherwise a 404 response
     * is sent. Query parameters are ignored.
     * The body of the response will be the content of the file "Hello world!"
     * and a Content-type header will be sent with a value of "text/plain".
     */
    public static void main( String[] args ) throws Exception {
        Path root = createZipFileSystem();
        var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, SimpleFileServer.OutputLevel.VERBOSE);
        server.start();
    }

    private static Path createZipFileSystem() throws Exception {
        var path = CWD.resolve("zipFS.zip").toAbsolutePath().normalize();
        var fs = FileSystems.newFileSystem(path, Map.of("create", "true"));
        assert fs != FileSystems.getDefault();
        var root = fs.getPath("/");  // root entry

        /* Create zip file system:
         *    |-- root
         *        |-- aFile.txt
         */

        Files.writeString(root.resolve("someFile.txt"), "Hello world!", CREATE);
        return root;
    }
}

Serving a Java Runtime Directory

There are times where it can be helpful to inspect the class files in the run-time image of a remote system, mainly for diagnosability. This can easily be achieved by starting a Simple Web Server that serves the modules directory of the given run-time image. This directory contains one subdirectory for each module in the image. To access it programmatically, the jrt:/ file system is loaded and then used to inspect the directory and retrieve the class files available at run-time. (See JEP 220 for more details on the jrt file system.)

package org.example;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.FileSystems;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

public class SWSJRT {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);

    public static void main( String[] args ) {
        var fs = FileSystems.getFileSystem(URI.create("jrt:/"));
        var root = fs.getPath("modules").toAbsolutePath();
        var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
        server.start();
    }
}

Class and resource files can then be requested over the network from another machine for local examination. Here’s an example of a request of the java.base/java/lang/Object.class file with curl:

$ curl -OL http://<address>:<port>/java.base/java/lang/Object.class

Combining a File Handler With a Canned Response Handler

Having looked at these different applications of the Simple Web Server, let us turn to its core component, the file handler, and explore another interesting use case. Specifically, what if one wants to complement the handler to support request methods other than GET and HEAD?

If a request of a method other than GET or HEAD is received, the file handler produces a 501 - Not Implemented or a 405 - Not Allowed. However, there might scenarios where a different response is required. For this, the file handler can be combined with a conditional canned response handler, using HttpHandlers::handleOrElse:

package org.example;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.util.function.Predicate;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpHandlers;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.Request;
import com.sun.net.httpserver.SimpleFileServer;

import static com.sun.net.httpserver.SimpleFileServer.OutputLevel;

/*
 * HttpServer with a handler that combines Simple Web Server's file handler with
 * a canned response handler for DELETE requests.
 */
public class SWSHandlerWithDeleteHandler {
    private static final InetSocketAddress LOOPBACK_ADDR =
            new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);
    static final Path CWD = Path.of(".").toAbsolutePath();


    /* Creates an HttpServer with a conditional handler that combines two
     * handlers: (1) A file handler for the current working directory and
     * (2) a canned response handler for DELETE requests.
     *
     * If a DELETE request is received, the server sends a 204 response.
     * The body of the response will be empty.
     * All other requests are handled by the file handler, equivalent to the
     * previous example.
     */
    public static void main( String[] args ) throws Exception {
        var fileHandler = SimpleFileServer.createFileHandler(CWD);
        var deleteHandler = HttpHandlers.of(204, Headers.of(), "");
        Predicate<Request> IS_DELETE = r -> r.getRequestMethod().equals("DELETE");
        var handler = HttpHandlers.handleOrElse(IS_DELETE, deleteHandler, fileHandler);
    
        var outputFilter = SimpleFileServer.createOutputFilter(System.out, OutputLevel.VERBOSE);
        var server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, outputFilter);
        server.start();
    }
}

Here, a fileHandler and a deleteHandler with pre-set response state (code 204, no additional headers, no body) are created. The two handlers are then combined to a third handler, which delegates incoming requests based on the request method. If the method is “DELETE”, handling is delegated to the deleteHandler, otherwise it is delegated to the fileHandler. Note that we also make use of the Simple Web Server’s output filter by adding it to the server instance for verbose logging to System.out.

This mechanism can also be used to complement or override the behaviour of the file handler based on other request state like the request URI or the request headers. As such, HttpHandlers::handleOrElse is a powerful API point to tailor the handler behaviour for specific use cases.

Conclusion

While the jwebserver tool suffices in many cases, the Simple Web Server API can help tackle certain less common scenarios and corner cases; the article showcased some of them. The Simple Web Server was designed to make prototyping, debugging, and testing easier - we hope the combination of minimal command-line tool and flexible API achieved this goal.