CodeGym /Courses /Java Server /HttpServer and HttpExchange: path → handler

HttpServer and HttpExchange: path → handler

Java Server
Level 22, Lesson 1
Available

1. The JDK HttpServer object map

When you first see server-side code, a beginner's brain often expects magic: “there must be one startServer() and then everything happens by itself.” In HttpServer, there is almost no magic — and that is exactly what makes it useful. There are several basic classes here that make up the whole picture: address, server, context (path), handler, and the request-response “exchange” object.

Server mode is already clear from the application's role. Now it is important to break the mechanics down into details: who holds the port, who maps the path, and who works with the specific request and response.

Let's build a short “map” in one table — that makes it easier to keep in mind who is responsible for what, and not confuse the “server” with the “request” (a very common beginner mix-up).

Entity What it is What it does in our server phase
InetSocketAddress address (host + port) where the server is actually “listening”
HttpServer server object holds the socket, accepts requests, knows about contexts
HttpContext path context binds a path string (for example, /health) to a handler
HttpHandler handler your code that will be called on a request
HttpExchange exchange (request+response) an object for one request; from it we read input and write output

To feel this not as a reference, but as a “flow,” it helps to see the diagram:

flowchart TD
  Client["HTTP client Postman / curl / browser"] -->|"GET /health"| Server["HttpServer"]
  Server --> Context["HttpContext: /health"]
  Context --> Handler["HttpHandler: handle(exchange)"]
  Handler --> Exchange["HttpExchange: request + response"]
  Handler -->|"status + headers + body"| Client

Now it is important not to mix up two steps. First, you need a live server skeleton: address, HttpServer, process start. The external contract /health makes sense only on top of a running skeleton.

In practice, there are two questions you should be able to answer confidently. The first is: “Which object lives for a long time and is the server itself?” (that is HttpServer). The second is: “Which object is created for each request and lets us communicate with the client?” (that is HttpExchange). Everything else is connective tissue that keeps us from drowning in chaos.

2. HttpServer: server and dispatcher

Intuitively, it is tempting to think that the “server” is the code that answers the request. But in JDK HttpServer, the server is more like a “dispatcher”: it listens on a port, accepts connections, and when it sees the request path, decides which handler (handler) to pass the work to. The handler is your code, where you read the request and build the response.

Important point: HttpServer is an object created once at server-mode startup and lives as long as your process runs. It is not created for each request. If you ever see code that “creates a new HttpServer for every request,” know this: somewhere nearby, one network port is crying.

At minimum, the server is created through the factory method HttpServer.create(...). Notice: here we are only creating the object for now. Starting it (start()) is the next step and the next lecture (where we will carefully integrate it into server mode and configuration).

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

// Address on which the server will listen for incoming connections
InetSocketAddress address = new InetSocketAddress("localhost", 8080);

// Create the server, but do NOT start it yet (start() will come later)
HttpServer server = HttpServer.create(address, 0);

In the second argument of create(address, 0), the word backlog looks like a spell from an ancient grimoire. In practice, it is the size of the incoming connection queue “at the OS level.” For a local teaching server, we honestly set 0 to use the default value and not turn this into a “Network Administrator in 3 Days” course.

Next, HttpServer gets a key responsibility: registering contexts, that is, binding pieces of a path to handlers. And here we smoothly move on to the word “context”.

3. InetSocketAddress: host + port without string magic

If you have ever built a URL by concatenating strings and then hit a bug like “one extra slash broke everything,” then you are emotionally ready to love InetSocketAddress. It solves a simple task: fix the listening address as a data structure, not as a “string in the style of ‘http://…’.”

For server mode, we care about two parameters: host and port. With the host, it is important not to overcomplicate things. localhost is usually enough when you are testing the server on your own machine and do not want it to be accessible from the local network. If you set something like 0.0.0.0, the server will start listening on all interfaces — sometimes that is convenient, but for a teaching project it more often means “I accidentally opened the apartment building door and now I wonder why there are people there.”

Here is a small example showing that InetSocketAddress is indeed a structure from which you can read the parameters:

import java.net.InetSocketAddress;

// Create an address as a structure (not as a "glued" string)
InetSocketAddress address = new InetSocketAddress("localhost", 8080);

// Read the parameters back — useful for logs and diagnostics
String host = address.getHostString();
int port = address.getPort();

And one more important trick: port 0. If you specify 0, the operating system will choose a free port for you. This is sometimes used in tests, but for us right now it is more of a source of confusion: you started the server, but it is listening “somewhere unclear.” So in teaching server mode we almost always want an explicit port (and in the next lecture we will take it from configuration).

4. HttpContext: binding a path to a handler

In HttpServer there are no @GetMapping annotations, no “controllers,” no “router out of the box” — and that is exactly why the context looks so transparent. A context is a link: “this path” → “this handler.” And it is defined by the createContext(...) method.

Important practical detail: a context in HttpServer is not full routing, but rather a binding by a path prefix. That is, roughly speaking, "/health" is a branch of the URL tree. And the server can choose the most suitable branch.

Here is what context registration looks like in the simplest form:

import com.sun.net.httpserver.HttpServer;

// Register a handler for /health
server.createContext("/health", exchange -> {
    // This is where the code will go that handles EVERY request to /health
    // (both GET and POST — the method must be checked separately)
});

Here are three things to remember.

First, a context is bound by path, not by the “full URL.” No http://localhost:8080/health should appear here — that is the client's job, not the server's.

Second, HttpServer does not choose a handler by HTTP method. If you bind /health, then both GET and POST and anything else will arrive in this handler. Distinguishing methods is a separate topic for the next day, and we are not going to “hide it under the carpet” in this lecture.

Third, contexts can be more general or more specific, and the server tries to choose the more specific one. For example, if you ever have both "/api" and "/api/v1/reading-list" contexts at the same time, then a request to "/api/v1/reading-list" should go to the more specific handler. This is not magic, just common sense built into the server: first we look for the most exact match.

5. HttpHandler: lambda or class

When you are starting out, a lambda looks very appealing: minimal code, maximum result. But as soon as the handler stops being “two lines,” the lambda turns into a monster that nobody wants to read. Good news: HttpHandler is a normal interface, and you can make the handler a separate class instead of keeping everything inside createContext(...).

Conceptually, HttpHandler looks like this: there is a handle(exchange) method, and exchange is that same “exchange” object that represents one incoming HTTP request and the future response.

Let's look at a handler class in minimal form. It does nothing yet (we will start writing the response in the lecture about /health), but it is already useful as a structure: there is a place where the logic of a specific endpoint will live.

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;

// A separate class is convenient when dependencies appear and the logic grows beyond "two lines"
public class HealthHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        // exchange is an object for one specific request (it contains both request and response)
        // TODO: in the next lecture, we will build the /health response
    }
}

For one technical /health, this may even be too much at this stage: lambdas in LocalApiServer are enough for now. A separate class is especially worthwhile when a path starts having dependencies and more than two lines of logic.

Now let's compare it with a lambda. A lambda is great when you want to quickly see the mechanics: “path → handler → exchange.” For that, it is ideal.

// Fast form: lambda as HttpHandler
server.createContext("/health", exchange -> {
    // Read the method (GET/POST/...)
    String method = exchange.getRequestMethod();

    // Read the path (/health)
    String path = exchange.getRequestURI().getPath();
});

Why do I usually suggest moving to separate classes fairly early? Because in a real project the handler almost always has dependencies: a logger, configuration, ObjectMapper for JSON, possibly services. In a lambda you will start “capturing” external variables, then passing five arguments, then moving code into a separate method, and then you will end up in a separate class anyway — only with a slight smell of refactoring throughout the code.

6. HttpExchange: request and response

HttpExchange is the heart of the whole mechanism. It is the object created by the server for each incoming request and passed to your HttpHandler. Through it, you can read the method, URI, headers, request body, and also set the response status, response headers, and write the response body.

And here it is important to make a psychological shift: HttpExchange is not a “request.” It is an “exchange.” That means it contains both input and output. Sometimes that seems strange (“why not two objects?”), but at a learning level this is actually convenient: you do not lose the connection between “what came in” → “what we sent out.”

Let's start with the simplest thing: method and path. These are the two things you almost always want to see in logs (and that will be very useful when we check server mode).

import com.sun.net.httpserver.HttpExchange;

// Request method: GET / POST / PUT / ...
String method = exchange.getRequestMethod();

// Request path: for example, /health
String path = exchange.getRequestURI().getPath();

Next, you often need to read the request headers. In HttpExchange, they are available as exchange.getRequestHeaders(). This is not a “pretty DTO,” but rather a straightforward structure. Still, for a start one method is enough: getFirst(...) to get one value.

import com.sun.net.httpserver.Headers;

// Request headers
Headers headers = exchange.getRequestHeaders();

// For example, read Accept (may be null — that is normal)
String accept = headers.getFirst("Accept");

accept here may turn out to be null, and that is normal: the client is not required to send Accept. Our goal right now is not validation or an ideal contract, but understanding the mechanics.

HttpExchange also has an input stream that contains the request body:

import java.io.InputStream;

// Request body is a byte stream (later we will decode/parse it, for example as JSON)
InputStream bodyStream = exchange.getRequestBody();

A quick but important note: the fact that body is an InputStream means “these are bytes, not JSON.” There are still many steps before turning it into a DTO through Jackson. We will do those later, on the next day, and that will be a separate story about “how not to lose your mind without Spring MVC binding.”

And finally, HttpExchange has the “response side.” Even if you are not writing the body yet, you should already understand that the response lives here: response headers and the response body stream.

import java.io.OutputStream;

// Set response headers BEFORE sending the body
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");

// Stream into which we write the response body
OutputStream out = exchange.getResponseBody();

Do not lock this in as a “ready-made template” yet. Right now we only need to understand that the same exchange is the place where you do everything. And in the next lecture we will assemble the minimal correct response for /health (with status and JSON).

7. Structure in ReadLater Starter

When you add a server to a project, there is a temptation to do everything in one place: create the server in ReadLaterApplication, register the paths there, and write the responses there too. That is indeed faster... right up until you want a second endpoint. Then a third appears, and suddenly ReadLaterApplication turns into the “main file of the universe,” which nobody opens without a little anxiety.

The structure we already laid down earlier helps us: package-by-feature plus an explicit composition root. It is more honest to keep the server startup and the technical /health in app.server for now, because this is not a domain reading-list feature, but an infrastructural entry point to the local API. ServerConfig stays in config, because it is configuration. And the domain handlers for reading-list paths logically belong near the feature, for example in readinglist.http.

A small illustration of “where to aim” (without trying to build a mini-framework) could look like this:

com.example.readlater
|-- app
|   `-- server
|       `-- LocalApiServer.java
|-- config
|   `-- ServerConfig.java
`-- readinglist
    `-- http

For now, with only one path, it is even useful to keep createContext("/health", ...) right in LocalApiServer: that way you can see the mechanism itself without extra layers. When there are many paths, you can move context registration into a separate class like ApiRoutes, but right now that would be more of a complication than a gain.

8. Typical mistakes when getting acquainted with HttpServer and HttpExchange

Mistake #1: confusing “server” and “handler”.
It is very easy to start thinking that HttpServer is something like a “controller,” and try to write response logic right next to create(...). But HttpServer is a long-lived object that accepts requests and distributes them to handlers. Response logic should live in HttpHandler, because that is what receives the specific request's HttpExchange.

Mistake #2: treating createContext(...) as full routing, like in Spring.
createContext("/api/v1/reading-list/{id}", ...) will not work the way you expect after working with Spring MVC. In JDK HttpServer, a context is a path binding (more like a prefix), without templates and without automatic variable extraction. If you keep that in mind from the start, you will be less annoyed with the technology and more aware of what exactly Spring will later “charge you for.”

Mistake #3: building URLs as strings and dragging the “full URL” into server-side code.
The server does not need to know http://localhost:8080/health as a string. It needs a host and a port for InetSocketAddress, and paths are registered as "/health". As soon as you start mixing the “listening address” and the “paths,” your head fills with confusion, and your code fills with concatenations that are hard to fix later.

Mistake #4: ignoring the fact that HttpExchange is request and response in one object.
Sometimes beginners read the method/path and then look for “where the response is” somewhere else — and cannot find it. In HttpServer, everything is done through exchange: you read the input from it, and through it you also set the status, headers, and write the body. Once this clicks, the following topics (JSON, statuses, correct headers) become much calmer.

Mistake #5: trying to “make it beautiful” by building a homemade mini-Spring.
As soon as createContext appears, many people’s inner architect wakes up: they want to make a router, annotations, reflection, and “automatic binding.” In the course framework, that is actively harmful: you will lose the main value — transparency. Our goal here is not to “make it forever,” but to see the mechanics with our own hands and prepare the brain for Spring MVC without a feeling of magic.

1
Task
Java Server, level 22, lesson 1
Locked
Minimal server with context `/ping`
Minimal server with context `/ping`
1
Task
Java Server, level 22, lesson 1
Locked
Separate handler class for the `/info` path
Separate handler class for the `/info` path
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION