jwebserver: The JDK's Built-in Static File Server
jwebserver is the tiny HTTP server that’s already sitting inside every JDK install from 18 onward — no download, no dependency, just a command that’s there. It serves one directory of static files over plain HTTP/1.1, and per its own man page it only understands GET and HEAD; ask it for anything else and it says so instead of guessing. That’s exactly why it’s good for a quick “let me just serve this folder” moment, and wrong for anything that needs to actually handle a POST. This post starts it against a one-file site and pokes it three ways: a normal GET, a GET for a file that isn’t there, and a POST — to see what actually comes back for each.
The code
#!/usr/bin/env bash
set -e
mkdir -p site
cat > site/index.html << 'SITEEOF'
<!DOCTYPE html>
<html>
<head><title>Demo Site</title></head>
<body><h1>Hello from jwebserver</h1></body>
</html>
SITEEOF
jwebserver -p 9321 -d "$(pwd)/site" -o info > server.log 2>&1 &
SERVER_PID=$!
sleep 1
echo "== GET / =="
curl -s -i http://127.0.0.1:9321/
echo
echo "== GET /missing.html =="
curl -s -i http://127.0.0.1:9321/missing.html
echo
echo "== POST / =="
curl -s -i -X POST http://127.0.0.1:9321/
sleep 1
kill "$SERVER_PID"
wait "$SERVER_PID" 2>/dev/null || true
echo
echo "== server log (-o info) =="
cat server.log
Per the man page, four flags matter for this script:
-p— port to listen on. Default is 8000; the script uses9321so it doesn’t collide with anything already running.-d— which directory to serve. Default is the current directory; the script points it atsite/, which holds nothing butindex.html.-o— how much it logs.info(the default, used here) logs one line per request;verboseadds request/response headers and the resource’s full path;nonelogs nothing.-b— not used in this script, but worth knowing: it defaults to loopback, so nothing outside this machine can reach the server. Switching it to0.0.0.0opens it to the whole network — the man page’s own advice is to only do that once you’re sure the directory you’re serving can’t leak anything sensitive.
There’s one more thing worth knowing before the output below: a GET for a directory checks for an index file first and serves that if one exists, which is why GET / returns index.html’s content instead of a directory listing.
Running it
Real output:
== GET / ==
HTTP/1.1 200 OK
Date: Sun, 30 Aug 2026 05:13:18 GMT
Last-modified: Sun, 30 Aug 2026 05:13:17 GMT
Content-type: text/html
Content-length: 113
<!DOCTYPE html>
<html>
<head><title>Demo Site</title></head>
<body><h1>Hello from jwebserver</h1></body>
</html>
== GET /missing.html ==
HTTP/1.1 404 Not Found
Date: Sun, 30 Aug 2026 05:13:18 GMT
Content-type: text/html; charset=UTF-8
Content-length: 135
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<h1>File not found</h1>
<p>/missing.html</p>
</body>
</html>
== POST / ==
HTTP/1.1 405 Method Not Allowed
Date: Sun, 30 Aug 2026 05:13:18 GMT
Allow: HEAD, GET
Content-length: 0
== server log (-o info) ==
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /tmp/jw/site and subdirectories on 127.0.0.1 port 9321
URL http://127.0.0.1:9321/
127.0.0.1 - - [30/Aug/2026:05:13:18 +0000] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [30/Aug/2026:05:13:18 +0000] "GET /missing.html HTTP/1.1" 404 -
127.0.0.1 - - [30/Aug/2026:05:13:18 +0000] "POST / HTTP/1.1" 405 -
The GET for / came back 200 with index.html’s content, matching the index-file rule above. The GET for /missing.html came back 404, with a small HTML error page jwebserver built itself — there’s no such file under site/. The POST came back 405, not the 501 the man page reserves for a method it doesn’t recognize at all; POST is a method it knows, it just won’t serve it. Down in the log, all three requests show up as one line each — method, path, status, nothing more, which is exactly what -o info promises.
Takeaway
One command, no build step, no dependency: jwebserver -p 9321 -d site turned a folder with one HTML file into a real HTTP server — 200 for the file that exists, 404 for the one that doesn’t, 405 (not a silent success) for a method it won’t serve. That’s the whole feature set: static files, GET and HEAD, nothing more — which is exactly why it’s good for a quick local check and wrong for anything that needs a POST to actually go somewhere.