Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

httpserv.sh

CILicense: MIT

A single-file Java 25 shebang script that serves a directory over HTTP. Built on the JDK's built-in com.sun.net.httpserver with virtual threads. No build step, no dependencies — just drop httpserv on your $PATH and run.

Features

  • Single executable file (httpserv) — runs via java --source 25
  • Virtual-thread-per-request executor
  • RFC 7233 range requests: single ranges, multi-range multipart/byteranges, and If-Range — useful for COGs, video, resumable downloads
  • Directory listing with sorted entries and human-readable sizes
  • Read-only: GET, HEAD, OPTIONS, TRACE
  • Optional ETag header (value = file's lastModified timestamp in millis)
  • Optional authentication: Basic, Bearer, API Key, Custom Header (repeatable, any match)
  • Network conditioning: --latency and --bandwidth to simulate a distant, throttled bucket
  • Path-traversal protection
  • Access log includes the Range header when present

Requirements

  • JDK 25 or newer on $PATH (tested with Temurin 25)

Install

The installed binary is named httpserv.sh to avoid colliding with the NSS test server that Homebrew ships as /opt/homebrew/bin/httpserv.

Clone and use the provided Makefile:

git clone https://github.com/multiversio/httpserv.sh.git
cd httpserv.sh
sudo make install # installs to /usr/local/bin/httpserv.sh

The install target respects the standard PREFIX and DESTDIR variables, so you can install without sudo into a user-writable location:

make install PREFIX=$HOME/.local # -> ~/.local/bin/httpserv.sh

To uninstall:

sudo make uninstall # or: make uninstall PREFIX=$HOME/.local

Alternatively, download the raw script directly:

curl -fsSL https://raw.githubusercontent.com/multiversio/httpserv.sh/main/httpserv.sh \
-o ~/.local/bin/httpserv.sh && chmod +x ~/.local/bin/httpserv.sh

Testing

make test# runs test/run.sh (curl against a live server)

Or drive the scripts directly:

./test/smoke.sh # open-server tests
./test/ranges.sh # RFC 7233 range request compliance
./test/auth.sh # auth + range-with-auth tests
./test/latency.sh # --latency
./test/bandwidth.sh # --bandwidth

They all honor PORT=... and HTTPSERV=... env overrides. CI runs the exact same scripts — no duplicated logic.

Usage

httpserv.sh [options] [directory]
-d, --dir DIR directory to serve (default: .)
-p, --port PORT listen port (default: 8080)
-s, --silent suppress access logging
-e, --etag send ETag header (value = lastModified millis)
--latency DUR fixed delay before each file response
(e.g. 150ms, 2s, 1500us; bare number = ms)
--bandwidth RATE throttle response body throughput
(e.g. 10MB/s, 500KB/s; /s optional, 1024-based)
-a, --auth SPEC require authentication (repeatable; any match passes)
basic:USER:PASS
bearer:TOKEN
api-key:HEADER:VALUE
header:H1=V1,H2=V2,... (all headers required)
-h, --help show this help

Examples:

httpserv.sh # serve current dir on :8080
httpserv.sh -p 9000 ./public # serve ./public on :9000
httpserv.sh --etag --silent /data # ETags on, no access logs

Authentication

Pass --auth one or more times. The server accepts a request as authorized if any of the configured credentials matches. With no --auth flags, the server is open.

httpserv.sh --auth basic:alice:s3cret # HTTP Basic
httpserv.sh --auth bearer:eyJhbGciOi... # OAuth/JWT Bearer
httpserv.sh --auth api-key:X-API-Key:abc123 # single-header API key
httpserv.sh --auth header:X-Tenant=acme,X-Env=prod # multi-header (all required)

Multiple schemes can be combined — handy for testing clients that try different auth strategies:

httpserv.sh \
--auth basic:alice:s3cret \
--auth bearer:tok123 \
--auth api-key:X-API-Key:abc

A request that fails authorization gets 401 Unauthorized. WWW-Authenticate challenges are emitted for Basic and Bearer when those schemes are configured.

Schemes map to io.tileverse.rangereader.http.*Authentication classes: BasicAuthentication, BearerTokenAuthentication, ApiKeyAuthentication, CustomHeaderAuthentication. Digest is intentionally unsupported for now.

Range requests

Range handling follows RFC 7233. Every file response advertises Accept-Ranges: bytes.

  • Single range206 Partial Content with a Content-Range header and the requested bytes as the body.
  • Multiple ranges206 with a multipart/byteranges payload. Each part repeats the representation's Content-Type and states its own Content-Range, and the parts keep the order the client listed them in. Ranges are never coalesced or reordered, and there is no cap on how many a client may ask for.
  • Suffix rangesbytes=-N returns the last N bytes; an N larger than the file returns the whole file.
  • Clamping — a last-byte-pos past the end of the file is clamped to the last byte.
  • If-Range — evaluated against the ETag (with --etag) and against Last-Modified, using strong comparison. A validator that no longer matches makes the server ignore Range and answer 200 with the whole file, which is what lets a client resume a download without splicing stale bytes.

416 Range Not Satisfiable, with Content-Range: bytes */<length>, comes back when no requested range overlaps the file, when a suffix length is zero, or when any spec is invalid (last-byte-pos before first-byte-pos). One invalid spec rejects the whole set.

Range is ignored, and the whole file returned, when the range unit is not bytes or when the header value does not parse as a byte-range-set.

HEAD is answered from the same evaluation as GET: same status, same Content-Range, same Content-Length, no body. RFC 7233 section 3.1 asks servers to ignore Range on any method other than GET, but Apache, nginx, Caddy and S3 all answer 206 here, and RFC 7231 section 4.3.2 asks a HEAD response to mirror the header fields of the matching GET. Standing in for those object stores matters more here than the letter of section 3.1, so a client probing range support with HEAD sees what they would send.

Network conditioning

These flags reproduce the quirks of a remote object store so clients can be tested against realistic conditions.

--latency

Adds a fixed delay before each file response is sent, simulating the round-trip time to a distant bucket. Virtual threads make the sleep cheap.

httpserv.sh --latency 150ms # 150 ms before every file response
httpserv.sh --latency 2s # a painfully distant region
httpserv.sh --latency 1500us # sub-millisecond precision

Values accept a us, ms, or s suffix; a bare number is milliseconds. The delay applies only to file responses (200/206); error responses (404, 403, 401), redirects, and directory listings stay instant.

--bandwidth

Throttles response body throughput, metering bytes as they are written so the sender holds back to the configured rate. Pairs with --latency to model a high-latency, fat-pipe object store.

httpserv.sh --bandwidth 10MB/s # cap every response body at 10 MB/s
httpserv.sh --bandwidth 500KB/s # a slow link
httpserv.sh --latency 150ms --bandwidth 5MB/s # both at once

Values accept B, KB, MB, or GB units (1024-based, case-insensitive); a bare number is bytes. The trailing /s is optional. The throttle covers every response body, including single-range and multipart/byteranges reads.

Log format

[2026-04-18T20:05:21.349Z] "GET /opendata/file.tif" "okhttp/5.3.2" Range: bytes=0-16383

Roadmap

httpserv primarily exists to test other components — imageio-ext, tileverse, GeoTools, GeoServer — against cloud-native formats like COG, PMTiles, and GeoParquet. Planned enhancements are geared at reproducing the quirks of real object-storage backends (S3, GCS, Azure Blob) rather than general-purpose static hosting.

Network conditioning

  • --ttfb <duration> — separate "time-to-first-byte" from streaming rate so we can model high-latency-but-fat-pipe object stores independently of throughput.
  • --jitter <pct> — randomize latency/bandwidth by ±pct to avoid lockstep clients.

Failure injection

  • --fail-rate <pct> — return 503 Service Unavailable on a configurable fraction of requests. Exercises client retry/backoff logic.
  • --fail-ranges-rate <pct> — only fail requests that carry a Range header (COG readers are especially sensitive to partial-read failures).
  • --truncate-rate <pct> — close the connection mid-response. Tests how clients handle short reads on range requests.

HTTP behavior

  • Conditional requests — honor If-None-Match / If-Modified-Since304 Not Modified. Pairs with the existing --etag flag to validate cache logic.
  • --tls — serve over HTTPS with an on-the-fly self-signed certificate. Some libraries take different code paths on TLS (connection pooling, ALPN, etc.).

Observability

  • --log-jsonl <file> — structured access log: method, path, range, status, bytes sent, duration. Grep-able for perf regressions and request-pattern asserts.
  • Replay / whitelist mode — load a manifest of allowed URL+range pairs; anything outside returns 403. Lets tests assert the exact request pattern a client made.

CORS is intentionally out of scope: consumers like GeoServer proxy requests server-side, so the browser never talks to httpserv directly.

License

MIT — see LICENSE.

About

Single-file read-only static server using the JDK's built-in HttpServer

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages