概要
HTTP サーバー自作連載の第3回として、POST リクエストの受信とフォームデータのパースを扱います。クエリ文字列までは URL だけを見れば十分でしたが、フォーム送信を扱う段階になると、リクエストボディの読み取り、Content-Length の解釈、URL エンコードされた値のデコードが必要になります。この記事では、学習用の最小サーバーに POST ハンドリングを足し、フォーム送信から PRG パターンで結果画面へ戻すところまでを一通り確認します。
使いどころ
POST リクエストのボディ読み取りとフォームデータの扱いを手で追いたい
GET と POST で必要になる処理の違いを整理したい
PRG パターンがなぜ必要かを、最小サーバーの流れで理解したい
コード例
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PostRequestServer {
static final int PORT = 8081;
// フォームデータを record で表現(Java 16+)
record FormData(String name, String age) {
boolean isValid() {
return name != null && !name.isEmpty();
}
}
/** URL エンコードされたフォームデータをパース */
static Map<String, String> parseFormData(String body)
throws UnsupportedEncodingException {
var params = new HashMap<String, String>();
if (body == null || body.isEmpty()) return params;
for (var pair : body.split("&")) {
var kv = pair.split("=", 2);
var key = URLDecoder.decode(kv[0], "UTF-8");
var value = kv.length > 1
? URLDecoder.decode(kv[1], "UTF-8") : "";
params.put(key, value);
}
return params;
}
/** HTTP レスポンスを送信 */
static void sendResponse(OutputStream out, int status,
String statusMsg, String contentType,
String body) throws IOException {
var writer = new PrintWriter(
new OutputStreamWriter(out, "UTF-8"), true);
var bodyBytes = body.getBytes("UTF-8");
writer.print("HTTP/1.1 " + status + " "
+ statusMsg + "\r\n");
writer.print("Content-Type: " + contentType
+ "; charset=UTF-8\r\n");
writer.print("Content-Length: "
+ bodyBytes.length + "\r\n");
writer.print("Connection: close\r\n\r\n");
writer.flush();
out.write(bodyBytes);
out.flush();
}
/** 303 リダイレクトを送信(PRG パターン) */
static void sendRedirect(OutputStream out,
String location) throws IOException {
var writer = new PrintWriter(
new OutputStreamWriter(out, "UTF-8"), true);
writer.print("HTTP/1.1 303 See Other\r\n");
writer.print("Location: " + location + "\r\n");
writer.print("Content-Length: 0\r\n");
writer.print("Connection: close\r\n\r\n");
writer.flush();
}
/** リクエストを処理する */
static void handleRequest(Socket socket)
throws IOException {
try (var in = socket.getInputStream();
var out = socket.getOutputStream()) {
var reader = new BufferedReader(
new InputStreamReader(in, "UTF-8"));
// リクエスト行の解析
var requestLine = reader.readLine();
if (requestLine == null) return;
var parts = requestLine.split(" ");
var method = parts[0];
var path = parts.length > 1 ? parts[1] : "/";
// ヘッダー読み取り
var headers = new HashMap<String, String>();
String line;
while ((line = reader.readLine()) != null
&& !line.isEmpty()) {
int idx = line.indexOf(':');
if (idx > 0) {
headers.put(
line.substring(0, idx)
.trim().toLowerCase(),
line.substring(idx + 1).trim());
}
}
// POST ボディ読み取り
var body = "";
if ("POST".equals(method)) {
var cl = headers.get("content-length");
if (cl != null) {
int len = Integer.parseInt(cl);
var buf = new char[len];
reader.read(buf, 0, len);
body = new String(buf);
}
}
// ルーティング
if ("GET".equals(method) && "/".equals(path)) {
var html = """
<html><body>
<h1>フォームデモ</h1>
<form method='POST' action='/submit'>
名前: <input name='name'><br>
年齢: <input name='age' type='number'><br>
<button>送信</button>
</form></body></html>
""";
sendResponse(out, 200, "OK",
"text/html", html);
} else if ("POST".equals(method)
&& "/submit".equals(path)) {
var paramMap = parseFormData(body);
var formData = new FormData(
paramMap.getOrDefault("name", ""),
paramMap.getOrDefault("age", ""));
System.out.println("受信: " + formData);
sendRedirect(out, "/result");
} else if ("GET".equals(method)
&& path.startsWith("/result")) {
sendResponse(out, 200, "OK", "text/html",
"<html><body><h1>送信完了</h1>"
+ "<a href='/'>戻る</a></body></html>");
} else {
sendResponse(out, 404, "Not Found",
"text/plain", "404 Not Found");
}
}
}
public static void main(String[] args)
throws IOException {
ExecutorService executor =
Executors.newFixedThreadPool(10);
System.out.println(
"HTTP サーバー起動: http://localhost:" + PORT);
try (var serverSocket = new ServerSocket(PORT)) {
while (true) {
var client = serverSocket.accept();
executor.submit(() -> {
try { handleRequest(client); }
catch (IOException e) {
System.err.println(e.getMessage());
}
finally {
try { client.close(); }
catch (IOException ignored) {}
}
});
}
}
}
}Version Coverage
テキストブロック、`var`、record により、フォーム送信の処理を読みやすく整理しやすい。
// Java 17: テキストブロック + record で構造化
record FormData(String name, String age) {
boolean isValid() {
return name != null && !name.isEmpty();
}
}
var html = """
<html><body>
<h1>フォーム</h1>
<form method='POST' action='/submit'>
名前: <input name='name'><br>
<button>送信</button>
</form></body></html>
""";Library Comparison
注意点
Content-Length の扱いが曖昧だと、ボディ読み取りで不足や待ちが発生しやすい
URLDecoder には文字コードを明示し、日本語や空白を含む入力でも挙動が崩れないようにしたい
POST の結果をそのまま返すと再送信しやすいため、PRG パターンを早めに見ておくと理解しやすい
この段階では CSRF、入力検証、監査ログなど実務向けの論点までは扱わない
Java 8 / 17 ではスレッドプールの上限がそのまま同時処理数に影響する
FAQ
そのまま使う前提ではありません。この記事は POST ボディの読み取りと PRG パターンを理解するための学習用です。
GET は URL の `?` 以降に値を載せ、POST はリクエストボディに載せます。共有しやすさや見え方の違いもあるため、用途に応じて使い分けます。
Post-Redirect-Get の略で、POST の結果を直接返さず、一度リダイレクトしてから GET で表示する形です。ブラウザの再読み込みによる再送信を避けやすくなります。
学習用サンプルで Java 21 を使えるなら Virtual Thread のほうが流れを追いやすいです。Java 17 以前では Executors.newFixedThreadPool を使う構成になります。