Serving

Serving files and requests

Serve a multi-file frontend and answer fetch, form and htmx requests from Java with the Router, with no socket.

loadResources shows a multi-file page and lets Java answer its requests: fetch, forms, htmx hx-get and hx-post, and JSON APIs. Files are served inside the web view, with no socket. The origin is https://jolt.localhost on Windows and jolt://app on Linux, so use relative URLs.

A request handler

window.loadResources(AppResources.classpath("ui"), request -> switch (request.path()) {
    case "/notes" -> AppResponse.html(renderNotes());
    case "/notes/add" -> {
        notes.add(request.formValue("text"));
        yield AppResponse.html(renderNotes());
    }
    default -> null;                                  // null: let the files answer
});

The Router

For more than a few routes use Router, which is a RequestHandler:

Router router = Router.create()
    .get("/notes/{id}", request -> AppResponse.html(pages.note(request.param("id"))))
    .post("/notes", request -> { notes.add(request.formValue("text")); return AppResponse.redirect("/notes"); })
    .group("/admin", admin -> admin
        .before(request -> loggedIn(request) ? null : AppResponse.redirect("/login"))
        .get("/users", request -> AppResponse.html(pages.users())))
    .onError((request, error) -> AppResponse.html(pages.error(error)).withStatus(500));
window.loadResources(AppResources.classpath("ui"), router);
  • Patterns: {id} captures one segment, {*rest} the rest. A plain segment beats {id}, which beats {*rest}.
  • A request no route matches falls through to your files. A wrong method on a known path gets 405 with Allow.
  • Handlers run on worker threads. An exception becomes a bare 500 and the details go to the log, never to the page.
  • Escape anything user-supplied before putting it in HTML, and keep GET handlers free of side effects.

Uploads

multipart/form-data works with plain forms, fetch and htmx. Read it with request.parts() or request.part(name):

AppRequest.Part file = request.part("document");
if (file != null && !file.filename().isEmpty()) {
    Files.write(uploads.resolve(UUID.randomUUID() + ".bin"), file.content());   // choose the stored name yourself
}

Treat uploaded files as untrusted: filename() and contentType() come from the user's machine. Bodies are held in memory, 8 MB by default (413 above that). Raise the limit with ResourceOptions.defaults().withMaxRequestBytes(...), up to 1 GB.

Loopback fallback

The in-process transport needs WebKitGTK 2.40 or newer on Linux. Older systems fall back automatically to a guarded server on 127.0.0.1 with a per-run CSRF token and Origin, Host and Sec-Fetch-Site checks. You can also ask for it, for example to open the app in a normal browser: window.loadResources(resources, handler, 48123).