Skip to content

HTTP Server

Luca_Previ0o edited this page Dec 10, 2025 · 2 revisions

HTTP Server: Protocol Implementation

HttpServer is a complete HTTP/1.1 server implementation built on top of ConnectionServer. It provides annotation-based routing, request parsing, response building, and static file serving.

Position in Architecture

┌─────────────────────────────────────┐
│  HttpServer                         │  ← YOU ARE HERE
│  (HTTP protocol implementation)     │
├─────────────────────────────────────┤
│  ConnectionServer                   │  ← TCP transport
│  (Socket handling)                  │
├─────────────────────────────────────┤
│  Server (abstract base)             │  ← Foundation
└─────────────────────────────────────┘

Files:


Quick Start

Basic Server

import jsi.connection.http.*;
import jsi.connection.http.response.HttpResponseType;

public class MyWebServer extends HttpServer {
    
    public MyWebServer(int port) {
        super(port);
    }
    
    @Route(path = "/")
    public HttpResponse home(HttpRequest request) {
        return createHtmlResponse(HttpResponseType.OK, 
            "<h1>Welcome!</h1>");
    }
    
    @Route(path = "/about")
    public HttpResponse about(HttpRequest request) {
        return createHtmlResponse(HttpResponseType.OK, 
            "<h1>About Page</h1>");
    }
    
    public static void main(String[] args) {
        new MyWebServer(8080).start();
    }
}

Access at:

  • http://localhost:8080/ - Home page
  • http://localhost:8080/about - About page

Core Features

1. Annotation-Based Routing

The @Route annotation maps URL paths to handler methods:

@Route(path = "/users")
public HttpResponse getUsers(HttpRequest request) {
    // Handle /users route
}

@Route(path = "/products")
public HttpResponse getProducts(HttpRequest request) {
    // Handle /products route
}

How it works:

  1. On server startup, HttpServer scans all methods in your class
  2. Methods annotated with @Route are registered as route handlers
  3. Incoming requests are matched against registered paths
  4. Matching method is invoked with the HttpRequest object

2. Static File Serving

Serve HTML files directly without writing handler logic:

@Route(path = "/", staticResource = "static/index.html")
public HttpResponse home(HttpRequest request) {
    return null;  // File served automatically
}

@Route(path = "/contact", staticResource = "static/contact.html")
public HttpResponse contact(HttpRequest request) {
    return null;  // File served automatically
}

File structure:

project/
├── static/
│   ├── index.html
│   ├── contact.html
│   └── about.html
├── 
└── MyWebServer.java

Important: Paths are relative to the working directory (where you run java).

3. Request Parameters

Extract query parameters from URLs:

@Route(path = "/greet")
public HttpResponse greet(HttpRequest request) {
    String name = (String) request.getParameter("name");
    String age = (String) request.getParameter("age");
    
    if (name == null) name = "Guest";
    
    String html = String.format(
        "<h1>Hello, %s!</h1><p>Age: %s</p>", 
        name, age != null ? age : "unknown"
    );
    
    return createHtmlResponse(HttpResponseType.OK, html);
}

Usage:

  • http://localhost:8080/greet → "Hello, Guest!"
  • http://localhost:8080/greet?name=Alice → "Hello, Alice!"
  • http://localhost:8080/greet?name=Bob&age=25 → "Hello, Bob! Age: 25"

4. Dynamic Response Building

Build responses with custom headers and content types:

@Route(path = "/api/users")
public HttpResponse getUsersApi(HttpRequest request) {
    String json = "[{\"id\": 1, \"name\": \"Alice\"}, " +
                  "{\"id\": 2, \"name\": \"Bob\"}]";
    
    return new HttpResponse(
        HttpResponseType.OK,
        new ResponseHeader[] {
            new HttpResponseHeader("Content-Type", "application/json"),
            new HttpResponseHeader("Cache-Control", "no-cache"),
            new HttpResponseHeader("Content-Length", 
                String.valueOf(json.getBytes(StandardCharsets.UTF_8).length))
        },
        new HttpResponseBody(json)
    );
}

Convenience method:

protected Response createHtmlResponse(HttpResponseType type, String content) {
    // Automatically sets Content-Type and Content-Length
}

HTTP Request Parsing

HttpRequest Class

File: connection/http/HttpRequest.java

public class HttpRequest implements Request {
    
    private String path;                        // "/users"
    private HttpRequestType requestType;        // GET, POST, PUT, DELETE
    private HttpRequestParameter[] parameters;  // Query params
    private HttpRequestHeader[] headers;        // HTTP headers
    
    public String getPath() { return path; }
    
    public Object getParameter(String name) {
        // Returns parameter value or null
    }
    
    public HttpRequestType getHttpRequestType() { 
        return requestType; 
    }
}

Request Components

Request line:

GET /users?id=123&name=Alice HTTP/1.1
│   │                        │
└─ Method                    └─ Protocol version
    └─ Path + query string

Parsing example:

// Raw HTTP request
String raw = "GET /users?id=123 HTTP/1.1\r\n" +
             "Host: localhost\r\n" +
             "User-Agent: Mozilla/5.0\r\n" +
             "\r\n";

HttpRequest request = new HttpRequest(raw);

String path = request.getPath();               // "/users"
String id = request.getParameter("id");        // "123"
HttpRequestType type = request.getHttpRequestType();  // GET

Supported Request Types

File: connection/http/request/HttpRequestType.java

public enum HttpRequestType {
    GET,
    POST,
    PUT,
    DELETE,
    HEAD,
    OPTIONS,
    PATCH
}

Usage:

@Route(path = "/users")
public HttpResponse handleUsers(HttpRequest request) {
    switch (request.getHttpRequestType()) {
        case GET:
            return listUsers();
        case POST:
            return createUser(request);
        case DELETE:
            return deleteUser(request);
        default:
            return createHtmlResponse(
                HttpResponseType.METHOD_NOT_ALLOWED, 
                "<h1>405 - Method Not Allowed</h1>"
            );
    }
}

HTTP Response Building

HttpResponse Class

File: connection/http/HttpResponse.java

public class HttpResponse implements Response {
    
    private ResponseType responseType;    // 200 OK, 404 NOT FOUND, etc.
    private ResponseHeader[] headers;     // HTTP headers
    private ResponseBody body;            // Response content
    
    @Override
    public String serialize() {
        // Converts to HTTP format:
        // HTTP/1.1 200 OK
        // Content-Type: text/html
        // Content-Length: 123
        //
        // <html>...</html>
    }
}

Response Types

File: connection/http/response/HttpResponseType.java

public enum HttpResponseType implements ResponseType {
    OK("200 OK"),
    CREATED("201 Created"),
    NO_CONTENT("204 No Content"),
    BAD_REQUEST("400 Bad Request"),
    UNAUTHORIZED("401 Unauthorized"),
    FORBIDDEN("403 Forbidden"),
    NOT_FOUND("404 Not Found"),
    METHOD_NOT_ALLOWED("405 Method Not Allowed"),
    INTERNAL_SERVER_ERROR("500 Internal Server Error"),
    SERVICE_UNAVAILABLE("503 Service Unavailable");
    
    private String name;
    
    HttpResponseType(String name) { this.name = name; }
    
    @Override
    public String getName() { return name; }
}

Building Responses

Simple HTML response:

return createHtmlResponse(HttpResponseType.OK, "<h1>Success</h1>");

JSON response:

String json = "{\"status\": \"ok\"}";
return new HttpResponse(
    HttpResponseType.OK,
    new ResponseHeader[] {
        new HttpResponseHeader("Content-Type", "application/json")
    },
    new HttpResponseBody(json)
);

Redirect response:

return new HttpResponse(
    HttpResponseType.MOVED_PERMANENTLY,
    new ResponseHeader[] {
        new HttpResponseHeader("Location", "/new-path")
    },
    new HttpResponseBody("")
);

Routing System Deep Dive

Route Registration Process

Server starts
    ↓
onServerStarted() called
    ↓
registerRoutes() scans class
    ↓
Finds methods with @Route annotation
    ↓
Creates RoutedMethod objects
    ↓
Stores in routes list
    ↓
Ready to handle requests

Internal Route Handling

File: connection/http/HttpServer.java

public class HttpServer extends ConnectionServer {
    
    private final List<RoutedMethod> routes = new ArrayList<>();
    
    protected void registerRoutes() {
        for (Method method : this.getClass().getDeclaredMethods()) {
            if (method.isAnnotationPresent(Route.class)) {
                Route routeAnnotation = method.getAnnotation(Route.class);
                String path = routeAnnotation.path();
                String staticResource = routeAnnotation.staticResource();
                
                routes.add(new RoutedMethod(path, method, staticResource));
            }
        }
    }
    
    @Override
    public HttpResponse handleRequest(Request request) {
        String path = extractPath(request);
        
        for (RoutedMethod handler : routes) {
            if (handler.getPath().equals(path)) {
                try {
                    return handler.handleRequest(request);
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                }
            }
        }
        
        return handleNotFound(request);
    }
}

RoutedMethod Class

public class RoutedMethod {
    String path;
    Method method;
    String staticResource;
    
    public HttpResponse handleRequest(Request request) throws Exception {
        // If static file, serve it
        if (staticResource != null && !staticResource.isEmpty()) {
            return serveHtmlFile(staticResource);
        }
        
        // Otherwise, invoke handler method
        method.setAccessible(true);
        return (HttpResponse) method.invoke(HttpServer.this, request);
    }
}

Custom 404 Handler

Override handleNotFound() to customize error pages:

@Override
protected HttpResponse handleNotFound(Request request) {
    String html = "<html><head><title>404</title></head><body>" +
                  "<h1>Page Not Found</h1>" +
                  "<p>The requested resource doesn't exist.</p>" +
                  "<a href='/'>Go Home</a>" +
                  "</body></html>";
    
    return createHtmlResponse(HttpResponseType.NOT_FOUND, html);
}

Static File Serving

Using staticResource

@Route(path = "/", staticResource = "static/index.html")
public HttpResponse home(HttpRequest request) {
    return null;  // Return value ignored
}

Behind the scenes:

  1. RoutedMethod checks if staticResource is set
  2. Calls serveHtmlFile(staticResource)
  3. Uses ConnectionServer.readFile() to read file
  4. Wraps content in HttpResponse with appropriate headers

Manual File Serving

@Route(path = "/download")
public HttpResponse download(HttpRequest request) {
    String filename = (String) request.getParameter("file");
    
    try {
        String content = readFile("files/" + filename);
        
        return new HttpResponse(
            HttpResponseType.OK,
            new ResponseHeader[] {
                new HttpResponseHeader("Content-Type", "application/octet-stream"),
                new HttpResponseHeader("Content-Disposition", 
                    "attachment; filename=\"" + filename + "\"")
            },
            new HttpResponseBody(content)
        );
    } catch (IOException e) {
        return handleFileReadError("files/" + filename, e);
    }
}

Supported File Types

By default, serveHtmlFile() sets Content-Type: text/html. For other types, use serveFile():

protected HttpResponse serveFile(String filePath, String contentType) {
    // Serves file with custom Content-Type
}

Example:

@Route(path = "/styles.css")
public HttpResponse css(HttpRequest request) {
    try {
        return serveFile("static/styles.css", "text/css");
    } catch (IOException e) {
        return handleFileReadError("static/styles.css", e);
    }
}

Advanced Usage

REST API Example

public class RestApiServer extends HttpServer {
    
    private List<User> users = new ArrayList<>();
    
    public RestApiServer(int port) {
        super(port);
        // Seed data
        users.add(new User(1, "Alice", "[email protected]"));
        users.add(new User(2, "Bob", "[email protected]"));
    }
    
    @Route(path = "/api/users")
    public HttpResponse getUsers(HttpRequest request) {
        String json = convertUsersToJson(users);
        
        return new HttpResponse(
            HttpResponseType.OK,
            new ResponseHeader[] {
                new HttpResponseHeader("Content-Type", "application/json")
            },
            new HttpResponseBody(json)
        );
    }
    
    @Route(path = "/api/user")
    public HttpResponse getUser(HttpRequest request) {
        String idParam = (String) request.getParameter("id");
        
        if (idParam == null) {
            return jsonError("Missing 'id' parameter");
        }
        
        int id = Integer.parseInt(idParam);
        User user = findUserById(id);
        
        if (user == null) {
            return jsonError("User not found");
        }
        
        return jsonResponse(HttpResponseType.OK, convertUserToJson(user));
    }
    
    private HttpResponse jsonError(String message) {
        String json = "{\"error\": \"" + message + "\"}";
        return jsonResponse(HttpResponseType.BAD_REQUEST, json);
    }
    
    private HttpResponse jsonResponse(HttpResponseType type, String json) {
        return new HttpResponse(
            type,
            new ResponseHeader[] {
                new HttpResponseHeader("Content-Type", "application/json")
            },
            new HttpResponseBody(json)
        );
    }
}

Session Management

public class SessionServer extends HttpServer {
    
    private Map<String, Session> sessions = new ConcurrentHashMap<>();
    
    @Route(path = "/login")
    public HttpResponse login(HttpRequest request) {
        String username = (String) request.getParameter("username");
        
        // Create session
        String sessionId = UUID.randomUUID().toString();
        sessions.put(sessionId, new Session(username));
        
        return new HttpResponse(
            HttpResponseType.OK,
            new ResponseHeader[] {
                new HttpResponseHeader("Set-Cookie", 
                    "sessionId=" + sessionId + "; Path=/; HttpOnly")
            },
            new HttpResponseBody("<h1>Logged in as " + username + "</h1>")
        );
    }
    
    @Route(path = "/profile")
    public HttpResponse profile(HttpRequest request) {
        String sessionId = extractSessionIdFromCookie(request);
        
        if (sessionId == null || !sessions.containsKey(sessionId)) {
            return createHtmlResponse(HttpResponseType.UNAUTHORIZED, 
                "<h1>Please log in</h1>");
        }
        
        Session session = sessions.get(sessionId);
        return createHtmlResponse(HttpResponseType.OK, 
            "<h1>Welcome, " + session.getUsername() + "</h1>");
    }
}

Configuration and Customization

Override Lifecycle Hooks

@Override
protected void onBeforeStart() {
    System.out.println("Loading configuration...");
    loadDatabase();
    initializeCache();
}

@Override
protected void onServerStarted() {
    System.out.println("HTTP Server running on port " + getPort());
    logToFile("Server started at " + LocalDateTime.now());
}

Custom Request Parsing

Override parseRequest() to customize HTTP parsing:

@Override
protected HttpRequest parseRequest(String input) {
    HttpRequest request = super.parseRequest(input);
    
    // Add custom processing
    logRequest(request);
    validateRequest(request);
    
    return request;
}

Middleware Pattern

public abstract class MiddlewareHttpServer extends HttpServer {
    
    @Override
    public HttpResponse handleRequest(Request request) {
        // Pre-processing
        HttpRequest httpReq = (HttpRequest) request;
        
        // Logging
        System.out.println(httpReq.getHttpRequestType() + " " + 
                           httpReq.getPath());
        
        // CORS headers
        HttpResponse response = super.handleRequest(request);
        addCorsHeaders(response);
        
        return response;
    }
    
    private void addCorsHeaders(HttpResponse response) {
        // Add CORS headers to response
    }
}

Performance Considerations

Threading

Each HTTP request is handled in a separate thread (inherited from ConnectionServer):

  • Pros: Simple, isolated request handling
  • Cons: Limited scalability (< 10,000 concurrent connections)

See ConnectionServer for details.

Static File Caching

Current implementation reads files on every request. For production, add caching:

private Map<String, String> fileCache = new ConcurrentHashMap<>();

@Override
protected String readFile(String filePath) throws IOException {
    return fileCache.computeIfAbsent(filePath, path -> {
        try {
            return super.readFile(path);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    });
}

Response Compression

Add gzip compression for large responses:

protected HttpResponse compressResponse(HttpResponse response) {
    // Implement gzip compression
    // Add Content-Encoding: gzip header
}

HTTP Protocol Details

HTTP/1.1 Request Format

GET /users?id=123 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: text/html
Connection: keep-alive

Components:

  1. Request line: Method, path, protocol version
  2. Headers: Key-value pairs
  3. Empty line: Separates headers from body
  4. Body: (optional, for POST/PUT requests)

HTTP/1.1 Response Format

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 123
Connection: close

<html><body><h1>Hello</h1></body></html>

Components:

  1. Status line: Protocol, status code, status text
  2. Headers: Key-value pairs
  3. Empty line: Separates headers from body
  4. Body: Response content

Related Documentation


Next: Explore Database Server to see a completely different protocol implementation, or dive into Extensibility Guide to build your own protocols.

Clone this wiki locally