Skip to content

Junaid-Ashraf-56/WebServer_Java

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Java Maven Docker JUnit5

Java Web Server

A multithreaded HTTP/1.1 server built from scratch in Java.


Table of Contents


Overview

This is a raw-socket HTTP/1.1 server written in Java using only the standard library — ServerSocket, Socket, and java.util.concurrent. There's no Spring Boot, no servlet container, and no third-party HTTP library underneath it. Every layer — accepting TCP connections, parsing the request line and headers, routing, and writing the response back onto the wire — is hand-built.

The goal of this project is to understand what frameworks like Spring normally abstract away: connection handling, protocol parsing, thread pool sizing, and graceful shutdown.

What it does:

  • Accepts TCP connections on a fixed port and hands each one to a bounded thread pool
  • Parses raw HTTP/1.1 requests (request line, headers, query string, body) by hand
  • Runs each request through a small middleware chain before routing
  • Dispatches to registered handlers by METHOD + path, falling back to static file serving for unmatched GET requests
  • Serves static files from a fixed webroot with directory traversal protection
  • Converts any unhandled exception into a proper HTTP error response instead of crashing the connection
  • Shuts down gracefully on SIGTERM/Ctrl+C, draining in-flight requests before closing

Architecture

Java Web Server Architecture

Architecture Details

Java Web Server Architecture

---

Request Lifecycle

  1. Main binds a ServerSocket on port 9090 and loops on accept()
  2. Each accepted Socket is wrapped in a ClientHandler and submitted to the ThreadPoolExecutor
  3. HttpRequestParser reads the request line, headers, and (if Content-Length is present) the body from the socket's InputStream
  4. The resulting HttpRequest is passed through the middleware chain:
    • Logging prints method, path, version, and key headers, then passes through
    • Validation rejects unsupported methods, malformed paths, non-HTTP/1.1 requests, and POSTs with an empty body — any rejection short-circuits the chain with a 400
  5. Router looks up METHOD + path in its handler map; if nothing matches and the method is GET, it falls back to StaticFileHandler; otherwise it returns a 404
  6. The handler builds an HttpResponse
  7. ResponseWriter serializes the status line, headers (computing Content-Length from the UTF-8 byte length of the body), and body, then flushes it to the socket
  8. The connection is closed — there's no keep-alive, so every request is a fresh TCP handshake

Concurrency Model

BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>(15);
ThreadPoolExecutor threadPoolExecutor =
    new ThreadPoolExecutor(5, 5, 1000, TimeUnit.SECONDS, queue);

Five worker threads, backed by a bounded queue of 15 pending connections — this is a deliberate choice over an unbounded pool or an unbounded queue: it caps both how many requests run concurrently and how many can pile up waiting, rather than letting either grow without limit under load.

Graceful shutdown is wired to the JVM shutdown hook in Main:

Runtime.getRuntime().addShutdownHook(new Thread(() ->
    ServerLifecycle.shutdown(serverSocket, threadPoolExecutor)
));

On shutdown, ServerLifecycle closes the listening socket first (so no new connections are accepted), then calls executor.shutdown() to let in-flight requests finish, waiting up to 5 seconds before forcing a shutdownNow().


Routing

Router maps "METHOD /path" keys to handlers and is seeded with two example resources on startup:

Method Path Handler
GET /hello HelloHandler
POST /hello HelloHandler
GET /users UsersHandler
POST /users UsersHandler

register(method, path, handler) and unregister(method, path) allow adding or removing routes at runtime. Any unmatched GET request falls through to StaticFileHandler before returning a 404; unmatched non-GET requests go straight to 404.

HelloHandler and UsersHandler are intentionally simple example endpoints backed by static in-memory data (SeedData) — they demonstrate the routing and handler contract rather than real business logic.


Static File Serving

StaticFileHandler serves files from src/main/resources/static, defaulting / to index.html. Every request path is resolved through DirectoryTraversalProtector before the file is touched:

Path root = staticRoot.normalize();
Path requestedFile = root.resolve(path).normalize();

if (!requestedFile.startsWith(root)) {
    throw new SecurityException("Access denied");
}

A request like GET /../../etc/passwd normalizes outside the static root and is rejected with a 400 rather than served — this is covered by a dedicated test (DirectoryTraversalProtectorTest).

MIME types are resolved by file extension (MimeTypeResolver): .html, .css, .js, .json, .txt, .png, .jpg/.jpeg, .gif, .svg, and .ico are recognized, falling back to application/octet-stream for anything else.


Middleware

List<Middleware> middlewareList = List.of(
    new Logging(),
    new Validation()
);

Each Middleware returns null to let the request continue, or an HttpResponse to short-circuit the chain. Validation currently enforces:

  • Method must be GET or POST
  • Path must be non-empty and start with /
  • HTTP version must be exactly HTTP/1.1
  • A POST request must have a non-empty body

Error Handling

GlobalExceptionHandler maps exceptions thrown anywhere in the request cycle to HTTP status codes:

Exception Status
IllegalArgumentException (malformed request line, headers, or Content-Length) 400 Bad Request
IOException 500 Internal Server Error
NullPointerException 500 Internal Server Error
Anything else 500 Internal Server Error

Every response — success or failure — is written back through the same ResponseWriter, so the client always gets a well-formed HTTP response.


Getting Started

Requires Java 21 and Maven.

# Clone
git clone https://github.com/Junaid-Ashraf-56/WebServer_Java.git
cd WebServer_Java

# Build and run tests
mvn clean install

# Run
java -jar target/java-web-server.jar

The server listens on port 9090. Try it:

curl http://localhost:9090/hello
curl http://localhost:9090/users
curl http://localhost:9090/

Running with Docker

docker build -t java-web-server .
docker run --rm -p 9090:9090 java-web-server

The image is a multi-stage build — a Maven stage compiles and packages the jar, and the runtime stage (eclipse-temurin:21-jre) runs it as a non-root user.


Testing

8 tests across 5 test classes, run via JUnit 5 and Maven Surefire:

Test Class What it covers
RouterTest Seeded route lookup, unknown route returns null
ValidationTest A valid GET request passes validation cleanly
MimeTypeResolverTest .html, .css, .js resolve to the correct MIME type
DirectoryTraversalProtectorTest A ../ traversal attempt throws SecurityException
StaticFileHandlerTest GET / correctly returns index.html with a 200
mvn test

Project Structure

WebServer/
├── Dockerfile
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   ├── Main.java
│   │   │   ├── data/
│   │   │   │   └── SeedData.java
│   │   │   ├── error/
│   │   │   │   └── GlobalExceptionHandler.java
│   │   │   ├── handler/
│   │   │   │   ├── RequestHandler.java
│   │   │   │   ├── HelloHandler.java
│   │   │   │   ├── UsersHandler.java
│   │   │   │   └── StaticFileHandler.java
│   │   │   ├── http/
│   │   │   │   ├── request/
│   │   │   │   │   ├── HttpRequest.java
│   │   │   │   │   └── HttpRequestParser.java
│   │   │   │   └── response/
│   │   │   │       ├── HttpResponse.java
│   │   │   │       ├── HttpStatus.java
│   │   │   │       └── ResponseWriter.java
│   │   │   ├── middleware/
│   │   │   │   ├── Middleware.java
│   │   │   │   ├── MiddlewareChain.java
│   │   │   │   ├── Logging.java
│   │   │   │   └── Validation.java
│   │   │   ├── router/
│   │   │   │   └── Router.java
│   │   │   ├── security/
│   │   │   │   └── DirectoryTraversalProtector.java
│   │   │   ├── server/
│   │   │   │   ├── ClientHandler.java
│   │   │   │   └── ServerLifecycle.java
│   │   │   └── util/
│   │   │       └── MimeTypeResolver.java
│   │   └── resources/
│   │       └── static/
│   │           ├── index.html
│   │           ├── about.html
│   │           ├── style.css
│   │           └── app.js
│   └── test/
│       └── java/
│           ├── RouterTest.java
│           ├── ValidationTest.java
│           ├── MimeTypeResolverTest.java
│           ├── DirectoryTraversalProtectorTest.java
│           └── StaticFileHandlerTest.java

Known Limitations

This is a learning project, and these are open with intent rather than hidden:

  • Only GET and POST are accepted. HEAD, PUT, DELETE, and PATCH are rejected by Validation with a 400 rather than a more specific 405/501.
  • No custom rejection policy on the thread pool. A burst of connections beyond 5 running + 15 queued will throw an unhandled RejectedExecutionException in the accept loop rather than failing gracefully.
  • The port is hardcoded (9090 in Main.java) — there's no external configuration file yet.
  • No HTTPS/TLS.

Releases

Packages

Contributors

Languages