A Simple HTTP Server in Java

I often find myself needing a very simple HTTP server for things like mocking out external services for testing purposes. In these cases, Tomcat or even Jetty feel like overkill; I just want something that will start and stop really fast and that will allow me to manipulate any arbitrary byte of the response. I usually re-write the same little dedicated app from scratch each time, but I finally broke down this week and coded it up as a reusable component.

Figure 1: Http Server Overview

Figure 1 illustrates the high-level structure of my simple HTTP server. The main class, HttpServer, is responsible for listening on port 80 (or 8080, if you don't have root privileges) and spawning an instance of SocketHandler for each client connection. Its start method, shown in listing 1, below, enters an infinite loop, spawning a new thread for each accepted connection and handing the new socket off to a new SocketHandler and then awaiting a new client connection. A "real" web server would use a thread pool to avoid running out of threads and memory and crashing the whole process, but for simple mock/testing purposes, this works perfectly.

  public void start() throws IOException  {
    ServerSocket socket = new ServerSocket(port);
    System.out.println("Listening on port " + port);
    Socket client;
    while ((client = socket.accept()) != null)  {
      System.out.println("Received connection from " + client.getRemoteSocketAddress().toString());
      SocketHandler handler = new SocketHandler(client, handlers);
      Thread t = new Thread(handler);
      t.start();
    }
  }

Listing 1: HttpServer.start

SocketHandler, in turn, is responsible for implementing the bulk of the HTTP protocol itself. Its goal is to create a Request object, fill it in with the parsed method, path, version and request headers, create a Response object and hand that off to the associated Handler to actually fill in and respond to the client. Most of the actual parsing of the HTTP request is handed off to the Request object itself. This doesn't even try to implement the javax.servlet.http.HttpRequest interface — my goal here is to keep things simple and flexible. The request parsing is illustrated in listing 2.

  public boolean parse() throws IOException {
    String initialLine = in.readLine();
    log(initialLine);
    StringTokenizer tok = new StringTokenizer(initialLine);
    String[] components = new String[3];
    for (int i = 0; i < components.length; i++) {
      if (tok.hasMoreTokens())  {
        components[i] = tok.nextToken();
      } else  {
        return false;
      }
    }

    method = components[0];
    fullUrl = components[1];

    // Consume headers
    while (true)  {
      String headerLine = in.readLine();
      log(headerLine);
      if (headerLine.length() == 0) {
        break;
      }

      int separator = headerLine.indexOf(":");
      if (separator == -1)  {
        return false;
      }
      headers.put(headerLine.substring(0, separator),
        headerLine.substring(separator + 1));
    }

    if (components[1].indexOf("?") == -1) {
      path = components[1];
    } else  {
      path = components[1].substring(0, components[1].indexOf("?"));
      parseQueryParameters(components[1].substring(
        components[1].indexOf("?") + 1));
    }

    if ("/".equals(path)) {
      path = "/index.html";
    }

    return true;
  }

Listing 2: Request.parse

Per the HTTP standard, Request expects a CR-LF delimited list of lines whose first line is of the form: VERB PATH VERSION followed by a variable-length list of headers in the form NAME: VALUE and a closing empty line indicating that the header list is complete. If the VERB supports an entity-body (like POST or PUT), the rest of the request is that entity body. I'm only worrying about GETs here, so I assume there's no entity body. Once this method completes, assuming everything was syntactically correct, Request's internal method, path, fullUrl and headers member variables are filled in. Also, since I almost always need to handle query parameters (that is, the stuff passed in after the '?' in the URL), I go ahead and parse these here as shown in listing 3. This is a departure from most other HTTP servers which delegate this sort of parsing to higher-level framework code, but for my purposes, it's very helpful.

  private void parseQueryParameters(String queryString) {
    for (String parameter : queryString.split("&")) {
      int separator = parameter.indexOf('=');
      if (separator > -1) {
        queryParameters.put(parameter.substring(0, separator),
          parameter.substring(separator + 1));
      } else  {
        queryParameters.put(parameter, null);
      }
    }
  }

Listing 3: Request.parseQueryParameters

Once the request has been successfully parsed, the controlling SocketHandler can go ahead and look to see if it has a Handler for the queried path. This suggests that there must be a handler already in place; the job of HttpServers addHandler, shown in listing 4, is to associated a method and a path to a handler.

  private Map<String, Map<String, Handler>> handlers;

  public void addHandler(String method, String path, Handler handler) {
    Map<String, Handler> methodHandlers = handlers.get(method);
    if (methodHandlers == null) {
      methodHandlers = new HashMap<String, Handler>();
      handlers.put(method, methodHandlers);
    }
    methodHandlers.put(path, handler);
  }

Listing 4: HttpServer.addHandler

This is simply implemented as a map of maps of strings to handlers - this way, GET /index.html could be handled by a different handler than DELETE /index.html. So now that SocketHandler has parsed out the method and the path that the client has requested, it looks for a handler as shown in listing 5.

  public void run() {
    BufferedReader in = null;
    OutputStream out = null;

    try {
      in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      out = socket.getOutputStream();

      Request request = new Request(in);
      if (!request.parse()) {
        respond(500, "Unable to parse request", out);
        return;
      }

      boolean foundHandler = false;
      Response response = new Response(out);
      Map<String, Handler> methodHandlers = handlers.get(request.getMethod());
      if (methodHandlers == null) {
        respond(405, "Method not supported", out);
        return;
      }

      for (String handlerPath : methodHandlers.keySet())  {
        if (handlerPath.equals(request.getPath()))  {
          methodHandlers.get(request.getPath()).handle(request, response);
          response.send();
          foundHandler = true;
          break;
        }
      }

      ...

Listing 5: SocketHandler.run

If the request doesn't parse correctly, the SocketHandler returns immediately with an error code 500; otherwise, it checks to see if a handler for the given method and path. If so, it hands it the fully parsed Request and newly instantiated Response; the Handler is responsible for responding to the client through the Response class. However, to simplify things a little bit, I allow a "default" handler to be installed at path "/*" as show in listing 6. Here, again, "real" web servers allow a lot more flexibility in associating paths with handlers — a significant source of complexity that I deliberately sidestepped here. If you need that sort of flexibility, you're probably better off biting the bullet and running Tomcat or Jetty.

      if (!foundHandler)  {
        if (methodHandlers.get("/*") != null) {
          methodHandlers.get("/*").handle(request, response);
          response.send();
        } else  {
          respond(404, "Not Found", out);
        }
      }

Listing 6: SocketHandler.run default handler

Response in listing 7 is a relatively passive class; it "HTTP-izes" what the Handler sends to it but is otherwise fairly simple. The HTTP protocol expects the server to respond with the text HTTP/1.1 STATUS MESSAGE, a variable-length list of headers in the from NAME: VALUE, a blank line, and then the response body. The Handler should set a response code, invoke addHeader once for each response header it plans to return, add a body and then invoke send to complete the response.

  public void setResponseCode(int statusCode, String statusMessage) {
    this.statusCode = statusCode;
    this.statusMessage = statusMessage;
  }

  public void addHeader(String headerName, String headerValue)  {
    this.headers.put(headerName, headerValue);
  }

  public void addBody(String body)  {
    headers.put("Content-Length", Integer.toString(body.length()));
    this.body = body;
  }

  public void send() throws IOException {
    headers.put("Connection", "Close");
    out.write(("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n").getBytes());
    for (String headerName : headers.keySet())  {
      out.write((headerName + ": " + headers.get(headerName) + "\r\n").getBytes());
    }
    out.write("\r\n".getBytes());
    if (body != null) {
      out.write(body.getBytes());
    }
  }

Listing 7: Response

This completes the HTTP protocol (pretty simple, isn't it?), but what about the handler itself? The interface is simple enough as shown in listing 8.

public interface Handler  {
  public void handle(Request request, Response response) throws IOException;
}

Listing 8: Handler

Since each subclass of Handler is only instantiated once, to be associated with the method and path it handles, each implementation must be thread safe. In practice, that's easy enough to do by putting all the processing inside the handle implementation. Note that I don't have any provision in here for anything like javax.servlet.http.HttpSession — again, that's a little more complex than the stub purposes I'm putting this to. Listing 9 illustrates the most fundamental of all Http Handlers: the file handler which looks for a file match the path name and returns it.

public class FileHandler implements Handler {
  public void handle(Request request, Response response) throws IOException {
    try {
      FileInputStream file = new FileInputStream(request.getPath().substring(1));
      response.setResponseCode(200, "OK");
      response.addHeader("Content-Type", "text/html");
      StringBuffer buf = new StringBuffer();
      // TODO this is slow
      int c;
      while ((c = file.read()) != -1) {
        buf.append((char) c);
      }
      response.addBody(buf.toString());
    } catch (FileNotFoundException e) {
      response.setResponseCode(404, "Not Found");
    }
  }
}

Listing 9: FileHandler

Finally, listing 10 illustrates a sample use of this simple library which returns a custom HTML response for the path "/hello" and responds to every other request by looking for a file (or returning a 404).

    HttpServer server = new HttpServer(8080);
    server.addHandler("GET", "/hello", new Handler() {
      public void handle(Request request, Response response) throws IOException {
        String html = "<body>It works, " + request.getParameter("name") + "</body>";
        response.setResponseCode(200, "OK");
        response.addHeader("Content-Type", "text/html");
        response.addBody(html);
      }
    });
    server.addHandler("GET", "/*", new FileHandler());  // Default handler
    server.start();

Listing 10: Simple mock HTTP server

Listing 11 includes all of the source code in this post, along with a few extraneous things I omitted in the prior listings above.

import java.util.Map;
import java.util.HashMap;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.StringTokenizer;

public class Request  {
  private String method;
  private String path;
  private String fullUrl;
  private Map<String, String> headers = new HashMap<String, String>();
  private Map<String, String> queryParameters = new HashMap<String, String>();
  private BufferedReader in; 

  public Request(BufferedReader in)  {
    this.in = in;
  }

  public String getMethod()  {
    return method;
  }

  public String getPath()  {
    return path;
  }

  public String getFullUrl()  {
    return fullUrl;
  }

  // TODO support mutli-value headers
  public String getHeader(String headerName)  {
    return headers.get(headerName);
  }

  public String getParameter(String paramName)  {
    return queryParameters.get(paramName);
  }

  private void parseQueryParameters(String queryString)  {
    for (String parameter : queryString.split("&"))  {
      int separator = parameter.indexOf('=');
      if (separator > -1)  {
        queryParameters.put(parameter.substring(0, separator),
          parameter.substring(separator + 1));
      } else  {
        queryParameters.put(parameter, null);
      }
    }
  }

  public boolean parse() throws IOException  {
    String initialLine = in.readLine();
    log(initialLine);
    StringTokenizer tok = new StringTokenizer(initialLine);
    String[] components = new String[3];
    for (int i = 0; i < components.length; i++)  {
      // TODO support HTTP/1.0?
      if (tok.hasMoreTokens())  {
        components[i] = tok.nextToken();
      } else  {
        return false;
      }
    }

    method = components[0];
    fullUrl = components[1];

    // Consume headers
    while (true)  {
      String headerLine = in.readLine();
      log(headerLine);
      if (headerLine.length() == 0)  {
        break;
      }

      int separator = headerLine.indexOf(":");
      if (separator == -1)  {
        return false;
      }
      headers.put(headerLine.substring(0, separator),
        headerLine.substring(separator + 1));
    }

    // TODO should look for host header, Connection: Keep-Alive header, 
    // Content-Transfer-Encoding: chunked

    if (components[1].indexOf("?") == -1)  {
      path = components[1];
    } else  {
      path = components[1].substring(0, components[1].indexOf("?"));
      parseQueryParameters(components[1].substring(
        components[1].indexOf("?") + 1));
    }

    if ("/".equals(path))  {
      path = "/index.html";
    }

    return true;
  }

  private void log(String msg)  {
    System.out.println(msg);
  }

  public String toString()  {
    return method  + " " + path + " " + headers.toString();
  }
}

import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import java.util.HashMap;

/** 
 * Encapsulate an HTTP Response.  Mostly just wrap an output stream and
 * provide some state.
 */
public class Response  {
  private OutputStream out;
  private int statusCode;
  private String statusMessage;
  private Map<String, String> headers = new HashMap<String, String>();
  private String body;

  public Response(OutputStream out)  {
    this.out = out;
  }

  public void setResponseCode(int statusCode, String statusMessage)  {
    this.statusCode = statusCode;
    this.statusMessage = statusMessage;
  }

  public void addHeader(String headerName, String headerValue)  {
    this.headers.put(headerName, headerValue);
  }

  public void addBody(String body)  {
    headers.put("Content-Length", Integer.toString(body.length()));
    this.body = body;
  }

  public void send() throws IOException  {
    headers.put("Connection", "Close");
    out.write(("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n").getBytes());
    for (String headerName : headers.keySet())  {
      out.write((headerName + ": " + headers.get(headerName) + "\r\n").getBytes());
    }
    out.write("\r\n".getBytes());
    if (body != null)  {
      out.write(body.getBytes());
    }
  }
}

import java.io.IOException;
import java.util.Map;
import java.io.BufferedReader;
import java.io.OutputStream;

/**
 * Handlers must be thread safe.
 */
public interface Handler  {
  public void handle(Request request, Response response) throws IOException;
}

import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.BufferedReader;

class SocketHandler implements Runnable  {
  private Socket socket;
  private Handler defaultHandler;
  private Map<String, Map<String, Handler>> handlers;

  public SocketHandler(Socket socket, 
                       Map<String, Map<String, Handler>> handlers)  {
    this.socket = socket;
    this.handlers = handlers;
  }

  /**
   * Simple responses like errors.  Normal reponses come from handlers.
   */
  private void respond(int statusCode, String msg, OutputStream out) throws IOException  {
    String responseLine = "HTTP/1.1 " + statusCode + " " + msg + "\r\n\r\n";
    log(responseLine);
    out.write(responseLine.getBytes());
  }

  public void run()  {
    BufferedReader in = null;
    OutputStream out = null;

    try  {
      in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      out = socket.getOutputStream();

      Request request = new Request(in);
      if (!request.parse())  {
        respond(500, "Unable to parse request", out);
        return;
      }

      // TODO most specific handler
      boolean foundHandler = false;
      Response response = new Response(out);
      Map<String, Handler> methodHandlers = handlers.get(request.getMethod());
      if (methodHandlers == null)  {
        respond(405, "Method not supported", out);
        return;
      }

      for (String handlerPath : methodHandlers.keySet())  {
        if (handlerPath.equals(request.getPath()))  {
          methodHandlers.get(request.getPath()).handle(request, response);
          response.send();
          foundHandler = true;
          break;
        }
      }
      
      if (!foundHandler)  {
        if (methodHandlers.get("/*") != null)  {
          methodHandlers.get("/*").handle(request, response);
          response.send();
        } else  {
          respond(404, "Not Found", out);
        }
      }
    } catch (IOException e)  {
      try  {
        e.printStackTrace();
        if (out != null)  {
          respond(500, e.toString(), out);
        }
      } catch (IOException e2)  {
        e2.printStackTrace();
        // We tried
      }
    } finally  {
      try  {
        if (out != null)  {
          out.close();
        }
        if (in != null)  {
          in.close();
        }
        socket.close();
      } catch (IOException e)  {
        e.printStackTrace();
      }
    }
  }

  private void log(String msg)  {
    System.out.println(msg);
  }
}

public class HttpServer  {
  private int port;
  private Handler defaultHandler = null;
  // Two level map: first level is HTTP Method (GET, POST, OPTION, etc.), second level is the
  // request paths.
  private Map<String, Map<String, Handler>> handlers = new HashMap<String, Map<String, Handler>>();

  // TODO SSL support
  public HttpServer(int port)  {
    this.port = port;
  }

  /**
   * @param path if this is the special string "/*", this is the default handler if
   *   no other handler matches.
   */
  public void addHandler(String method, String path, Handler handler)  {
    Map<String, Handler> methodHandlers = handlers.get(method);
    if (methodHandlers == null)  {
      methodHandlers = new HashMap<String, Handler>();
      handlers.put(method, methodHandlers);
    }
    methodHandlers.put(path, handler);
  }

  public void start() throws IOException  {
    ServerSocket socket = new ServerSocket(port);
    System.out.println("Listening on port " + port);
    Socket client;
    while ((client = socket.accept()) != null)  {
      System.out.println("Received connection from " + client.getRemoteSocketAddress().toString());
      SocketHandler handler = new SocketHandler(client, handlers);
      Thread t = new Thread(handler);
      t.start();
    }
  }

  public static void main(String[] args) throws IOException  {
    HttpServer server = new HttpServer(8080);
    server.addHandler("GET", "/hello", new Handler()  {
      public void handle(Request request, Response response) throws IOException  {
        String html = "It works, " + request.getParameter("name") + "";
        response.setResponseCode(200, "OK");
        response.addHeader("Content-Type", "text/html");
        response.addBody(html);
      }
    });
    server.addHandler("GET", "/*", new FileHandler());  // Default handler
    server.start();
  }
}

Listing 11: Full (mini) HTTP server

In my next post, I'll extend this to include support for POST requests and chunked response bodies — that is, response bodies whose lengths aren't known or which can't fit in memory.

Add a comment:

Completely off-topic or spam comments will be removed at the discretion of the moderator.

You may preserve formatting (e.g. a code sample) by indenting with four spaces preceding the formatted line(s)

Name: Name is required
Email (will not be displayed publicly):
Comment:
Comment is required
My Book

I'm the author of the book "Implementing SSL/TLS Using Cryptography and PKI". Like the title says, this is a from-the-ground-up examination of the SSL protocol that provides security, integrity and privacy to most application-level internet protocols, most notably HTTP. I include the source code to a complete working SSL implementation, including the most popular cryptographic algorithms (DES, 3DES, RC4, AES, RSA, DSA, Diffie-Hellman, HMAC, MD5, SHA-1, SHA-256, and ECC), and show how they all fit together to provide transport-layer security.

My Picture

Joshua Davies

Past Posts