Replaced the existing HttpServer implementation and associated handler classes (`AbstractHandler`, `ActionHandler`, `GuildHandler`, `RootHandler`) with a Javalin-based routing system. Improved modularity by introducing `ActionRoutes` and `GuildRoutes` for route definitions, and centralized JSON mapping using `GsonJsonMapper`. Simplified caching via the new `ResponseCache` utility, updated `WebModule` for cleaner timeout handling, and extended test coverage for API endpoints.
11 KiB
sessionId
| sessionId |
|---|
| session-260923-111902-5uwc |
Requirements
Overview & Goals
Replace the hand-rolled com.sun.net.httpserver stack in me.bobert.netneutrality.http.server with Javalin, so the bot's embedded API needs far fewer custom classes and gains correct status codes, JSON handling and error responses. Additionally simplify the outbound WebModule HTTP client.
Scope
In scope
WebApibecomes a thin Javalin-basedLifecyclecomponent (same priority 70, sameNETNEUTRALITY_API_PORTsetting).- Delete
AbstractHandler,RootHandler,ActionHandler,GuildHandler; their logic moves into small route-registration units. - Standardise the API layer on Gson — remove
org.json(JSONObject/JSONArray) usage from the HTTP server package. - Correct HTTP semantics: 404 for unknown guilds/paths, 405 for wrong methods, 500 on exceptions, UTF-8 safe bodies.
- Simplify
WebModule(outbound OkHttp wrapper): extract the static cache, remove duplicated/unused members.
Out of scope
- Changing the public URL shape of existing endpoints (
/,/actions/*,/guilds/*) — paths and payload keys stay backwards compatible. - Authentication/authorisation for the API.
- Migrating
org.jsonusage elsewhere in the project (e.g. task classes usingWebModule.getAsObject()).
Functional Requirements
GET /→ HTML liveness page (unchanged text).GET /actions/status→{"health":...,"ping":...}.POST /actions/shutdown→ runs the shutdown hook and exits (acceptGETtoo for compatibility if desired).GET /actions/settings/current/{guildId}→{"status":"ok","settings":{...}}, or 404 +{"status":"error"}for an unknown guild.POST /actions/settings/reload/{guildId}→ reloads the guild setting cache,{"status":"true"}; 404 for an unknown guild.GET /guilds/owner→ array of{guild_id, owner_id}.GET /guilds/channels/{guildId}andGET /guilds/roles/{guildId}→ arrays of id/name objects; 404 for an unknown guild.- Requests arriving before JDA is ready must return 503 rather than throwing an NPE.
Non-Functional Requirements
- Startup/shutdown stays deterministic through
ApplicationContext—stop()must release the port. - Shaded jar must still run (
maven-shade-plugin); Javalin/Jetty service files need merging. - No behavioural regression for existing consumers of the endpoints.
Technical Design
Current Implementation
WebApi.java(92 lines) implementsLifecycle(priority 70), creates anHttpServer, registers three contexts and a fixed pool built fromBaseThreadFactory("web-api"). Constructed inApp.java:154.AbstractHandlerofferssendStringResponse/sendJsonResponse; both hardcode status 200 and usestr.length()instead of byte length (breaks non-ASCII).ActionHandler/GuildHandlerroute viaif (path.startsWith(...))chains and parse ids withgetRequestURI().toString().split("/"); they mix Gson andorg.json, never check the HTTP method, and calljda.getGuildById()twice in places.WebModuleis an unrelated outbound OkHttp wrapper with a builder, a static LRU cache and aCachedResponserecord; it builds a brand-newOkHttpClientper request and has three redundant timeout setters plus unusedelapsedTime/errorMessageaccessors.
Key Decisions
- Javalin (6.x, JDK 17+ / Java 25 compatible) as the embedded server — removes routing, method matching, error mapping and content-type handling code.
- Gson only in the HTTP layer: register a
GsonJsonMapperon the Javalin config soctx.json(obj)works with plain records/maps;org.jsondisappears fromhttp/server. - Route groups instead of handler classes:
ActionRoutesandGuildRoutesexpose a singlestatic void register(Javalin app, deps...), soAbstractHandler,RootHandler,ActionHandler,GuildHandlerare all deleted (4 classes → 2 tiny registrars). - Guild lookup guard: one private helper resolves
{guildId}→Guildor throwsNotFoundResponse, replacing repeated null checks. - WebModule: keep the builder API used by callers, but move the cache into a package-private
ResponseCache, share a single staticOkHttpClientand derive per-request timeouts withnewBuilder().
Proposed Changes
http/server/WebApi.java
public class WebApi implements Lifecycle {
private Javalin app;
@Override public int priority() { return 70; }
@Override public void start() {
int port = Integer.parseInt(settings.get("NETNEUTRALITY_API_PORT", "9000"));
app = Javalin.create(cfg -> cfg.jsonMapper(new GsonJsonMapper()));
app.get("/", ctx -> ctx.html(LIVENESS_HTML));
ActionRoutes.register(app, jdaSupplier, guildServices, shutdownHook);
GuildRoutes.register(app, jdaSupplier);
app.exception(Exception.class, (e, ctx) -> { log.error(...); ctx.status(500).json(Map.of("status","error")); });
app.start(port);
}
@Override public void stop() { if (app != null) { app.stop(); app = null; } }
}
http/server/GuildRoutes.java — /guilds/owner, /guilds/channels/{guildId}, /guilds/roles/{guildId} mapped to List<Map<String,String>> and returned via ctx.json(...).
http/server/ActionRoutes.java — /actions/status, /actions/shutdown, /actions/settings/current/{guildId}, /actions/settings/reload/{guildId}.
http/server/GsonJsonMapper.java — ~15 lines implementing Javalin's JsonMapper over a shared Gson.
http/WebModule.java — shared client, cache extracted, redundant setters/accessors dropped; public builder methods used by callers remain.
Data Models / Contracts
Small records replace ad-hoc JSON building, serialized by Gson with the existing key names:
record GuildOwner(String guild_id, String owner_id) {}
record NamedEntity(String channel_id, String channel_name) {}
record StatusResponse(String health, String ping) {}
File Structure
src/main/java/me/bobert/netneutrality/http/
WebModule.java (modified)
server/WebApi.java (rewritten, Javalin)
server/GsonJsonMapper.java (new)
server/ActionRoutes.java (new)
server/GuildRoutes.java (new)
server/AbstractHandler.java (deleted)
server/RootHandler.java (deleted)
server/ActionHandler.java (deleted)
server/GuildHandler.java (deleted)
pom.xml (+ io.javalin:javalin, shade transformer)
Architecture Diagram
graph LR
App-->ApplicationContext
ApplicationContext-->WebApi
WebApi-->Javalin
WebApi-->GsonJsonMapper
Javalin-->ActionRoutes
Javalin-->GuildRoutes
ActionRoutes-->GuildServiceProvider
GuildRoutes-->JDA
Risks
- Shaded jar: Jetty/Javalin rely on
META-INF/services; the shade plugin needsServicesResourceTransformer, otherwise startup fails only in the packaged build. - Jar size / SLF4J binding: Jetty logs through SLF4J; project already has log4j + slf4j — check for duplicate bindings.
- JDA not ready:
jdaSupplier.get()may be null; guard with a 503 mapping. System.exit(0)in the shutdown route must run after the response is flushed.
Testing
Validation Approach
Build with ./mvnw -q verify (checkstyle included), then start the app/WebApi on an ephemeral port and exercise every endpoint with curl, comparing payload shape against the pre-refactor behaviour.
Key Scenarios
GET /returns 200 and the HTML liveness text.GET /actions/statusreturns 200application/jsonwithhealthandping.GET /guilds/ownerreturns a JSON array withguild_id/owner_idkeys.GET /guilds/channels/{id}and/guilds/roles/{id}return arrays withchannel_id/channel_name.GET /actions/settings/current/{id}returns{"status":"ok","settings":{...}}.
Edge Cases
- Unknown guild id → 404 with
{"status":"error"}(previously 200). - Unknown path → 404 (previously echoed the path with 200).
- Wrong method on a route → 405.
- Non-ASCII content in settings values round-trips correctly (old
str.length()bug). Lifecycle.stop()frees the port — a secondstart()on the same port succeeds.
Test Changes
- Add a lightweight JUnit test starting
WebApion port 0 with a mockedSupplier<JDA>andGuildServiceProvider, hitting the routes with Javalin's test client orjava.net.http.HttpClient. - Add a
WebModuletest covering cache hit/expiry after the cache extraction.
Delivery Steps
✓ Step 1: Add Javalin dependency and Gson JSON mapper
The project builds with Javalin available and a Gson-backed JSON mapper ready for use.
- Add
io.javalin:javalin(6.x) topom.xmldependencies. - Add
ServicesResourceTransformerto themaven-shade-pluginconfiguration so Jetty/Javalin service files survive shading. - Create
http/server/GsonJsonMapper.javaimplementing Javalin'sJsonMapperover a sharedGsoninstance. - Verify
./mvnw -q packagesucceeds and the shaded jar still starts.
✓ Step 2: Rewrite WebApi as a Javalin Lifecycle component
WebApi starts and stops a Javalin server while keeping its Lifecycle contract and the root endpoint.
- Replace the
HttpServer/ExecutorServicefields with aJavalin appfield. - Build the instance in
start()with theGsonJsonMapper, readingNETNEUTRALITY_API_PORTfromSettingsProvideras today. - Register
GET /returning the liveness HTML and deleteRootHandler.java. - Add global
exceptionanderrorhandlers returning 500/404 JSON, plus a 503 guard whenjdaSupplier.get()is null. - Implement
stop()viaapp.stop(); keeppriority() == 70and theApp.javaconstruction site unchanged.
✓ Step 3: Migrate guild endpoints to GuildRoutes
All /guilds/* endpoints are served by Javalin route lambdas with proper status codes and Gson serialization.
- Create
http/server/GuildRoutes.register(Javalin, Supplier<JDA>). - Map
GET /guilds/owner,GET /guilds/channels/{guildId},GET /guilds/roles/{guildId}using path params instead ofsplit("/"). - Return
List<record>payloads viactx.json(...), preserving the existingguild_id/owner_id/channel_id/channel_namekeys. - Resolve guilds through a shared helper that throws
NotFoundResponsefor unknown ids (singlegetGuildByIdcall). - Delete
GuildHandler.java.
✓ Step 4: Migrate action endpoints to ActionRoutes
All /actions/* endpoints run on Javalin, with org.json fully removed from the HTTP server package.
- Create
http/server/ActionRoutes.register(Javalin, Supplier<JDA>, GuildServiceProvider, Runnable). - Map
/actions/status,/actions/shutdown,/actions/settings/current/{guildId},/actions/settings/reload/{guildId}. - Replace
JSONObject/JSONArraywith Gson-serialized maps/records keeping the same response keys. - Ensure the shutdown route flushes its response before running the hook and exiting.
- Delete
ActionHandler.javaandAbstractHandler.java, and add endpoint tests startingWebApion port 0 with mocked JDA/guild services.
✓ Step 5: Simplify the outbound WebModule client
WebModule keeps its builder API but sheds duplicated state and its inlined cache.
- Extract the static LRU cache and
CachedResponseinto a package-privateResponseCacheclass inhttp/. - Share a single static
OkHttpClientand derive per-request timeouts withnewBuilder()instead of constructing a client per build. - Collapse the redundant
timeout/connectTimeout/readTimeout/writeTimeoutsetters and remove unused accessors (getErrorMessage,elapsedTimeexposure) after checking call sites. - Add a unit test covering cache hit, expiry and POST bypass; verify existing
WebModulecallers still compile.