Extending jHTTPd: Writing Custom Handlers and MiddlewarejHTTPd is a compact, embeddable Java HTTP server designed for minimal footprint and straightforward integration into applications that need basic web-serving capabilities without the complexity of a full Java EE stack. While its core provides routing, static file serving, and basic request/response handling, the true power for many projects comes from extending jHTTPd with custom handlers and middleware. This article walks through designing, implementing, testing, and deploying custom handlers and middleware for jHTTPd, with practical examples and best practices.
Table of contents
- Why extend jHTTPd?
- jHTTPd architecture overview
- Handler vs. middleware: roles and responsibilities
- Designing your custom handler
- Example: dynamic JSON API handler
- Example: file upload handler
- Implementing middleware
- Example: request logging middleware
- Example: authentication middleware (token-based)
- Chaining middleware and ordering concerns
- Error handling and recovery
- Performance considerations and benchmarking
- Testing strategies (unit and integration)
- Packaging and deployment
- Security best practices
- Example project: a small REST microservice using jHTTPd
- Conclusion
Why extend jHTTPd?
Extending jHTTPd allows you to:
- Add application-specific business logic directly into the request pipeline.
- Implement cross-cutting concerns (logging, auth, metrics) without external proxies.
- Keep the server lightweight while tailoring functionality precisely to your use case.
Extensibility keeps your application modular and maintainable.
jHTTPd architecture overview
At its core, jHTTPd typically exposes:
- A listener that accepts TCP connections.
- A simple request parser that produces an object representing the HTTP request (method, path, headers, body).
- A response builder that streams status, headers, and body back to the client.
- A routing mechanism which maps paths (often via simple path patterns) to handler instances.
jHTTPd’s extension points generally include:
- Handler interface (or abstract class) for endpoint logic.
- Middleware hooks that run before/after handlers.
- Static file serving hooks with customizable root directories and caching rules.
Understanding these elements is essential before adding custom code.
Handler vs. middleware: roles and responsibilities
- Handler: Core processing unit that produces a response for a matched route. It is usually invoked once routing chooses a target for the request.
- Middleware: A wrapper around the chain of handlers that can modify the request or response, short-circuit processing, add headers, perform authentication, log activity, etc.
Think of middleware as layers of an onion around handlers: each middleware can inspect and change the request on the way in and the response on the way out.
Designing your custom handler
A well-designed handler should:
- Accept an immutable or clearly-documented mutable request object.
- Return a response object (or write to a streamed response).
- Avoid blocking long-running tasks on the request thread — use async mechanisms or background executors where appropriate.
- Validate inputs and sanitize outputs.
Example: dynamic JSON API handler
Goals: create a handler that responds to GET /api/time with JSON containing the server time and a request ID.
Pseudocode interface (illustrative — adapt to actual jHTTPd API):
public class TimeApiHandler implements HttpHandler { @Override public void handle(HttpRequest req, HttpResponse res) throws IOException { String requestId = req.getHeader("X-Request-ID"); if (requestId == null) requestId = UUID.randomUUID().toString(); Map<String, Object> payload = new HashMap<>(); payload.put("time", Instant.now().toString()); payload.put("requestId", requestId); String json = new ObjectMapper().writeValueAsString(payload); res.setStatus(200); res.setHeader("Content-Type", "application/json"); res.getWriter().write(json); } }
Notes:
- Use a shared, thread-safe ObjectMapper instance to avoid repeated costly instantiation.
- Consider caching common response fragments if under heavy load.
Example: file upload handler
Goals: handle multipart/form-data POST to /upload, stream file content to disk without loading into memory.
Key points:
- Use a streaming multipart parser.
- Validate file size and type before accepting.
- Write to a temporary file and move to a final location only after validation.
Illustrative snippet:
public class UploadHandler implements HttpHandler { private final Path uploadDir; private final long maxBytes; public UploadHandler(Path uploadDir, long maxBytes) { ... } @Override public void handle(HttpRequest req, HttpResponse res) throws IOException { if (!"POST".equals(req.getMethod())) { res.setStatus(405); return; } MultipartStream multipart = new MultipartStream(req.getInputStream(), req.getHeader("Content-Type")); while (multipart.hasNext()) { Part part = multipart.next(); if (part.isFile()) { Path temp = Files.createTempFile(uploadDir, "up-", ".tmp"); try (OutputStream out = Files.newOutputStream(temp, StandardOpenOption.WRITE)) { part.writeTo(out, maxBytes); // enforce limit inside } // validate, then move Files.move(temp, uploadDir.resolve(sanitize(part.getFilename())), ATOMIC_MOVE); } } res.setStatus(201); } }
Implementing middleware
Middleware can be implemented as a chain of components that receive a request and a reference to “next” in the chain. Each middleware may call next.handle(request, response) to continue, or short-circuit by writing a response and returning.
Example: request logging middleware
Logs method, path, status, latency, and optionally request ID.
public class LoggingMiddleware implements Middleware { private final Logger logger = LoggerFactory.getLogger(LoggingMiddleware.class); @Override public void handle(HttpRequest req, HttpResponse res, Chain next) throws IOException { long start = System.nanoTime(); try { next.handle(req, res); } finally { long elapsedMs = (System.nanoTime() - start) / 1_000_000; String requestId = req.getHeader("X-Request-ID"); logger.info("{} {} {} {}ms", req.getMethod(), req.getPath(), res.getStatus(), elapsedMs); } } }
Tips:
- Avoid logging large request/response bodies.
- Use sampling under high load.
Example: authentication middleware (token-based)
Validates an Authorization header and sets an authenticated user attribute on the request.
public class TokenAuthMiddleware implements Middleware { private final TokenService tokenService; public TokenAuthMiddleware(TokenService tokenService) { this.tokenService = tokenService; } @Override public void handle(HttpRequest req, HttpResponse res, Chain next) throws IOException { String auth = req.getHeader("Authorization"); if (auth == null || !auth.startsWith("Bearer ")) { res.setStatus(401); res.setHeader("WWW-Authenticate", "Bearer"); res.getWriter().write("Unauthorized"); return; } String token = auth.substring(7); User user = tokenService.verify(token); if (user == null) { res.setStatus(401); res.getWriter().write("Invalid token"); return; } req.setAttribute("user", user); next.handle(req, res); } }
Security notes:
- Verify tokens using a cryptographic library; avoid custom crypto.
- Consider token expiry, revocation lists, and scopes/claims.
Chaining middleware and ordering concerns
Order matters. Typical ordering:
- Connection-level middleware (rate limiting, IP allow/deny)
- Security/authentication
- Request parsing (body, form, multipart)
- Application middleware (metrics, business logic wrappers)
- Response transformation/compression
- Logging (often placed around everything to capture final status)
Implement chain construction that’s deterministic and easy to reason about (e.g., builder or pipeline pattern).
Error handling and recovery
- Catch unchecked exceptions in middleware and handlers; convert to appropriate HTTP responses (500, 400, etc.).
- Avoid leaking stack traces in production responses. Log internal errors with an error ID and return a generic message with that ID.
- Provide a global exception middleware as the outermost layer to capture any uncaught exceptions.
Example:
public class ExceptionMiddleware implements Middleware { @Override public void handle(HttpRequest req, HttpResponse res, Chain next) throws IOException { try { next.handle(req, res); } catch (BadRequestException bre) { res.setStatus(400); res.getWriter().write(bre.getMessage()); } catch (Exception e) { String errorId = UUID.randomUUID().toString(); logger.error("Unhandled error {}: {}", errorId, e); res.setStatus(500); res.getWriter().write("Internal server error. ID: " + errorId); } } }
Performance considerations and benchmarking
- Use non-blocking I/O where possible; if jHTTPd is blocking, use a pool of worker threads and avoid per-request thread creation.
- Reuse objects (e.g., ObjectMapper) that are thread-safe.
- Prefer streaming for large uploads/downloads to avoid OOM.
- Use compression selectively; compressed responses use CPU.
- Add metrics (request counts, latencies) and benchmark using tools like wrk, ApacheBench, or k6.
Measure:
- Throughput (requests/sec)
- Median and p95/p99 latencies
- CPU and memory usage under load
Testing strategies (unit and integration)
- Unit test handlers in isolation by mocking request/response objects.
- Integration test the whole pipeline with an embedded jHTTPd instance listening on a random port. Use HTTP clients (HttpClient, OkHttp) to make real requests.
- Test edge cases: malformed headers, partial bodies, slow clients.
- Use property-based tests for parsers and multipart handling if possible.
Packaging and deployment
- Package custom handlers/middleware as a JAR that your application loads. Keep dependencies minimal.
- If embedding jHTTPd into a larger app, ensure lifecycle hooks for graceful shutdown to close open streams and finish in-flight requests.
- For production, run behind a reverse proxy (if needed) for TLS termination, virtual hosting, or advanced routing — or implement TLS in jHTTPd if supported.
Security best practices
- Enforce TLS for sensitive endpoints. Prefer widely-used libraries for TLS management.
- Limit request body sizes and implement timeouts to mitigate slowloris.
- Sanitize file names and paths to prevent path traversal.
- Use secure headers (Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security).
- Validate input lengths and types to avoid injection attacks.
Example project: a small REST microservice using jHTTPd
Sketch of components:
- Main: initialize jHTTPd, register middleware and handlers.
- Middleware: ExceptionMiddleware, LoggingMiddleware, TokenAuthMiddleware, MetricsMiddleware
- Handlers: TimeApiHandler (/api/time), UploadHandler (/upload), StaticHandler (/assets)
- Utilities: TokenService, StorageService, JsonUtil (shared ObjectMapper)
Main wiring (illustrative):
public class App { public static void main(String[] args) throws IOException { HttpServer server = new JHttpdServer(8080); Pipeline pipeline = new Pipeline.Builder() .add(new ExceptionMiddleware()) .add(new LoggingMiddleware()) .add(new TokenAuthMiddleware(new TokenService())) .add(new MetricsMiddleware()) .build(); Router router = new Router(); router.get("/api/time", new TimeApiHandler()); router.post("/upload", new UploadHandler(Paths.get("uploads"), 10_000_000)); router.get("/assets/*", new StaticHandler(Paths.get("public"))); server.setHandler((req, res) -> pipeline.handle(req, res, () -> router.route(req, res))); server.start(); } }
Conclusion
Extending jHTTPd with custom handlers and middleware keeps your application lightweight while enabling powerful, application-specific capabilities. Focus on clean separation between request handling and cross-cutting concerns, pay attention to ordering and error handling, and apply performance and security best practices. With careful design you can build robust microservices and embed web functionality directly into your Java applications without pulling in heavy frameworks.
Leave a Reply