dev.mccue.microhttp.handler

What is it

microhttp-handler provides interfaces for making composable handlers for microhttp.

There are two interfaces in the module, IntoResponse and Handler.

IntoResponse is something which can be converted into a Response. Handler is a function which takes a Request and returns something which implements IntoResponse.

There are also two implementations of Handler provided for convenience - RouteHandler and DelegatingHandler.

RouteHandler checks the request's method and uri and returns null if it doesn't match a chosen method and regex.

DelegatingHandler tries a list of handlers in order, returning the first non-null response or falling back to a default for when no match is found. Combined with RouteHandler, this can act as a very basic request router.

Why use it

A normal microhttp handler takes a Request and a Consumer<Response> that should be called later.

While this is fine, in the age of virtual threads there isn't much downside to modeling handlers as functions that takes Requests and return Responses and there are many upsides to doing so.

The programming model is simpler to compose, simpler to test, and it provides an opportunity to introduce concepts such as middleware and IntoResponse

IntoResponse is useful because making the normal Response record requires dealing with reason phrases, content-type headers, and body encoding. IntoResponse provides a seam for custom types to box up much of that logic.

Getting Started

import org.microhttp.EventLoop;
import org.microhttp.Response;
import org.microhttp.Header;

import dev.mccue.microhttp.handler.Handler;
import dev.mccue.microhttp.handler.RouteHandler;
import dev.mccue.microhttp.handler.DelegatingHandler;

import dev.mccue.reasonphrase.ReasonPhrase;

import java.util.List;
import java.util.regex.Pattern;

record TextResponse(int status, String value) 
        implements IntoResponse {
    @Override
    public Response intoResponse() {
        return new Response(
                status,
                ReasonPhrase.forStatus(status),
                List.of(new Header("Content-Type", "text/plain")),
                value.getBytes()
        );
    }
}

void main() throws Exception {
    Handler index = RouteHandler.of(
            "GET", 
            Pattern.compile("/"), 
            request -> new TextResponse(200, "Hello, world")
    );

    Handler rootHandler = new DelegatingHandler(
            List.of(index),
            new TextResponse(404, "Not Found")
    );

    var error = new TextResponse(500, "Internal Error");

    var eventLoop = new EventLoop((request, callback) -> {
        Thread.startVirtualThread(() -> {
            try {
                callback.accept(
                        rootHandler.handle(request)
                                .intoResponse()
                );
            } catch (Exception e) {
                callback.accept(error.intoResponse());
            }
        });
    });
    eventLoop.start();
    eventLoop.join();
}

<- Index