Repository navigation
Conversation
A process ended by a signal exits with no code at all, and _handleClose only counted a non-zero code as a crash, so a JVM killed from outside was recorded as a clean stop: no crashed state or event, and no auto-restart. That is exactly how the kernel's OOM killer ends a server in a memory-limited container. An exit by signal that nobody asked for is now a crash with crashReason "signal" and a null exitCode. Kill and the stop timeout still read as clean stops, since both mark the stop as requested before signalling. The crash banner and the dashboard card say the process was killed, and the banner no longer shows an empty exit code when there is none. The API reference now lists the crash reasons. The operations test's signal check no longer runs as a known issue.
Forge and NeoForge installers are checked against a checksum published beside them on Maven before they run, and Craftbox insisted on the .sha256 one. Forge's Maven has only .sha1 and .md5 for many older installers, so 1.12.2 (14.23.5.2864), 1.14.4 and 1.15.2 servers could not be created at all. Both now take the strongest sidecar published: .sha256, then .sha1, then .md5, moving on only when one is missing. Any other failure, or no sidecar at all, still refuses the installer. The sidecar comes from the same host as the installer either way, so what it guards against, corruption on the way, is the same. Unit tests cover the order and the refusals, and the weekly Forge 1.12.2 start no longer runs as a known issue.
The backup schedule and retention endpoints dropped a value they didn't accept and still answered 200, so an API client was told a bad value had been saved, and the Backups page said "Backup schedule saved." when the warning countdown was set to 50 and nothing changed. parseInt also read '12abc' as 12 and 1.5 as 1. Both now check every field before saving any of them and answer 400 with the allowed range, which the Backups page shows as its error toast. A whole number, as a number or a string of digits, is required; a field left out or null still keeps its value, as the page sends null for an empty box. The schedule's `enabled` also has to be true or false. The operations test's backup checks now expect the 400s, including a request with one good and one bad value saving neither.
The upgrade check answered a vanilla server with "No builds published for this version.", which reads as though Mojang had withdrawn the release. Vanilla's provider has a getBuilds() that always returns null, since a release is a single jar, so the check went looking for builds and found none. It now answers vanilla as it does custom, with "No build tracking for this server type.", which is what the API reference already said. The operations test checks the reason, not just that there is one.
A download started from the panel is matched to its outcome by the dl
token on its URL, and the API reference says every message about it
carries that token. A failure didn't: it went out through
broadcastOperation, which sent only an error string for `failed`. The
panel made do by settling the failure against the one download it had
in flight, so with two in flight a failure left both spinning, and
every open page on the server showed the error toast, not just the one
that started the download.
broadcastOperation can now send a payload beside a failure's error,
and a failed download's report carries {token, label} like the others.
The panel settles a failure by its token and ignores reports for
downloads it didn't start. The transfers test checks the token.
The login limiter allowed five attempts per 15 minutes per address and counted every one, so signing in five times in a row, from a few browsers or a script, locked the address out with the right password. The API reference described the limit as five attempts, which is how it behaved, but a limit on guessing has no reason to count sign-ins that succeed. Only failed ones count now. Every attempt is still counted as it arrives, so parallel guesses are capped as before, and a successful one is taken back off once it has answered. A success and a failure are both redirects, so the login route marks a success for the limiter to read rather than it going by status code. The web test signs in six times in a row and checks none of them used up an attempt, before the existing check that five failures lock the address out.
Starting a server on a port another running server held let the second JVM start, fail to bind, and die with a BindException, which Craftbox recorded as a crash. Nothing warned when a server was created or edited onto a port another one used either; only an import did. Start and restart are now refused with a 409 naming the server that holds the port. Restart checks before stopping, so a port changed while the server ran can't leave it down, and so does restart behind a backup. A running server's port and address are taken when its JVM is spawned, and server-ip is respected, so two servers bound to different specific addresses can still share a port. Sharing a port is still allowed, as only one of them can run at a time. Creating, duplicating or importing a server onto a port another server uses, or moving one onto it through Settings or Properties, answers with a `warnings` array saying so. The panel shows those as toasts after saving, and the Port fields on the create page and on the Settings page (Port and Duplicate) say so as the port is typed. The operations test creates a second server on the lifecycle server's port and checks the warnings, the refused start and the refused restart. The browser test checks the hint beside each Port field.
Duplicating without the world removed the world, world_nether and world_the_end folders from the copy, whatever the server called its world. A server whose level-name had been changed kept its world in other folders, so the copy took the whole world with it. The copy now also leaves out the folders level-name names, with their _nether and _the_end siblings. level-name is free text, so a name that would point at the copy itself or outside it is skipped rather than deleted. The API reference says what is left out. The transfers test duplicates a server whose world is under its own level-name, split into three folders, and checks none of them came along. Before this change all three did.
At startup, and again at shutdown, Craftbox reset every server that wasn't stopped to stopped, so a crash was forgotten whenever the panel restarted: the crash banner went, and the reason and exit code no longer showed. The reset is there for states a restart makes untrue, like running, and crashed only fell into it because stopped was the one state it skipped. It was worse for a duplicate or import that had failed half way, which came back looking like a whole server that could be started. A crashed server now stays crashed across a restart. Auto-start still starts one that has it on, as it did when the crash had been turned into a stop. The crash banner also shows an import failure's reason, as it does a failed duplicate's, and the API reference says what a panel restart does to each state. The operations test crashes the lifecycle server before its panel restart and checks the crash, and the server page's banner, survive.
Before 1.14 Minecraft reads gamemode and difficulty only as numbers. Craftbox always wrote the names, which such a server can't parse, so it quietly played Survival on Easy whatever was chosen, and rewrote the file with those numbers. Probed on the real server jars: 1.8.9, 1.12.2 and 1.13.2 fall back that way, while 1.14, 1.20.4 and 26.3 read the names, and read numbers as well. Craftbox still keeps the names everywhere else, in the server record, the forms and the API, and writes the numbers to server.properties for a server before 1.14: on create, a modpack install, Settings and Properties saves. Reading the file back turns a number into its name, so the record and the Properties page's selects show names too. A server that already ran with the fallback now shows the difficulty it actually played, which a save through Settings or Properties corrects. Unit tests cover which versions get numbers and the reading back. The start test now creates each server as Creative on Hard and checks the server kept both, which the weekly 1.12.2 starts would have failed.
The start test checks a server answers `list`, and knew two wordings of the reply: "0/20" before 1.13 and "0 of a max of 20" from 1.16. Minecraft 1.13 to 1.15 say "0 of a max 20", so a start of any of those failed at that step although the server had answered. No CI run covers those versions, which is why it hadn't shown; starting Forge 1.14.4 and 1.15.2 by hand did.
The image carried the app, its dependencies and package.json, but not LICENSE, and the AGPL asks for its text to go with every copy distributed, which a published image is. It now sits at /app/LICENSE. The image's licence label came from GitHub's licence detection, which gives AGPL-3.0. The publish workflow now sets it to AGPL-3.0-only, as package.json declares.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A patch release on top of 1.2.2, fixing what widening CI (#72) turned up: five bugs and API mismatches, four more problems found along the way, and the licence missing from the Docker image. Not versioned yet; the "Promote release to 1.2.3" commit comes last, once this is reviewed.
Fixed
crashedevent and no auto-restart. That's how the kernel's OOM killer ends a server in a memory-limited container. It's now a crash withcrashReason: "signal"; Kill and the stop timeout still count as clean stops..sha256checksum beside the installer, and Forge's Maven publishes only.sha1and.md5for many older ones (1.12.2, 1.14.4, 1.15.2). Forge and NeoForge now take the strongest one published, and still refuse an installer with none.server.propertiesfor those versions and keeps names everywhere else. A server that already ran with the fallback now shows the difficulty it actually played, which a save corrects.stopped, so the crash banner, reason and exit code went, and a half-finished duplicate or import looked whole.crashednow survives a restart.world*folders were left out, so a world under anotherlevel-namecame along. Those folders are now left out too, with a guard so alevel-namelike..can't point outside the copy.LICENSEis now at/app/LICENSE, as the AGPL asks for every copy distributed. The image's licence label, which came from GitHub's detection asAGPL-3.0, is now set toAGPL-3.0-onlyto matchpackage.json; that label only takes effect on the next release's publish.API changes
POST /servers/:id/startand/restartanswer409while another server's process holds the port, naming it:Port 25565 is in use by "Survival". Stop that server, or change this one's port.Restart checks before stopping, and so does restart behind a backup.server-ipis respected, so servers bound to different specific addresses can share a port./servers,/from-modpack,/from-mrpack),/duplicate,/import, and/editand/propertieswhen the port changes, now return awarningsarray, empty when there's nothing to say:Port 25565 is also used by "Survival". Only one of them can run at a time.Sharing a port is still allowed./importalready warned; its wording now matches.POST /servers/:id/backup-scheduleand/backup-retentionanswer400for a value out of range or not a whole number ('12abc',1.5), and then save nothing. They used to drop the value and answer200.GET /servers/:id/check-upgradeon vanilla says "No build tracking for this server type.", as documented, instead of "No builds published for this version."operationmessage now carriespayload: {token, label}besideerror, like every other download message.POST /logincounts only failed sign-ins towards its 5 per 15 minutes; signing in five times in a row no longer locks the address out.crashReasongainssignal(with a nullexitCode), and the reference now lists every reason and what a panel restart does to each state.Panel changes
CI
listcheck didn't know the 1.13 to 1.15 wording of the reply ("0 of a max 20").Checked locally
Against this branch's image: API 31/31, WebSocket 14/14, Transfers 16/16, Web 23/23, Browser 10/10, Operations lifecycle 13/13 and backups 7/7, unit 48/48, and starts of vanilla, Paper, Fabric and Forge at their latest versions plus vanilla 1.12.2 and Forge 1.12.2, 1.14.4 and 1.15.2 on Java 8. Before its fix, the same duplicate steps copied all three world folders, and the new 1.12.2 start check fails on the published 1.2.2 image (
gamemode=0, difficulty=1).Notes
26.3.0.33-betaon 2026-09-28, which broke every NeoForge create. It was republished whole at 10:57 UTC today, so the NeoForge jobs should pass; nothing in Craftbox changed for it.