HTTP server from scratch: Configuration

This post is the conclusion of HTTP server from scratch. Previous post about modularization can be found here.

In this post we will make our webserver somewhat more configurable. We will add one more switch to the to point to a external directory that will be used to serve the documents from. Since it is a good idea to support compression of the documents that we send, we will add support to gzip compression and finally, we will update our build configuration to generate a property distribution.


The new configuration property that we will add is the web.root, default setting will point to server.root, when running it outside of the IDE we will be able to use -Dweb.root=/path/to/web/root.

Before we can do that we have add one more HTTP status code 400 Bad Request. According to the HTTP spec, the resource the user-agent can request should be a absolute path and not a relative path. As a defensive approach we will sanitize the incoming request to contain only absolute paths, otherwise we will return with HTTP status code 400 Bad Request.

So far we have been only serving a single file, index.html from the root of server.root, but a web server should be able to serve other static documents from root and sub-directories. Typically all CSS documents will be located in css and JavaScript in js directories.

Improved logging

We will improve our logging to include the requested resource in the output. To do that we will add an additional String argument resource to the method log. Now we will know what was requested and its corresponding status.

Resolving the requested resource

To help resolve the requested resource, we will add a helper method resolveResource, which will take a String and return a String. In this method we will walk the requested resource path and skipping relative paths like . and ...

private String resolveResource(String requestedPath) {
Path resolvedPath = FileSystems.getDefault().getPath("");
Path other = FileSystems.getDefault().getPath(requestedPath);
for (Path path : other) {
if (!path.startsWith(".") && !path.startsWith("..")) {
resolvedPath = resolvedPath.resolve(path);
if (resolvedPath.startsWith("")) {
resolvedPath = resolvedPath.resolve(INDEX_HTML);
return resolvedPath.toString();

Other helper methods

We need other helper methods which will parse the request headers, determine if the User-Agent can support response compression and get the correct type of OutputStream to which we will write the response.

private Map<String, String> parseRequestHeaders(BufferedReader in) throws IOException {
Map<String, String> headers = new HashMap<>();
// parse the first line.
String header = in.readLine();
StringTokenizer tokenizer = new StringTokenizer(header);
headers.put(METHOD, tokenizer.nextToken().toUpperCase());
headers.put(RESOURCE, tokenizer.nextToken().toLowerCase());
headers.put(PROTOCOL, tokenizer.nextToken());
// Rest of the headers
String line;
while ((line = in.readLine()) != null && !line.isEmpty()) {
int idx = line.indexOf(':');
if (idx > 0) {
headers.put(line.substring(0, idx).toLowerCase(), line.substring(idx + 1).trim());
return headers;
private OutputStream getDataOutputStream(Map<String, String> requestHeaders, OutputStream outputStream)
throws IOException {
String acceptedEncoding = requestHeaders.getOrDefault(Http.Header.ACCEPT_ENCODING, "");
if (acceptedEncoding.contains("gzip")) {
return new GZIPOutputStream(outputStream);
return new BufferedOutputStream(outputStream);
private String getContentEncoding(Map<String, String> requestHeaders) {
String acceptedEncoding = requestHeaders.getOrDefault(Http.Header.ACCEPT_ENCODING, "");
return acceptedEncoding.contains(GZIP) ? GZIP : null;
private void writeResponseHeaders(PrintWriter out, String protocol, String status, String mimeType, String date,
int length, String contentEncoding) {
out.println(protocol + status);
out.println(SERVER + config.getProperty(SERVER_VERSION_PARAM));
out.println(DATE + date);
if (contentEncoding != null)
out.println(CONTENT_ENCODING + contentEncoding);
out.println(CONTENT_TYPE + mimeType + ";charset=\"utf-8\"");
out.println(CONTENT_LENGTH + length);
out.println(CONNECTION + "close");
view raw hosted with ❤ by GitHub

We will also update the existing helper method, writeResponseHeaders to take an additional argument describing the MIME type of the response. Earlier we had hardcoded the MIME type as text/html. To help determine the MIME type of the document, Java already has a static method probeContentType in Files class.

Finally we are in a position to refactor the run method to get to what we set out to achieve in the beginning of the post.

public void run() {
OutputStream dataOut = null;
try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream())) {
Map<String, String> requestHeaders = parseRequestHeaders(in);
String method = requestHeaders.get(METHOD);
String resource = requestHeaders.get(RESOURCE);
String protocol = requestHeaders.get(PROTOCOL);
String status;
String resolvedResourcePath;
File outputFile;
if (resource.contains("./") || resource.contains("../")) {
status = Http.Status.BAD_REQUEST;
outputFile = new File(this.serverRoot, BAD_REQUEST);
} else if (Http.Method.GET.equals(method)) {
resolvedResourcePath = resolveResource(resource);
outputFile = new File(this.webRoot, resolvedResourcePath);
if (!outputFile.exists()) {
status = Http.Status.NOT_FOUND;
outputFile = new File(this.serverRoot, NOT_FOUND);
} else {
if (outputFile.isDirectory()) {
outputFile = new File(outputFile, INDEX_HTML);
if (outputFile.exists()) {
status = Http.Status.OK;
} else {
status = Http.Status.NOT_FOUND;
outputFile = new File(this.serverRoot, NOT_FOUND);
} else {
outputFile = new File(this.serverRoot, NOT_IMPLEMENTED);
status = Http.Status.NOT_IMPLEMENTED;
ZonedDateTime now ="GMT"));
String date = now.format(HTTP_FORMATTER);
String mimeType = Files.probeContentType(outputFile.toPath());
byte[] data = readFile(outputFile);
// write the headers
writeResponseHeaders(out, protocol, status, mimeType, date, data.length, getContentEncoding(requestHeaders));
// write the outputFile contents
dataOut = getDataOutputStream(requestHeaders, client.getOutputStream());
dataOut.write(data, 0, data.length);
if (dataOut instanceof GZIPOutputStream) {
((GZIPOutputStream) dataOut).finish();
} else {
// log the request
log(client.getInetAddress(), date, method, status, requestHeaders.getOrDefault(UA, ""), resource);
} catch (IOException e) {
} finally {
if (dataOut != null) {
try {
} catch (IOException ex) {
view raw hosted with ❤ by GitHub

We will not know the response type until we have parsed the headers, so we will not be able to use the try-with-resources for the dataOut OutputStream. So we will have to do it the old fashioned way. After parsing the headers we determine how to respond to the request the corresponding Status code. Finally, we will create either a BufferedOutputStream or GZIPOutputStream depending on what the User-Agent accepts.

Building a distribution

To let gradle know that we have to package the server directory, which is our default server.root, we will update our build.gradle and add the following.

distributions {
main {
contents {
from 'server', {
into 'bin/server'
view raw build.gradle hosted with ❤ by GitHub

Now if execute gradle distTar or gradle distZip we will get a nice distribution ready tar or zip file.

Code so far

Code for this post and the rest of the posts in the series can be found on GitHub at Branch step-3 contains the modifications from this post.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s