RESTful HTTP API for Paper servers, configured through PlaceholderAPI placeholders.
HMTAPI is a fork of APIMachine, developed in cooperation with HM Gaming for TitanSMP. It replaces the original Spark/Jetty stack with the JDK's built-in HTTP server, so the plugin ships no third-party libraries at all.
User documentation and the full configuration reference live at gaming.henrymeyer.de/projects/plugins/hmtapi/. This README is aimed at people who want to build, read or extend the code.
| Server | Paper 26.3 or newer (api-version: 26.3) |
| Java | 25 |
| Runtime dependency | PlaceholderAPI |
| Build | Maven 3.9+ and a JDK 25 |
mvn clean verifyThis compiles, runs the test suite and writes target/HMTAPI-<version>.jar. The jar has no bundled
dependencies, so it is deployed by copying it into the server's plugins folder.
mvn clean install additionally publishes the artifact to your local repository.
The plugin is split so that all Bukkit access lives on the server thread and the HTTP layer stays free of it.
| Class | Responsibility |
|---|---|
HMTAPI |
JavaPlugin lifecycle, config loading, and the ApiContext implementation |
api/ApiServer |
HTTP server, routing, status codes, request/response handling |
api/ApiContext |
Everything ApiServer needs from the plugin; the seam that makes it testable |
api/ApiSettings |
Immutable, validated view of the global config |
api/EndpointRegistry |
Immutable snapshot of the configured endpoints |
api/Endpoint |
One configured endpoint: name, require_player, key/value templates, optional error |
api/RequestTarget |
Raw request path to route classification |
api/TemplateRenderer |
Placeholder substitution, pure functions only |
api/Placeholders |
Defensive bridge to PlaceholderAPI |
api/Json |
Shared Gson instance for responses |
commands/ReloadCommand |
/hmtapi reload and its tab completion |
com.sun.net.httpserver.HttpServer dispatches onto a ThreadPoolExecutor (2 core, 8 max, 60 s idle
timeout) using threads named HMTAPI-HTTP-*. Those threads must never call the Bukkit API, because
Bukkit is not thread safe.
The flow for one request:
- A worker thread parses the path (
RequestTarget) and reads the immutableEndpointRegistryandApiSettingssnapshot. HMTAPI.callSyncschedules a singleCallableon the server thread and returns aCompletableFuture.- The worker awaits that future for
request_timeout_seconds(1–60 s, default 5 s). - The
Callableresolves the player, substitutes placeholders and returns the JSON payload.
Consequences worth knowing before you change this code:
- The timeout is a safety valve, not a scheduling hint. A timed-out request still completes its task on the server thread; only the HTTP response is abandoned. Keep the callable cheap.
context.isActive()distinguishes "plugin is shutting down" from a genuine failure so shutdown does not produce spurious500s in the log.- Player lookups use
Server#getOfflinePlayerIfCachedfirst and only fall back toServer#getOfflinePlayerfor unknown names, because the fallback hits the database.
EndpointRegistry and ApiSettings are replaced wholesale on reload and are volatile fields on
the plugin. A request therefore always observes one consistent snapshot, never a half-written
FileConfiguration. Everything in them is unmodifiable; endpoint values keep their config.yml
order so the JSON key order is stable.
port and bind_address cannot be changed at runtime. A reload that changes them logs a warning
telling the operator to restart.
RequestTarget classifies a raw path into ENDPOINT, FAVICON, UNKNOWN or MALFORMED:
- Endpoint names must match
[A-Za-z0-9_-]{1,64}. - Usernames may be at most 32 characters and may not contain whitespace or control characters.
- Percent-encoded segments are decoded;
+is preserved as a literal plus rather than a space. - Broken escape sequences such as
%zzare reported asMALFORMEDinstead of throwing.
- Errors that describe a configuration problem return
200with an{"error": "..."}body. This is the original APIMachine behaviour and is kept for client compatibility. - Errors that describe a bad request return a real status code:
400for a malformed path or a missing username,404for an unknown route,405for anything butGET/HEAD,503on timeout or shutdown,500on unexpected failures. - A body of a rejected request is drained up to 64 KiB before responding, so keep-alive connections stay usable. Larger bodies are abandoned and the connection is closed.
HEADis answered with the correct status and headers but no body.
mvn testsrc/test/java/de/jumpstone/hmtapi/api/ holds unit tests for the pure logic
(RequestTarget, TemplateRenderer, Json, ApiSettings, EndpointRegistry) and
ApiServerTest for the HTTP layer. The latter starts a real ApiServer on a free port and drives
it with java.net.http.HttpClient; the Bukkit surface is stubbed through ApiContext, with
OfflinePlayer backed by a java.lang.reflect.Proxy. TestResources loads YAML fixtures without
requiring a running server.
ApiServerTest also has a raw-socket helper, because HttpClient refuses to build a URI with a
broken escape sequence and a few edge cases can only be reached by a client that sends the bytes
unchecked.
No test needs a Minecraft server, and none touches the network.
- Fork the repository and create a branch.
- Keep the build green:
mvn clean verifymust pass with no compiler warnings. The compiler runs with-Xlint:all,-serial,-processing. - Add tests for new behaviour. Config parsing and HTTP status codes are the areas most likely to regress silently.
- Open a pull request against
main.
CI runs on published GitHub releases only. Publishing a release tagged v26.3-1.0.0 builds
HMTAPI-26.3-1.0.0.jar and attaches it to that release, so the asset name always matches the tag.
The version comes from the tag, not from the pom: the workflow passes it in as
-Drevision=<version> and the build writes target/HMTAPI-<version>.jar. A leading v on the tag
is stripped, so both v26.3-1.0.0 and 26.3-1.0.0 produce HMTAPI-26.3-1.0.0.jar. The revision
property in pom.xml is only the default for local builds, and plugin.yml inside the jar picks up
whatever version was used.
Build and test the exact release jar locally with:
mvn clean verify -Drevision=26.3-1.0.0Note that the workflow builds the tagged commit, not main. Any change that should ship has to be
on the tagged commit.
- Add a
staticsubstitution method toTemplateRenderer, keeping it free of Bukkit types. - Call it from
ApiServer#buildPayloadin the correct order. Built-in placeholders run before{papi:...}so a value produced by PlaceholderAPI is never re-substituted. - Cover it in
TemplateRendererTest.
- Add a field and a validated reader to
ApiSettings; invalid values log a warning and fall back to the default rather than throwing. - Document it in
src/main/resources/config.yml. - Add a case to
ApiSettingsTest.
- Documentation: gaming.henrymeyer.de/projects/plugins/hmtapi/
- Upstream project: APIMachine
- Original wiki: mallusrgreat.gitbook.io