HTTP GET/POSTリクエストにおけるパラメータの扱いの違いとTomcatでの挙動

HTTP (Hypertext Transfer Protocol) は、Webブラウザとサーバー間でデータをやり取りするための基盤となるプロトコルです。このプロトコルには、GETやPOSTといった様々なリクエストメソッドが存在し、それぞれが特定の目的とパラメータの伝送方法を持っています。特に、GETとPOSTはWebアプリケーションで頻繁に利用されますが、パラメータの送信方法と、それをWebサーバー(例: Tomcat)がどのように解釈するかに違いがあります。

GETリクエストでのパラメータ伝送

GETリクエストの主な目的は、サーバーからリソースを取得することです。この際、必要なパラメータは通常、URLのクエリ文字列として付与されます。例えば、/search?keyword=example&page=1 のように、URLの疑問符(?)以降にキー=値の形式で連結されます。

ServletにおけるGETパラメータの取得

Java Servlet環境において、HttpServletRequest.getParameter()メソッドは、GETリクエストのクエリ文字列からパラメータを容易に取得できます。しかし、GETリクエストでリクエストボディにパラメータを含めた場合、getParameter()がそのボディ内のデータを取得しないことが一般的です。以下のServletコードは、この挙動を検証するために使用できます。

package com.example.web;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ParameterEchoServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("--- リクエスト受信開始 ---");

        // クエリ文字列の取得
        String queryStr = request.getQueryString();
        System.out.println("受信クエリ文字列: " + (queryStr != null ? queryStr : "なし"));

        // パラメータの取得を試みる
        String requestedId = request.getParameter("userId");
        String requestedPassword = request.getParameter("userPassword");
        String requestedAge = request.getParameter("userAge");
        String requestedAddress = request.getParameter("userAddress");

        System.out.println("getParameter(\"userId\"): " + (requestedId != null ? requestedId : "取得不可"));
        System.out.println("getParameter(\"userPassword\"): " + (requestedPassword != null ? requestedPassword : "取得不可"));
        System.out.println("getParameter(\"userAge\"): " + (requestedAge != null ? requestedAge : "取得不可"));
        System.out.println("getParameter(\"userAddress\"): " + (requestedAddress != null ? requestedAddress : "取得不可"));

        // クライアントへ簡単な応答を返す
        response.setContentType("text/plain; charset=UTF-8");
        response.getWriter().write("パラメータ処理済みです。サーバーログを確認してください。");
        System.out.println("--- リクエスト処理完了 ---");
    }
}

このServletに対して、例えばツールを使ってGETリクエストを送信する際、URLにuserId=testuser&userPassword=password123を含め、かつリクエストボディにuserAge=30&userAddress=Tokyoのような形式でパラメータを送信したとします。この場合、ServletのログにはuserIdとuserPasswordの値は表示されますが、リクエストボディに含めたuserAgeとuserAddressはgetParameter()では取得されないことが確認できます。

生のHTTPリクエストの観察

HTTPプロトコル自体は、技術的にはGETリクエストでもリクエストボディを持つことを禁止していません。しかし、Webサーバーやフレームワークの実装が、GETリクエストのボディを処理しないのが一般的です。以下のカスタムソケットサーバーを使って、生のHTTPリクエストを傍受することで、このプロトコルと実装の間の違いを理解できます。

package com.example.network;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class HttpRequestLogger {

    private static final int LISTEN_PORT = 8081; // 監視ポート
    private static final String BIND_ADDRESS = "127.0.0.1"; // バインドアドレス

    public static void main(String[] args) {
        try (ServerSocket serverListenSocket = new ServerSocket(LISTEN_PORT, 5, InetAddress.getByName(BIND_ADDRESS))) {
            System.out.println("HTTPリクエスト監視サーバーが " + BIND_ADDRESS + ":" + LISTEN_PORT + " で起動しました。");

            while (true) { // 継続的にリクエストを監視
                try (Socket clientConnection = serverListenSocket.accept()) {
                    System.out.println("\n--- クライアント接続受付: " + clientConnection.getInetAddress() + " ---");

                    InputStreamReader isr = new InputStreamReader(clientConnection.getInputStream(), StandardCharsets.UTF_8);
                    BufferedReader reader = new BufferedReader(isr);

                    StringBuilder receivedRequestBuilder = new StringBuilder();
                    String line;
                    int contentLength = 0;

                    // ヘッダーの読み込み
                    while ((line = reader.readLine()) != null && !line.isEmpty()) {
                        receivedRequestBuilder.append(line).append("\n");
                        if (line.toLowerCase().startsWith("content-length:")) {
                            try {
                                contentLength = Integer.parseInt(line.substring("content-length:".length()).trim());
                            } catch (NumberFormatException e) {
                                System.err.println("Content-Lengthヘッダーの解析に失敗しました: " + e.getMessage());
                            }
                        }
                    }
                    receivedRequestBuilder.append("\n"); // ヘッダーとボディの間の空行

                    // ボディの読み込み (Content-Lengthが存在する場合)
                    if (contentLength > 0) {
                        char[] bodyChars = new char[contentLength];
                        int bytesRead = reader.read(bodyChars, 0, contentLength);
                        if (bytesRead != -1) {
                            receivedRequestBuilder.append(bodyChars, 0, bytesRead);
                        }
                    }

                    System.out.println("受信したHTTPリクエスト:\n" + receivedRequestBuilder.toString());

                    // クライアントへシンプルなHTTPレスポンスを返す
                    String responseBody = "<h1>リクエストを正常に受信しました!</h1>";
                    String httpResponse = "HTTP/1.1 200 OK\r\n" +
                                          "Content-Type: text/html; charset=UTF-8\r\n" +
                                          "Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
                                          "\r\n" +
                                          responseBody;
                    OutputStream outputStream = clientConnection.getOutputStream();
                    outputStream.write(httpResponse.getBytes(StandardCharsets.UTF_8));
                    outputStream.flush();

                    System.out.println("--- リクエスト処理完了 ---");
                } catch (IOException e) {
                    System.err.println("クライアント接続処理中にエラーが発生しました: " + e.getMessage());
                }
            }
        } catch (IOException e) {
            System.err.println("サーバーの起動またはI/Oエラー: " + e.getMessage());
            System.exit(1);
        }
    }
}

このHttpRequestLoggerに対してGETリクエストを送信し、URLクエリとリクエストボディの両方にパラメータを含めると、サーバーログにはリクエストボディの内容も明示的に表示されます。これは、HTTPプロトコルレベルではGETリクエストでもボディが伝送されうることを示していますが、TomcatなどのWebサーバーがGETリクエストのボディをgetParameter()の対象としない実装になっていることを裏付けています。

POSTリクエストでのパラメータ伝送

POSTリクエストは、主にサーバーにデータを送信(作成または更新)するために使用されます。GETリクエストとは異なり、POSTリクエストではパラメータをURLのクエリ文字列として送ることも、リクエストボディに含めて送ることも、両方行うことも可能です。

ServletにおけるPOSTパラメータの取得

POSTリクエストの場合、HttpServletRequest.getParameter()メソッドは、URLのクエリ文字列とリクエストボディの両方からパラメータを適切に取得します。上記のParameterEchoServletをPOSTリクエストで利用した場合の挙動を見てみましょう。

例えば、POSTリクエストを送信する際に、URLにuserId=postuser&userPassword=securepassを含め、同時にリクエストボディにContent-Type: application/x-www-form-urlencodedとしてuserAge=25&userAddress=Osakaのような形式でパラメータを送信したとします。この場合、ServletのログにはuserId、userPassword、userAge、userAddressの全ての値がgetParameter()によって正常に取得されていることが確認できます。

生のHTTPリクエストの観察

POSTリクエストにおける生のHTTPプロトコルも、上記のHttpRequestLoggerで確認できます。POSTリクエストの場合、リクエストヘッダーにContent-LengthとContent-Type(例: application/x-www-form-urlencodedやapplication/jsonなど)が明示され、その後に指定されたContent-Length分のデータがリクエストボディとして続きます。HttpRequestLoggerの出力は、このリクエストボディの内容がサーバーに正確に伝送されていることを明確に示します。

まとめ

HTTPプロトコルにおいて、GETとPOSTはパラメータの伝送方法において重要な違いがあります。プロトコルレベルでは、GETリクエストでもリクエストボディにデータを付与することは可能ですが、Tomcatのような一般的なWebサーバーやServletコンテナの実装では、GETリクエストのHttpServletRequest.getParameter()はURLのクエリ文字列からのパラメータのみを処理し、リクエストボディ内のデータは通常無視します。

これに対し、POSTリクエストの場合、getParameter()メソッドはURLクエリ文字列と、application/x-www-form-urlencodedなどの適切なContent-Typeが指定されたリクエストボディの両方からパラメータを抽出して処理します。この挙動の違いは、Webアプリケーション設計において、データの種類(機密性、量)、目的(取得か送信か)、キャッシュの必要性などを考慮し、適切なHTTPメソッドとパラメータ伝送方法を選択するための重要な指針となります。

タグ: HTTP GET POST Tomcat Servlet

11月6日 15:37 投稿