As we delve deeper into the series, today we attempt to write a Static file server, and wire it up to serve css and html pages.
As with the other posts, you can see the merge request for the code change, the repository for the entire code or the series for all the articles on the journey.
The first thing that needed to be done is write a handler for static files. Here is the structure of the StaticFileHandler
:
public RouteResponse handle(RouteRequest req) {
// only support get calls
if (req.method() != RequestMethod.GET) {
// 404: Not Found is automatically handled
return null;
}
String uriPath = computeUriPath(req.uri().getPath());
// never allow access to any directory
var path = Paths.get(this.folder, uriPath);
// if the file doesn't exist, or is a directory, return null
if (!Files.isRegularFile(path)) {
// 404: Not Found is automatically handled
return null;
}
// get content mime time based on file name
try {
var contentType = Files.probeContentType(path);
if (contentType == null) {
throw new IllegalArgumentException("Unable to determine content type for file: " + path);
}
// read the file and return in the response
var fileContent = new String(Files.readAllBytes(path));
return new RouteResponse(200, contentType, fileContent);
} catch (IOException e) {
LoggerConfig.getApplicationLogger().log(Level.SEVERE, "Error while accessing file: {0} ", path);
LoggerConfig.getApplicationLogger().log(Level.SEVERE, "Error Message: {0} ", e.getLocalizedMessage());
LoggerConfig.getApplicationLogger().log(Level.SEVERE, "Error Trace: {0} ", e.getStackTrace());
return new RouteResponse(403, ContentType.CONTENT_TYPE_TEXT.getValue(), "Forbidden");
}
}
The notable pieces here are:
the handling of only the
GET
verbhandling of file existence, and only a file, not a directory
setting the content type of the response, based on the file name
returning a 403 / Forbidden, when there is an exception accessing the file
The Router
's sendResponse
now handles more cases, ensuring a proper cleanup, and a content type, if there is a body specified in the response:
private void sendResponse(HttpExchange exchange, RouteResponse response) throws IOException {
if (response.body() != null && !HeaderHelper.hasContentType(response.headers())) {
throw new IllegalArgumentException("Need a content type if response body is specified");
}
try (var os = exchange.getResponseBody()) {
int respBodyLen = response.body() == null ? -1 : response.body().getBytes().length;
exchange.sendResponseHeaders(response.statusCode(), respBodyLen);
var responseHeaders = exchange.getResponseHeaders();
// add the headers from the response to the exchange
responseHeaders.putAll(response.headers());
if (respBodyLen >= 0) {
os.write(response.body().getBytes());
}
}
}
The tests have been updated. This time to handle the missing condition of "Internal server exception", in the RouterTest
.
@Test
void testInternalServerError() {
var req = RouterHelper.createGetRequest("/error");
var resp = router.processRequest(req);
assertEquals(500, resp.statusCode());
assertEquals(List.of("text/plain"), resp.headers().get("Content-Type"));
}
The route for handling /error
in the test is just a simple:
config.addHandler("/error", (req) -> {
throw new RuntimeException("Exception for testing.");
});
The logging usage is slightly unconventional. But first, the setup code:
static void setupGlobalConsoleLogger() {
// Get the global logger
Logger globalLogger = Logger.getLogger("");
// always log info and above for all loggers
globalLogger.setLevel(Level.INFO);
// Remove any existing handlers to avoid duplication
Handler[] handlers = globalLogger.getHandlers();
for (Handler handler : handlers) {
globalLogger.removeHandler(handler);
}
// Create a console handler and set its log level
var consoleHandler = new ConsoleHandler();
consoleHandler.setFilter(f -> true);
consoleHandler.setFormatter(new Formatter() {
final SimpleDateFormat loggerDateFormat = new SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS");
public String format(LogRecord lr) {
var message = lr.getMessage();
var params = lr.getParameters();
if (params != null && params.length > 0) {
message = MessageFormat.format(message, params);
}
String formattedDate = loggerDateFormat.format(Date.from(lr.getInstant()));
return String.format("%d::%s::%s::%s::%s::%s%n",
lr.getLongThreadID(), lr.getLoggerName(), lr.getSourceClassName(), lr.getSourceMethodName(), formattedDate, message);
}
});
// Add the console handler to the global logger
globalLogger.addHandler(consoleHandler);
// setup the specific app loggers after the global settings
if (RuntimeConfig.getInstance().runMode() == RuntimeConfig.RunMode.PROD) {
// only log sever message from the debug logs, in prod
getDebugLogger().setLevel(Level.SEVERE);
} else {
getDebugLogger().setLevel(Level.FINE);
}
}
When I asked the question: What is the logger caching strategy for java.util.logging framework ? When do the created loggers release memory ?
to ChatGPT, the response I got was:
The Logger instances are typically stored using weak references. This means that if there are no strong references to a logger (i.e., it's not actively used), it may be eligible for garbage collection. Weak references allow unused loggers to be automatically released when memory is needed.
This essentially means that I have been doing loggers wrong all this while, but just creating and keeping references at the instance level.
Also, when I looked at the Formatter
available for the ConsoleHandler
above, I could log the class and method name of the caller. So, there was no need to create loggers that are specific to each class. Instead, I decided on using two kinds of loggers, an Application level logger, and a Debug logger. The idea behind these is to have explicit intent. Anything that goes into the "Application Logger" is intended to always be logged. While the "Debug logger" should be turned on only for debug purposes, unless the debug message is a "SEVERE" in which case it should always be logged. This allows better memory utilisation, and ensures that the printing format is under control.
There is an explicit intent to use only the ConsoleHandler, and no file handler, because I assume that the application will always be deployed with a Container manager like Docker, or a process manager that will redirect the standard console and error streams to its respective handling mechanism.
With this implementation, the server is "functional". It can serve static content, from a specified directory, and also handle requests received over the http flow. It handles the 404, and 500 errors, and cleans up the streams on its way out. So, the client will not be waiting for a response.
Next step will be to explore an authorisation