JEP 408: Simple Web Server

OwnerJulia Boes
TypeFeature
ScopeJDK
StatusCandidate
Componentcore-libs / java.net
Discussionnet dash dev at openjdk dot java dot net
EffortS
DurationS
Reviewed byBrian Goetz, Chris Hegarty, Daniel Fuchs
Created2021/01/27 12:47
Updated2021/08/18 09:35
Issue8260510

Summary

Provide a command-line tool to start a minimal web server that serves static files in the current directory. This low-threshold utility will be useful for prototyping, ad-hoc coding, and testing purposes, particularly in educational contexts.

Goals

Non-Goals

Motivation

A common rite of passage for developers is to serve a file on the web, likely a “Hello, world!” HTML file. Most computer science curricula introduce students to web development, where local testing servers are commonly used. Developers usually also learn about system administration and web services, other areas where development tools with basic server functionality can come in handy. Educational and informal tasks such as these are where a small out-of-the-box server is desirable. Use cases include:

In all these cases we can, of course, use a web-server framework, but that approach has a high activation energy: We have to look for options, pick one, download it, configure it, and figure out how to use it before we can serve our first request. These steps amount to quite a bit of ceremony, which is a drawback; getting stuck somewhere on the way can be frustrating and might even hinder the further use of Java. A basic web server spun up from the command line or via a few lines of code lets us bypass this ceremony, so that we can instead focus on the task at hand.

Python, Ruby, PHP, Erlang, and many other platforms offer out-of-the-box servers run from the command line. This variety of existing alternatives demonstrates a recognized need for this type of tool.

Description

Command-line tool

The following command starts the server:

$ java -m jdk.httpserver [-b bind address] [-p port] [-d directory]
                         [-o none|info|verbose] [-h to show options]

The command-line options can be used to specify the bind address and port, the directory, and the output format for request information. If startup is successful then the server prints a message to System.out listing the local address and the absolute path of the directory being served. For example:

$ java -m jdk.httpserver -b 127.0.0.1
Serving /current/directory (and subdirectories) on 
http://127.0.0.1:8000/ ...

The output filter, which can be set via the -o option, prints information about HTTP requests. With the output format set to info or verbose, information about each request is printed upon receiving it. For example, the info format produces output of the form:

127.0.0.1 - - [10/Feb/2021:14:34:11 +0000] "GET /some/subdirectory/ HTTP/1.1" 200 –

The verbose format additionally includes the request and response headers as well as the absolute path of the requested resource.

Once started successfully, the server runs until it is stopped. On Unix platforms, the server can be stopped by sending it a SIGINT signal (Ctrl+C in a terminal window).

The -h option will display a help message listing the options. The options follow the guidelines described in JEP 293.

Options:
-b, --bind-address    - Address to bind to. Default: 0.0.0.0 (all interfaces).
-d, --directory       - Directory to serve. Default: current directory.
-o, --output          - Output format. none|info|verbose. Default: info.
-p, --port            - Port to listen on. Default: 8000.
-h, -?, --help        - Print this help message.
To stop the server, press Ctrl + C.

Implementation

The command above runs the main method of the unexported implementation class, which is the entry point of the existing jdk.httpserver module. The implementation uses the existing public APIs HttpServer, HttpHandler, and Filter in the com.sun.net.httpserver package. The server runs in the foreground and binds to the wildcard address and port 8000 by default. The server handles only the idempotent HEAD and GET requests. It maps GET requests to the structure of the given directory, with the current working directory as default.

If the requested resource is a file, its content is served. If the requested resource is a directory that contains an index file, the content of the index file is served. Otherwise, the names of all files and subdirectories of the directory are listed. Symbolic links and hidden files are not listed or served.

MIME types are supported via the java.net.URLConnection::getFileNameMap API.

API

While the command-line tool is useful, what if one wants to use the components of the simple server (i.e., server, handler, and filter) with existing code, or further customize the behavior of the handler? While some configuration is possible on the command line, a concise and intuitive programmatic solution for creation and customization would improve the utility of the server components. To bridge the gap between the simplicity of the command-line tool and the write-it-yourself approach of the current API, we define new APIs for server creation and customized request handling.

The new classes are SimpleFileServer, HttpHandlers and Request, each built on the existing jdk.httpserver abstractions HttpServer, HttpHandler, Filter, and HttpExchange.

The SimpleFileServer class supports the creation of a file server, a file-server handler, and an output filter:

package com.sun.net.httpserver;

public final class SimpleFileServer {
    public static HttpServer createFileServer(InetSocketAddress addr,
                                              Path rootDirectory,
                                              OutputLevel outputLevel) {...}
    public static HttpHandler createFileHandler(Path rootDirectory) {...}
    public static Filter createOutputFilter(OutputStream out,
                                            OutputLevel outputLevel) {...}
    ...
}

With this class, a minimal yet customized server can be started in a few lines of code in jshell:

jshell> var server = SimpleFileServer.createFileServer(8080,
   ...> Path.of("/some/path"), OutputLevel.VERBOSE);
jshell> server.start()

A customized file-server handler can be added to an existing server:

jshell> var server = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/store/", new SomePutHandler());
jshell> var handler = SimpleFileServer.createFileHandler(Path.of("/some/path"));
jshell> server.createContext("/browse/", handler);
jshell> server.start();

A customized output filter can be added to a server during creation:

jshell> var filter = SimpleFileServer.createOutputFilter(System.out,
   ...> OutputLevel.INFO);
jshell> var server = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/store/", new SomePutHandler(), filter);
jshell> server.start();

These last two examples are enabled by new overloaded create methods in the HttpServer and HttpsServer classes:

public static HttpServer create(InetSocketAddress addr,
                                int backlog,
                                String root,
                                HttpHandler handler,
                                Filter... filters) throws IOException {...}

Enhanced request handling

The core functionality of the simple server is provided by its handler. To support extending this handler for use with existing code we introduce a new HttpHandlers class with two static methods for handler creation and customization as well as a new method in the Filter class for adapting a request:

package com.sun.net.httpserver;

public final filterclass HttpHandlers {
    public static HttpHandler handleOrElse(Predicate<Request> handlerTest,
                                           HttpHandler handler,
                                           HttpHandler fallbackHandler) {...}
    public static HttpHandler of(int statusCode, Headers headers, String body) {...}
    {...}
}

public abstract class Filter {
    public static Filter adaptRequest(String description,
                                      UnaryOperator<Request> requestOperator) {...}
    {...}
}

handleOrElse complements a conditional handler with another handler, while the factory method of lets you create handlers with pre-set response state. The pre-processing filter obtained from adaptRequest can be used to inspect and adapt certain properties of a request before handling it. Use cases for these methods include delegating exchanges based on the request method, creating a "canned response" handler that always returns a certain response, or adding a header to all incoming requests.

The existing API captures an HTTP request as part of a request-response pair represented by an instance of the HttpExchange class, which describes the full and mutable state of an exchange. Not all of this state is meaningful for handler customization and adaptation. We therefore introduce a simpler Request interface to provide a limited view of the immutable request state:

public interface Request {
    URI getRequestURI();
    String getRequestMethod();
    Headers getRequestHeaders();
    default Request with(String headerName, List<String> headerValues)
    {...}
}

This enables the straightforward customization of an existing handler, for example:

jshell> var h = HttpHandlers.handleOrElse(r -> r.getRequestMethod().equals("PUT"),
   ...> new SomePutHandler(), new SomeHandler());
jshell> var f = Filter.adaptRequest("Add Foo header", r -> r.with("Foo", List.of("Bar")));
jshell> var s = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/", h, f);
jshell> s.start();

Alternatives

We considered several API alternatives during prototyping:

Testing

The core functionality of the command-line tool is provided by the API, so most of our testing effort will focus on the API. The API points can be tested in isolation with unit tests and the existing test framework. We will focus particularly on file-system access and URI sanitization. We will complement the API tests with command and sanity testing of the command-line tool.

Risks and Assumptions

This simple server is intended for testing, development, and debugging purposes only. Within this scope the general security concerns of servers apply, and will be addressed by following security best practices and thorough testing.