Repository: oonisidesu/webserv
Pull request #122: 108 requestクラスにおけるリクエストパース
Author: login: yu-432
Created: 2025-05-25T07:14:30Z | Merged: 2025-05-26T04:32:47Z

## 概要
108 requestクラスにおけるリクエストパース
## 変更内容

* recvRequest関数の作成
recvを実行
問題なければRequestParserを起動する
  - fieldまでrecvできていた場合はfetchConfigを実行し、clientMaxBodySizeとContent-Length / recievedLengthを比較する
  - できていなければtrueを返し次の処理に進む

以前までrequestParserでContent-length / recievedLengthの比較をしていたためその処理の部分を削除した
この変更により、以前動作していたテストケースで1つ失敗が発生するが、今後解消される

* chunkedの場合のprocessBodyが不完全だったため修正を加えた
* src/http/request/request.hppがうまく取り込めず？(正しくブランチを切ったつもりだった)ヘッダファイルを変更してしまうとコンフリクトが起きてしまうため現状関数分割ができていません。この後CGIのissueを進める際に関数分割する予定です。

## 関連Issue

* #115 

## 影響範囲

* src/http/request

## テスト

* test/http/request_parser 1つのテストが失敗しますが今後解消される予定です。

## その他

* このプロジェクトは特別な制約によりC++98に基づいて作成されています。C++98はモダンなC++の機能（例: スマートポインタ、ラムダ式など）が欠如しているため、コードの記述やメンテナンスに制約が生じる可能性があります。詳細については、[C++98の公式ドキュメント](https://en.cppreference.com/w/cpp/00)をご参照ください。



<!-- This is an auto-generated comment: release notes by OSS CodeRabbit -->
### Summary by CodeRabbit

| 種類 | 説明 |
| --- | --- |
| Refactor | `FieldValidator::validateContentLength`から`MAX_BODY_SIZE`チェックを削除し、コードの簡素化を図りました。 |
| Refactor | `BaseParser`クラスの`getValidatePos`メソッドをconstとして再定義しました。 |
| Bug fix | `HTTPRequest::Body`構造体の`recvedLength`を`receivedLength`にリネームし、初期化を改善しました。 |
| New Feature | `RequestParser`にチャンク転送エンコーディング処理を追加し、`hasLastChunk`関数で最後のチャンクを検出します。 |
| Test | テスト用Makefileを更新し、特定のソースファイルを明示的に含めるようにしました。 |

このプルリクエストでは、コードの可読性と保守性が向上しており、新機能の追加によってHTTPリクエストの処理がより堅牢になっています。特に、チャンク転送エンコーディングの改善は非常に効果的です。
<!-- end of auto-generated comment: release notes by OSS CodeRabbit -->


Diff:
diff --git a/Makefile b/Makefile
index 9e858fa..dac3b75 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 NAME = webserv
 
 # sources
-SRCS = $(wildcard toolbox/*.cpp) $(wildcard src/*/*.cpp)
+SRCS = $(wildcard toolbox/*.cpp) $(wildcard src/*/*.cpp) $(wildcard src/*/*/*.cpp)
 
 OBJS = $(SRCS:.cpp=.o)
 
diff --git a/cgi-bin/script.py b/cgi-bin/script.py
index b308b93..ecacc4d 100755
--- a/cgi-bin/script.py
+++ b/cgi-bin/script.py
@@ -5,22 +5,25 @@
 import datetime
 import urllib.parse
 import re
+import cgi
 
-def sendResponse(status, headers=None):
+def sendResponse(status, headers=None, body=None):
     """
     Send HTTP response with status code and optional headers
     Args:
         status: HTTP status code (e.g., 200, 404, 500)
         headers: Dictionary of HTTP headers to include in response
+        body: Optional response body content to print
     """
     print(f"Status: {status}")
     if headers:
         for key, value in headers.items():
             print(f"{key}: {value}")
     print()
+    if body:
+        print(body)
 
-
-def sanitize_filename(filename):
+def sanitizeFilename(filename):
     """
     Sanitize filename to prevent security issues
     - Remove path components
@@ -28,82 +31,173 @@ def sanitize_filename(filename):
     - Ensure filename is safe for filesystem
     """
     filename = os.path.basename(filename)
-    filename = re.sub(r'[^\w\-\.]', '_', filename)
+    filename = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '_', filename)
     return filename
 
-def processFile(filename):
+def validateFilename(filename):
+    """
+    Validate filename length and content
+    - Check if filename is empty or too long
+    - Check if filename contains invalid characters
+    """
+    if not filename or len(filename) > 255:
+        return ""
+    safeFilename = sanitizeFilename(filename)
+    return safeFilename
+
+def generateUniqueFullPath(uploadDir, filename):
+    """
+    Generate a unique full path for the given filename
+    - Check if file exists and append timestamp if necessary
     """
-    Process file upload request
-    - Validate upload directory
-    - Handle duplicate filenames
-    - Write file content
-    - Return appropriate status and headers
+    fullPath = os.path.join(uploadDir, filename)
+    if os.path.exists(fullPath):
+        name, ext = os.path.splitext(filename)
+        timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
+        filename = f"{name}_{timestamp}{ext}"
+        fullPath = os.path.join(uploadDir, filename)
+    return fullPath
+
+def getUploadDirectory():
+    """
+    Get and validate upload directory
+    Returns the upload directory path or None if invalid
+    """
+    uploadDir = os.getenv("UPLOAD_DIR")
+    # 500 Internal Server Error
+    if not uploadDir:
+        return 500
+    # 403 Forbidden
+    if not os.path.exists(uploadDir) or not os.access(uploadDir, os.W_OK):
+        return 403
+    # 200 OK
+    return uploadDir
+
+def handleMultipartUpload(uploadDir):
+    """
+    Handle multipart file upload
+    - Use cgi.FieldStorage to parse the form data
+    - Check if file exists and handle duplicates
+    - Read data from uploaded files
+    - Write files to upload directory
+    - Return success response with uploaded file names
+    Args:
+        uploadDir: Directory to save the uploaded files
     """
     try:
-        safeFilename = sanitize_filename(filename)
-        uploadDir = os.getenv("UPLOAD_DIR")
+        form = cgi.FieldStorage()
+        uploaded_files = []
+
+        for field in form:
+            if hasattr(form[field], 'filename') and form[field].filename:
+                filename = form[field].filename
+
+                safeFilename = validateFilename(filename)
+                if not safeFilename:
+                    sendResponse(400, {"Content-Type": "text/plain"}, f"Invalid filename: {filename}")
+                    return
+
+                fullPath = generateUniqueFullPath(uploadDir, safeFilename)
+                with open(fullPath, 'wb') as f:
+                    f.write(form[field].file.read())
+                uploaded_files.append(safeFilename)
+
+        if uploaded_files:
+            sendResponse(201, {"Content-Type": "text/plain"}, f"Files uploaded: {', '.join(uploaded_files)}")
+        else:
+            sendResponse(400, {"Content-Type": "text/plain"}, "No files uploaded")
+    except Exception as e:
+        sendResponse(500, {"Content-Type": "text/plain"}, f"Error: {str(e)}")
 
-        if not os.path.exists(uploadDir):
-            return 404, {"Content-Type": "text/plain"}
+def handleRawUpload(uploadDir):
+    """
+    Handle raw file upload
+    - Get filename from query string
+    - Check if file exists and handle duplicates
+    - Read data from stdin using CONTENT_LENGTH
+    - Write to file
+    - Return location in response
+    Args:
+        uploadDir: Directory to save the uploaded file
+    """
+    try:
+        MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB
+        contentLength = int(os.getenv("CONTENT_LENGTH") or 0)
+
+        if contentLength > MAX_FILE_SIZE:
+            sendResponse(413, {"Content-Type": "text/plain"}, f"File too large.")
+            return
 
-        if not os.access(uploadDir, os.W_OK):
-            return 403, {"Content-Type": "text/plain"}
+        if contentLength <= 0:
+            sendResponse(400, {"Content-Type": "text/plain"}, "No content to upload")
+            return
 
-        fullPath = os.path.join(uploadDir, safeFilename)
-        if os.path.exists(fullPath):
-            safeFilename = safeFilename + "_" + datetime.datetime.now().strftime('%Y%m%d%H%M%S')
-            fullPath = os.path.join(uploadDir, safeFilename)
+        query = os.getenv("QUERY_STRING")
+        queryDict = urllib.parse.parse_qs(query or "")
+        filename = queryDict.get("filename", ["uploaded_file"])[0]
 
-        contentLength = int(os.environ.get("CONTENT_LENGTH", 0))
-        file = open(fullPath, "wb")
-        file.write(sys.stdin.buffer.read(contentLength))
-        file.close()
+        safeFilename = validateFilename(filename)
+        if not safeFilename:
+            sendResponse(400, {"Content-Type": "text/plain"}, "Invalid filename")
+            return
+
+        fullPath = generateUniqueFullPath(uploadDir, safeFilename)
+        rawData = sys.stdin.buffer.read(contentLength)
+
+        with open(fullPath, 'wb') as f:
+            f.write(rawData)
 
         location = f"/uploads/{safeFilename}"
-        return 201, {"Location": location, "Content-Type": "text/plain"}
+        sendResponse(201, {"Location": location, "Content-Type": "text/plain"}, f"File uploaded: {location}")
     except Exception as e:
-        return 500, {"Content-Type": "text/plain"}
+        sendResponse(500, {"Content-Type": "text/plain"}, f"Error: {str(e)}")
 
-def getRequest():
+def handleGetRequest():
     """
     Handle GET request
     Returns current timestamp in Japanese format
     """
-    sendResponse(200, {"Content-Type": "text/plain"})
+    body = datetime.datetime.now().strftime('%Y年%m月%d日 %H時%M分%S秒')
+    sendResponse(200, {"Content-Type": "text/plain"}, body)
 
-def postRequest():
+def handlePostRequest():
     """
     Handle POST request
-    - Parse query parameters for filename
-    - Process file upload
-    - Return appropriate response
+    - Get upload directory and validate it
+    - Check Content-Type to determine upload method
+    - Route to multipart or raw upload handler
     """
-    query = os.environ.get("QUERY_STRING")
-    queryDict = urllib.parse.parse_qs(query)
-
-    status, headers= processFile(queryDict.get("filename", ["cgiPost"])[0])
-    sendResponse(status, headers)
+    uploadDir = getUploadDirectory()
+    if uploadDir == 500:
+        return sendResponse(500, {"Content-Type": "text/plain"}, "Internal Server Error")
+    elif uploadDir == 403:
+        return sendResponse(403, {"Content-Type": "text/plain"}, "Forbidden")
+    contentType = os.getenv("CONTENT_TYPE")
+    if contentType and "multipart/form-data" in contentType.lower():
+        handleMultipartUpload(uploadDir)
+    else:
+        handleRawUpload(uploadDir)
 
-def otherRequest():
+def handleOtherRequest():
     """
     Handle unsupported HTTP methods
     Returns 501 Not Implemented
     """
-    sendResponse(501, {"Content-Type": "text/plain"})
+    sendResponse(501, {"Content-Type": "text/plain"}, "Not Implemented")
 
 def main():
     """
     Main entry point
     Routes request to appropriate handler based on HTTP method
     """
-    requestMethod = os.environ.get("REQUEST_METHOD")
+    requestMethod = os.getenv("REQUEST_METHOD")
 
     if requestMethod == "GET":
-        getRequest()
+        handleGetRequest()
     elif requestMethod == "POST":
-        postRequest()
+        handlePostRequest()
     else:
-        otherRequest()
+        handleOtherRequest()
 
 if __name__ == "__main__":
     main()
diff --git a/src/socket/client.cpp b/src/core/client.cpp
similarity index 100%
rename from src/socket/client.cpp
rename to src/core/client.cpp
diff --git a/src/socket/client.hpp b/src/core/client.hpp
similarity index 100%
rename from src/socket/client.hpp
rename to src/core/client.hpp
diff --git a/src/core/main.cpp b/src/core/main.cpp
index ecacdfc..c4a51cd 100644
--- a/src/core/main.cpp
+++ b/src/core/main.cpp
@@ -9,10 +9,10 @@
 #include <cstdio>
 #include <sstream>
 
+#include "server.hpp"
+#include "client.hpp"
 #include "../config/config_namespace.hpp"
 #include "../config/config_parser.hpp"
-#include "../socket/server.hpp"
-#include "../socket/client.hpp"
 #include "../event/epoll.hpp"
 #include "../event/tagged_epoll_event.hpp"
 #include "../../toolbox/string.hpp"
diff --git a/src/socket/server.cpp b/src/core/server.cpp
similarity index 100%
rename from src/socket/server.cpp
rename to src/core/server.cpp
diff --git a/src/socket/server.hpp b/src/core/server.hpp
similarity index 100%
rename from src/socket/server.hpp
rename to src/core/server.hpp
diff --git a/src/event/epoll.hpp b/src/event/epoll.hpp
index ed6dad9..a869dbf 100644
--- a/src/event/epoll.hpp
+++ b/src/event/epoll.hpp
@@ -6,8 +6,8 @@
 #include <exception>
 #include <map>
 
-#include "../socket/server.hpp"
-#include "../socket/client.hpp"
+#include "../core/server.hpp"
+#include "../core/client.hpp"
 #include "../../toolbox/shared.hpp"
 
 class Epoll {
diff --git a/src/http/http_namespace.cpp b/src/http/http_namespace.cpp
index 5abf5d5..99863c5 100644
--- a/src/http/http_namespace.cpp
+++ b/src/http/http_namespace.cpp
@@ -50,7 +50,6 @@ SET_COOKIE,    LOCATION,         WWW_AUTHENTICATE, LAST_MODIFIED
 };
 const std::size_t FIELD_SIZE = sizeof(FIELDS) / sizeof(FIELDS[0]);
 const std::size_t MAX_FIELDLINE_SIZE = 8192;
-const std::size_t MAX_BODY_SIZE = 1048576;
 }  // namespace fields
 
 namespace symbols {
diff --git a/src/http/http_namespace.hpp b/src/http/http_namespace.hpp
index ba95ee4..ed31e78 100644
--- a/src/http/http_namespace.hpp
+++ b/src/http/http_namespace.hpp
@@ -47,7 +47,6 @@ extern const char* LAST_MODIFIED;
 extern const char* FIELDS[];
 extern const std::size_t FIELD_SIZE;
 extern const std::size_t MAX_FIELDLINE_SIZE;
-extern const std::size_t MAX_BODY_SIZE;
 }  // namespace fields
 
 namespace symbols {
diff --git a/src/http/parsing/base_parser.hpp b/src/http/parsing/base_parser.hpp
index 521e881..ad9aa92 100644
--- a/src/http/parsing/base_parser.hpp
+++ b/src/http/parsing/base_parser.hpp
@@ -32,6 +32,7 @@ class BaseParser {
     virtual ~BaseParser() {}
 
     BaseParser::ParseStatus run(const std::string& buf);
+    ValidatePos getValidatePos() const { return _validatePos; }
 
  protected:
     BaseParser(const BaseParser& other);
@@ -42,7 +43,6 @@ class BaseParser {
     virtual ParseStatus processBody() = 0;
     std::string* getBuf() { return &_buf; }
     void setBuf(std::string buf) { _buf = buf; }
-    ValidatePos getValidatePos() { return _validatePos; }
     void setValidatePos(ValidatePos state) { _validatePos = state; }
     std::size_t findNewLinePos(std::string& buffer);
     std::size_t getLineEndLen(std::string& line, std::size_t lineEndPos);
diff --git a/src/http/parsing/field_validator.cpp b/src/http/parsing/field_validator.cpp
index 72467c6..f6d0ee1 100644
--- a/src/http/parsing/field_validator.cpp
+++ b/src/http/parsing/field_validator.cpp
@@ -83,12 +83,6 @@ HttpStatus::EHttpStatus FieldValidator::validateContentLength(
             return HttpStatus::BAD_REQUEST;
         }
     }
-    std::size_t size = std::strtol(contentLength->second[0].c_str(), NULL, 10);
-    if (size > fields::MAX_BODY_SIZE) {
-        toolbox::logger::StepMark::info
-            ("FieldValidator: content-length too large");
-        return HttpStatus::PAYLOAD_TOO_LARGE;
-    }
     return HttpStatus::OK;
 }
 
diff --git a/src/http/request/http_request.hpp b/src/http/request/http_request.hpp
index acfd213..ee18896 100644
--- a/src/http/request/http_request.hpp
+++ b/src/http/request/http_request.hpp
@@ -3,6 +3,7 @@
 #include <map>
 #include <vector>
 #include <string>
+#include <limits>
 
 #include "http_fields.hpp"
 #include "../http_status.hpp"
@@ -14,14 +15,14 @@ class HTTPRequest {
         Body() :
             isChunked(false),
             lastChunk(false),
-            contentLength(0),
-            recvedLength(0) {
+            contentLength(std::numeric_limits<std::size_t>::max()),
+            receivedLength(0) {
             }
         bool isChunked;
         bool lastChunk;
         std::string content;
         std::size_t contentLength;
-        std::size_t recvedLength;
+        std::size_t receivedLength;
     };
     struct URI {
         std::string fullUri;
@@ -35,6 +36,7 @@ class HTTPRequest {
     ~HTTPRequest() {}
 
     HttpStatus httpStatus;
+    std::string originalRequestLine;
     std::string method;
     URI uri;
     std::string version;
diff --git a/src/http/request/recv_request.cpp b/src/http/request/recv_request.cpp
new file mode 100644
index 0000000..0902f30
--- /dev/null
+++ b/src/http/request/recv_request.cpp
@@ -0,0 +1,73 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
+        }
+    } else if (bytesReceived == -1) {
+        toolbox::logger::StepMark::error("Request: recvRequest: recv failed "
+            "in recv from " + toolbox::to_string(_client->getFd()));
+        _response.setStatus(HttpStatus::INTERNAL_SERVER_ERROR);
+        return false;
+    }
+
+    _parsedRequest.run(std::string(buffer, bytesReceived));
+
+    std::size_t clientMaxBodySize = 0;
+
+    BaseParser::ValidatePos validatePos = _parsedRequest.getValidatePos();
+    bool hasContentLength = _parsedRequest.get().body.contentLength != 
+                           std::numeric_limits<std::size_t>::max();
+
+    if ((validatePos == BaseParser::V_BODY ||
+        validatePos == BaseParser::V_COMPLETED) && hasContentLength) {
+        if (_config.getPath().empty()) {  // If path is empty, fetch the config
+            fetchConfig();
+        }
+
+        clientMaxBodySize = _config.getClientMaxBodySize();
+
+        std::size_t contentLength = _parsedRequest.get().body.contentLength;
+        if (contentLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: content length exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+
+        std::size_t receivedLength = _parsedRequest.get().body.receivedLength;
+        if (receivedLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: receivedLength exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+    }
+    return true;
+}
+
+}  // namespace http
diff --git a/src/http/request/request.hpp b/src/http/request/request.hpp
deleted file mode 100644
index 64410ef..0000000
--- a/src/http/request/request.hpp
+++ /dev/null
@@ -1,65 +0,0 @@
-#pragma once
-
-#include "request_parser.hpp"
-#include "../response/response.hpp"
-#include "../../config/config.hpp"
-#include "../../core/client.hpp"
-#include "../../../toolbox/shared.hpp"
-
-namespace http {
-
-class Request {
- public:
-    /**
-     * @brief Constructs a Request object with the given client.
-     * @param client A shared pointer to the Client object
-     * associated with this request.
-     */
-    Request(toolbox::SharedPtr<Client> client, std::size_t requestDepth = 0);
-
-    /**
-     * @brief Destructor for the Request object.
-     */
-    ~Request();
-
-    /**
-     * @brief recieve and parses the raw HTTP request string.
-     * @note fetchConfig() is called inside this method to retrieve
-     * the configuration for the request.
-     * @return True if the request was fully received and parsed,
-     * false otherwise.
-     */
-    bool recvRequest();
-
-    void setLocalRedirectInfo(const std::string& method, 
-        const std::string& path, const std::string& host)
-
-    /**
-    * @brief Fetches the configuration for the current request.
-    */
-    void fetchConfig();
-
-    /**
-     * @brief Handles the HTTP request and prepares a response.
-     */
-    void handleRequest();
-
-    /**
-     * @brief Sends the prepared HTTP response to the client.
-     */
-    void sendResponse() const;
-
- private:
-    http::RequestParser _parsedRequest;
-    config::LocationConfig _config;
-    http::Response _response;
-    toolbox::SharedPtr<Client> _client;
-    std::size_t _requestDepth;
-
-    Request();
-    Request(const Request& other);
-    Request& operator=(const Request& other);
-
-};
-
-}
diff --git a/src/http/request/request_impl_config.cpp b/src/http/request/request_impl_config.cpp
deleted file mode 100644
index 3dcc3d7..0000000
--- a/src/http/request/request_impl_config.cpp
+++ /dev/null
@@ -1 +0,0 @@
-#include "request.hpp"
\ No newline at end of file
diff --git a/src/http/request/request_impl_exec.cpp b/src/http/request/request_impl_exec.cpp
deleted file mode 100644
index 3dcc3d7..0000000
--- a/src/http/request/request_impl_exec.cpp
+++ /dev/null
@@ -1 +0,0 @@
-#include "request.hpp"
\ No newline at end of file
diff --git a/src/http/request/request_impl_parse.cpp b/src/http/request/request_impl_parse.cpp
deleted file mode 100644
index 3dcc3d7..0000000
--- a/src/http/request/request_impl_parse.cpp
+++ /dev/null
@@ -1 +0,0 @@
-#include "request.hpp"
\ No newline at end of file
diff --git a/src/http/request/request_impl_response.cpp b/src/http/request/request_impl_response.cpp
deleted file mode 100644
index 3dcc3d7..0000000
--- a/src/http/request/request_impl_response.cpp
+++ /dev/null
@@ -1 +0,0 @@
-#include "request.hpp"
\ No newline at end of file
diff --git a/src/http/request/request_parser.cpp b/src/http/request/request_parser.cpp
index 2124a10..eeed402 100644
--- a/src/http/request/request_parser.cpp
+++ b/src/http/request/request_parser.cpp
@@ -3,20 +3,53 @@
 #include <deque>
 #include <numeric>
 #include <cstdlib>
-#include <iostream>
+#include <limits>
 
 #include "request_parser.hpp"
 
 namespace http {
-static std::size_t addLength(std::size_t sum, const std::string& s) {
+namespace {
+std::size_t addLength(std::size_t sum, const std::string& s) {
     return sum + s.size();
 }
 
-static std::size_t calcTotalLength(const std::deque<std::string>& deq) {
+std::size_t calcTotalLength(const std::deque<std::string>& deq) {
     return std::accumulate(deq.begin(), deq.end(),
         static_cast<std::size_t>(0), addLength);
 }
 
+std::string removeConsecutiveSpaces(const std::string& str) {
+    std::stringstream ss(str);
+
+    std::string word;
+    std::string result;
+    while (ss >> word) {
+        if (!result.empty()) {
+            result += " ";
+        }
+        result += word;
+    }
+    return result;
+}
+
+bool hasLastChunk(const std::string* buf) {
+    std::stringstream ss(*buf);
+    std::string line;
+    bool prevLineSizeZero = false;
+    while (std::getline(ss, line)) {
+        if (line == "0") {
+            prevLineSizeZero = true;
+        } else if (prevLineSizeZero && line.empty()) {
+            return true;  // Found the last chunk
+        } else {
+            prevLineSizeZero = false;  // Reset if we find a non-empty line
+        }
+    }
+    return false;
+}
+
+}  // namespace
+
 BaseParser::ParseStatus RequestParser::processFieldLine() {
     while (true) {
         std::size_t lineEndPos = getBuf()->find(symbols::CRLF);
@@ -25,6 +58,8 @@ BaseParser::ParseStatus RequestParser::processFieldLine() {
         }
         if (lineEndPos == 0) {
             if (!FieldValidator::validateRequestHeaders(_request.fields, _request.httpStatus)) {
+                toolbox::logger::StepMark::error(
+                    "RequestParser: invalid request headers");
                 throw ParseException("");
             }
             setValidatePos(V_BODY);
@@ -34,12 +69,15 @@ BaseParser::ParseStatus RequestParser::processFieldLine() {
 
         std::string line = toolbox::trim(getBuf(), symbols::CRLF);
         if (!FieldValidator::validateFieldLine(line)) {
+            toolbox::logger::StepMark::error(
+                "RequestParser: invalid character in field line");
             _request.httpStatus.set(HttpStatus::BAD_REQUEST);
             continue;
         }
         HTTPFields::FieldPair pair = RequestFieldParser::splitFieldLine(&line);
         if (!_fieldParser.parseFieldLine(pair, _request.fields.get(),
                                         _request.httpStatus)) {
+            toolbox::logger::StepMark::error("RequestParser: invalid field line");
             throw ParseException("");
         }
     }
@@ -55,6 +93,8 @@ BaseParser::ParseStatus RequestParser::processRequestLine() {
     validateMethod();
     processURI();
     if (_request.httpStatus.get() != HttpStatus::OK) {
+        toolbox::logger::StepMark::error(
+            "RequestParser: invalid request line");
         throw ParseException("");
     }
     setValidatePos(V_FIELD);
@@ -64,34 +104,40 @@ BaseParser::ParseStatus RequestParser::processRequestLine() {
 void RequestParser::parseRequestLine() {
     std::string line = toolbox::trim(getBuf(), symbols::CRLF);
     if (line.find(symbols::SP) == std::string::npos) {
+        toolbox::logger::StepMark::error(
+            "RequestParser: request line has no space");
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
         return;
     }
-    _request.method = toolbox::trim(&line, symbols::SP);
-    utils::skipSpace(&line);
-    _request.uri.fullUri = toolbox::trim(&line, symbols::SP);
-    utils::skipSpace(&line);
-    _request.version = toolbox::trim(&line, symbols::SP);
-    if (!line.empty()) {
+    _request.originalRequestLine = removeConsecutiveSpaces(line);
+    std::stringstream ss(_request.originalRequestLine);
+
+    std::getline(ss, _request.method, *symbols::SP);
+    std::getline(ss, _request.uri.fullUri, *symbols::SP);
+    std::getline(ss, _request.version);
+
+    if (_request.version.find(symbols::SP) != std::string::npos) {
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
+        toolbox::logger::StepMark::error(
+            "RequestParser: invalid request line");
     }
 }
 
 void RequestParser::validateVersion() {
     if (_request.version.empty()) {
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
-        toolbox::logger::StepMark::info(
+        toolbox::logger::StepMark::error(
             "RequestParser: version not found");
         return;
     }
     if (utils::hasCtlChar(_request.version)) {
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
-        toolbox::logger::StepMark::info(
+        toolbox::logger::StepMark::error(
             "RequestParser: version has control character");
         return;
     }
     if (!isValidFormat()) {
-        toolbox::logger::StepMark::info(
+        toolbox::logger::StepMark::error(
             "RequestParser: invalid version format");
         return;
     }
@@ -127,10 +173,14 @@ bool RequestParser::isValidFormat() {
 void RequestParser::validateMethod() {
     if (_request.method.empty()) {
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
+        toolbox::logger::StepMark::error(
+            "RequestParser: method not found");
         return;
     }
     if (!utils::isUpperStr(_request.method)) {
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
+        toolbox::logger::StepMark::error(
+            "RequestParser: method is not uppercase");
         return;
     }
 }
@@ -163,13 +213,13 @@ void RequestParser::parseURI() {
 void RequestParser::validatePath() {
     if (_request.uri.fullUri.empty()) {
         _request.httpStatus.set(HttpStatus::BAD_REQUEST);
-        toolbox::logger::StepMark::info(
+        toolbox::logger::StepMark::error(
             "RequestParser: uri not found");
         return;
     }
     if (_request.uri.path.size() > http::uri::MAX_URI_SIZE) {
         _request.httpStatus.set(HttpStatus::URI_TOO_LONG);
-        toolbox::logger::StepMark::info(
+        toolbox::logger::StepMark::error(
             "RequestParser: uri too large");
     }
 }
@@ -199,7 +249,7 @@ void RequestParser::percentDecode(std::string& line) {
                 if (line[0] == '/' ||
                     i + parser::HEX_DIGIT_LENGTH < line.size()) {  // is path
                     _request.httpStatus.set(HttpStatus::BAD_REQUEST);
-                    toolbox::logger::StepMark::info
+                    toolbox::logger::StepMark::error
                         ("RequestParser: path has invalid hexdecimal");
                     return;
                 }
@@ -275,6 +325,8 @@ void RequestParser::verifySafePath() {
     for (std::size_t i = 0; i < _request.uri.splitPath.size(); ++i) {
         if (_request.uri.splitPath[i] == "/..") {
             if (pathDeque.empty()) {
+                toolbox::logger::StepMark::error(
+                    "RequestParser: invalid path, try parent directory");
                 _request.httpStatus.set(HttpStatus::BAD_REQUEST);
                 return;
             } else {
@@ -293,11 +345,17 @@ void RequestParser::verifySafePath() {
 }
 
 BaseParser::ParseStatus RequestParser::processBody() {
-    if (_request.body.isChunked || isChunkedEncoding()) {
-        parseChunkedEncoding();
-        return P_COMPLETED;
+    if (isChunkedEncoding()) {
+        _request.body.content += *getBuf();
+        _request.body.receivedLength += getBuf()->size();
+        if (hasLastChunk(getBuf())) {
+            parseChunkedEncoding();
+            return P_COMPLETED;
+        }
+        getBuf()->clear();
+        return P_NEED_MORE_DATA;
     }
-    if (!_request.body.contentLength) {
+    if (_request.body.contentLength == std::numeric_limits<std::size_t>::max()) {
         std::vector<std::string>& contentLen =
             _request.fields.getFieldValue(fields::CONTENT_LENGTH);
         if (contentLen.empty()) {
@@ -306,13 +364,13 @@ BaseParser::ParseStatus RequestParser::processBody() {
             _request.body.contentLength = std::atoi(contentLen.front().c_str());
         }
     }
-    if (_request.body.contentLength > _request.body.recvedLength) {
-        std::size_t remainLen = _request.body.contentLength - _request.body.recvedLength;
+    if (_request.body.contentLength > _request.body.receivedLength) {
+        std::size_t remainLen = _request.body.contentLength - _request.body.receivedLength;
 
         _request.body.content += getBuf()->substr(0, remainLen);
-        _request.body.recvedLength += _request.body.content.size();
+        _request.body.receivedLength = _request.body.content.size();
     }
-    if (_request.body.contentLength <= _request.body.recvedLength) {
+    if (_request.body.contentLength <= _request.body.receivedLength) {
         setValidatePos(V_COMPLETED);
         return P_COMPLETED;
     }
@@ -341,6 +399,7 @@ void RequestParser::parseChunkedEncoding() {
         char* endPtr;
         chunkSize = strtol(hexStr.c_str(), &endPtr, 16);
         if (*endPtr != *http::symbols::CR) {
+            toolbox::logger::StepMark::error("RequestParser: invalid chunk size");
             throw ParseException("");
         }
         if (chunkSize == 0) {
@@ -353,12 +412,8 @@ void RequestParser::parseChunkedEncoding() {
             return;
         }
         std::string chunkData = getBuf()->substr(0, chunkSize);
-        if (_request.body.recvedLength + chunkSize > fields::MAX_BODY_SIZE) {
-            _request.httpStatus.set(HttpStatus::PAYLOAD_TOO_LARGE);
-            throw ParseException("");
-        }
         _request.body.content.append(chunkData);
-        _request.body.recvedLength += chunkSize;
+        _request.body.receivedLength += chunkSize;
         setBuf(getBuf()->substr(chunkSize + parser::HEX_DIGIT_LENGTH));
     }
 }
diff --git a/src/http/request/request_parser.hpp b/src/http/request/request_parser.hpp
index 1ce02cf..537543e 100644
--- a/src/http/request/request_parser.hpp
+++ b/src/http/request/request_parser.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <string>
+#include <sstream>
 
 #include "../parsing/base_parser.hpp"
 #include "../parsing/field_validator.hpp"
diff --git a/test/core/client/Makefile b/test/core/client/Makefile
index 2e9d25a..767fb7c 100644
--- a/test/core/client/Makefile
+++ b/test/core/client/Makefile
@@ -1,8 +1,8 @@
 NAME = client_test.out
 
 SRCS = main.cpp \
-		../../../src/socket/server.cpp \
-		../../../src/socket/client.cpp \
+		../../../src/core/server.cpp \
+		../../../src/core/client.cpp \
 		../../../src/event/epoll.cpp \
 		../../../toolbox/string.cpp 
 OBJS = $(SRCS:.cpp=.o)
diff --git a/test/core/client/main.cpp b/test/core/client/main.cpp
index f5afbfb..980b17e 100644
--- a/test/core/client/main.cpp
+++ b/test/core/client/main.cpp
@@ -11,8 +11,8 @@
 #include <cstdio>
 #include <sstream>
 
-#include "../../../src/socket/server.hpp"
-#include "../../../src/socket/client.hpp"
+#include "../../../src/core/server.hpp"
+#include "../../../src/core/client.hpp"
 #include "../../../src/event/epoll.hpp"
 #include "../../../src/event/tagged_epoll_event.hpp"
 #include "../../../toolbox/string.hpp"
diff --git a/test/http/cgi/cgi_get_test.py b/test/http/cgi/cgi_get_test.py
index 6f2d47c..806eb53 100755
--- a/test/http/cgi/cgi_get_test.py
+++ b/test/http/cgi/cgi_get_test.py
@@ -22,12 +22,13 @@ def testGetRequest(self):
             stderr=subprocess.PIPE
         )
 
-        stdout, stderr = process.communicate()
+        stdout, _ = process.communicate()
         response = stdout.decode('utf-8')
         headers, body = response.split('\n\n', 1)
 
         self.assertIn('Status: 200', headers)
         self.assertIn('Content-Type: text/plain', headers)
+        self.assertRegex(body, r'\d{4}年\d{1,2}月\d{1,2}日 \d{1,2}時\d{1,2}分\d{1,2}秒')
 
 if __name__ == '__main__':
     unittest.main()
\ No newline at end of file
diff --git a/test/http/cgi/cgi_method_test.py b/test/http/cgi/cgi_method_test.py
new file mode 100755
index 0000000..dc34877
--- /dev/null
+++ b/test/http/cgi/cgi_method_test.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+
+import os
+import unittest
+import subprocess
+import datetime
+import random
+import string
+import tempfile
+
+def generateRandomString(length):
+    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
+
+class TestCgiAdditional(unittest.TestCase):
+    def setUp(self):
+        self.uploadDir = "./uploads/"
+        self.cgiPath = "../../../cgi-bin/script.py"
+        self.randomString = generateRandomString(30)
+        self.randomStringLength = len(self.randomString)
+        os.makedirs(self.uploadDir, exist_ok=True)
+
+    def tearDown(self):
+        for file in os.listdir(self.uploadDir):
+            try:
+                os.remove(os.path.join(self.uploadDir, file))
+            except:
+                pass
+        try:
+            os.rmdir(self.uploadDir)
+        except:
+            pass
+
+    def testFileSizeLimit(self):
+        """ファイルサイズ制限テスト（10MB超過）"""
+        # 10MB + 1バイトのデータを作成
+        large_data = b'x' * (10 * 1024 * 1024 + 1)
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "filename=large_file.txt",
+            "CONTENT_LENGTH": str(len(large_data)),
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(large_data)
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 413', headers)
+        self.assertIn('File too large', body)
+
+    def testEmptyContent(self):
+        """空のコンテンツアップロードテスト"""
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "filename=empty.txt",
+            "CONTENT_LENGTH": "0",
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(b'')
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 400', headers)
+        self.assertIn('No content to upload', body)
+
+    def testLongFilename(self):
+        """長すぎるファイル名テスト（255文字超過）"""
+        long_filename = 'a' * 256 + '.txt'
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": f"filename={long_filename}",
+            "CONTENT_LENGTH": str(self.randomStringLength),
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(self.randomString.encode('utf-8'))
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 400', headers)
+        self.assertIn('Invalid filename', body)
+
+    def testEmptyFilename(self):
+        """空のファイル名テスト"""
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "filename=",
+            "CONTENT_LENGTH": str(self.randomStringLength),
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(self.randomString.encode('utf-8'))
+        response = stdout.decode('utf-8')
+        headers, _ = response.split('\n\n', 1)
+
+        self.assertIn('Status: 201', headers)
+        self.assertIn('Location: /uploads/uploaded_file', headers)
+
+    def testMultipartFormData(self):
+        """multipart/form-dataアップロードテスト"""
+        boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
+        multipart_data = (
+            f"--{boundary}\r\n"
+            f"Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n"
+            f"Content-Type: text/plain\r\n\r\n"
+            f"{self.randomString}\r\n"
+            f"--{boundary}--\r\n"
+        ).encode('utf-8')
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "",
+            "CONTENT_LENGTH": str(len(multipart_data)),
+            "CONTENT_TYPE": f"multipart/form-data; boundary={boundary}",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(multipart_data)
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 201', headers)
+        self.assertIn('Files uploaded', body)
+        self.assertIn('test.txt', body)
+
+    def testMultipartMultipleFiles(self):
+        """複数ファイルのmultipart/form-dataアップロードテスト"""
+        boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
+        multipart_data = (
+            f"--{boundary}\r\n"
+            f"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n"
+            f"Content-Type: text/plain\r\n\r\n"
+            f"File 1 content\r\n"
+            f"--{boundary}\r\n"
+            f"Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"\r\n"
+            f"Content-Type: text/plain\r\n\r\n"
+            f"File 2 content\r\n"
+            f"--{boundary}--\r\n"
+        ).encode('utf-8')
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "",
+            "CONTENT_LENGTH": str(len(multipart_data)),
+            "CONTENT_TYPE": f"multipart/form-data; boundary={boundary}",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(multipart_data)
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 201', headers)
+        self.assertIn('Files uploaded', body)
+        self.assertIn('test1.txt', body)
+        self.assertIn('test2.txt', body)
+
+    def testMultipartNoFiles(self):
+        """ファイルなしのmultipart/form-dataテスト"""
+        boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
+        multipart_data = (
+            f"--{boundary}\r\n"
+            f"Content-Disposition: form-data; name=\"text_field\"\r\n\r\n"
+            f"some text value\r\n"
+            f"--{boundary}--\r\n"
+        ).encode('utf-8')
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "",
+            "CONTENT_LENGTH": str(len(multipart_data)),
+            "CONTENT_TYPE": f"multipart/form-data; boundary={boundary}",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(multipart_data)
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 400', headers)
+        self.assertIn('No files uploaded', body)
+
+    def testUnsupportedMethod(self):
+        """サポートされていないHTTPメソッドテスト"""
+        env = {
+            "REQUEST_METHOD": "PUT",
+            "QUERY_STRING": "",
+            "CONTENT_LENGTH": "0",
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(b'')
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 501', headers)
+        self.assertIn('Not Implemented', body)
+
+    def testJapaneseFilename(self):
+        """日本語ファイル名テスト"""
+        japanese_filename = "テスト.txt"
+        encoded_filename = japanese_filename
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": f"filename={encoded_filename}",
+            "CONTENT_LENGTH": str(self.randomStringLength),
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(self.randomString.encode('utf-8'))
+        response = stdout.decode('utf-8')
+        headers, _ = response.split('\n\n', 1)
+
+        self.assertIn('Status: 201', headers)
+        self.assertIn('Location: /uploads/テスト.txt', headers)
+
+    def testBinaryFileUpload(self):
+        """バイナリファイルアップロードテスト"""
+        binary_data = bytes(range(256))  # 0-255のバイト列
+
+        env = {
+            "REQUEST_METHOD": "POST",
+            "QUERY_STRING": "filename=binary.bin",
+            "CONTENT_LENGTH": str(len(binary_data)),
+            "CONTENT_TYPE": "application/octet-stream",
+            "UPLOAD_DIR": self.uploadDir,
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate(binary_data)
+        response = stdout.decode('utf-8')
+        headers, _ = response.split('\n\n', 1)
+
+        self.assertIn('Status: 201', headers)
+        self.assertIn('Location: /uploads/binary.bin', headers)
+
+        uploaded_file = os.path.join(self.uploadDir, "binary.bin")
+        self.assertTrue(os.path.exists(uploaded_file))
+        with open(uploaded_file, 'rb') as f:
+            self.assertEqual(f.read(), binary_data)
+
+    def testGetRequestTimestamp(self):
+        """GETリクエストのタイムスタンプ確認テスト"""
+        env = {
+            "REQUEST_METHOD": "GET",
+            "QUERY_STRING": "",
+            "CONTENT_LENGTH": "0",
+            "CONTENT_TYPE": "application/octet-stream",
+        }
+
+        process = subprocess.Popen(
+            [self.cgiPath],
+            env=env,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE
+        )
+
+        stdout, _ = process.communicate()
+        response = stdout.decode('utf-8')
+        headers, body = response.split('\n\n', 1)
+
+        self.assertIn('Status: 200', headers)
+        self.assertIn('Content-Type: text/plain', headers)
+        # 日本語タイムスタンプ形式の確認
+        self.assertRegex(body, r'\d{4}年\d{1,2}月\d{1,2}日 \d{1,2}時\d{1,2}分\d{1,2}秒')
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/test/http/cgi/cgi_post_test.py b/test/http/cgi/cgi_post_test.py
index f688150..e392ba5 100755
--- a/test/http/cgi/cgi_post_test.py
+++ b/test/http/cgi/cgi_post_test.py
@@ -130,7 +130,7 @@ def testSpecialCharsInFilename(self):
         headers, _ = response.split('\n\n', 1)
 
         self.assertIn('Status: 201', headers)
-        self.assertIn('Location: /uploads/test____.txt', headers)
+        self.assertIn('Location: /uploads/test@#$%.txt', headers)
 
     def testSpecialCharsInBody(self):
         special_chars = "!@#$%^&*()_+{}|:<>?~`-=[]\\;',./"
diff --git a/test/http/request/test_main.cpp b/test/http/request/test_main.cpp
index c4afb23..2ac3a90 100644
--- a/test/http/request/test_main.cpp
+++ b/test/http/request/test_main.cpp
@@ -2,16 +2,8 @@
 
 #include "../../../toolbox/stepmark.hpp"
 
-void requestLineTest();
-void fieldTest();
-void testRequestParser();
-
 int main(void) {
     toolbox::logger::StepMark::setLogFile("request_test.log");
     toolbox::logger::StepMark::setLevel(toolbox::logger::DEBUG);
-    requestLineTest();
-    fieldTest();
-    testRequestParser();
-
     return 0;
 }
diff --git a/test/http/request_parse/Makefile b/test/http/request_parse/Makefile
new file mode 100644
index 0000000..53435c8
--- /dev/null
+++ b/test/http/request_parse/Makefile
@@ -0,0 +1,38 @@
+NAME = test.out
+
+SRC_DIRS = ../../../src
+TOOLBOX_DIR = ../../../toolbox
+
+TEST_SRCS = $(wildcard $(SRC_DIRS)/http/*.cpp)\
+			../../../src/http/request/http_fields.cpp\
+			../../../src/http/request/request_field_parser.cpp\
+			../../../src/http/request/request_parser.cpp\
+			$(wildcard $(SRC_DIRS)/http/parsing/*.cpp)\
+			$(wildcard $(TOOLBOX_DIR)/*.cpp)\
+			$(wildcard *.cpp) \
+            $(wildcard http/*.cpp) \
+            $(wildcard http/request/*.cpp) \
+
+CXX = c++
+CXXFLAGS = -Wall -Wextra -Werror -std=c++98 -I..
+
+TEST_OBJS = $(TEST_SRCS:.cpp=.o)
+
+all: $(NAME)
+
+$(NAME): $(TEST_OBJS)
+	$(CXX) $(CXXFLAGS) -o $(NAME) $(TEST_OBJS)
+
+clean:
+	$(RM) $(TEST_OBJS)
+
+fclean: clean
+	$(RM) $(NAME)
+	$(RM) request_test.log
+
+re: fclean all
+
+run: all
+	./$(NAME)
+
+.PHONY: all clean fclean re run
\ No newline at end of file
diff --git a/test/http/request/base_test.hpp b/test/http/request_parse/base_test.hpp
similarity index 100%
rename from test/http/request/base_test.hpp
rename to test/http/request_parse/base_test.hpp
diff --git a/test/http/request/field_test.hpp b/test/http/request_parse/field_test.hpp
similarity index 100%
rename from test/http/request/field_test.hpp
rename to test/http/request_parse/field_test.hpp
diff --git a/test/http/request/field_testcase.cpp b/test/http/request_parse/field_testcase.cpp
similarity index 100%
rename from test/http/request/field_testcase.cpp
rename to test/http/request_parse/field_testcase.cpp
diff --git a/test/http/request/request_buf_size_test.cpp b/test/http/request_parse/request_buf_size_test.cpp
similarity index 100%
rename from test/http/request/request_buf_size_test.cpp
rename to test/http/request_parse/request_buf_size_test.cpp
diff --git a/test/http/request/requestline_test.hpp b/test/http/request_parse/requestline_test.hpp
similarity index 100%
rename from test/http/request/requestline_test.hpp
rename to test/http/request_parse/requestline_test.hpp
diff --git a/test/http/request/requestline_testcase.cpp b/test/http/request_parse/requestline_testcase.cpp
similarity index 99%
rename from test/http/request/requestline_testcase.cpp
rename to test/http/request_parse/requestline_testcase.cpp
index 1c9d0c3..5ad5a94 100644
--- a/test/http/request/requestline_testcase.cpp
+++ b/test/http/request_parse/requestline_testcase.cpp
@@ -146,9 +146,9 @@ void makeMethodTests(TestVector& t) {
     r._request = " / HTTP/1.1\r\nHost: sample\r\n\r\n";
     r._httpStatus.set(http::HttpStatus::BAD_REQUEST);  // Bad Request
     r._isSuccessTest = false;
-    r._exceptRequest.method = "";
-    r._exceptRequest.path = "/";
-    r._exceptRequest.version = "HTTP/1.1";
+    r._exceptRequest.method = "/";
+    r._exceptRequest.path = "HTTP/1.1";
+    r._exceptRequest.version = "";
     t.push_back(r);
     clearUri(r);
 }
@@ -590,7 +590,7 @@ void makeRequestStructureTests(TestVector& t) {
     r._isSuccessTest = false;
     r._exceptRequest.method = "GET";
     r._exceptRequest.path = "/";
-    r._exceptRequest.version = "HTTP/1.1";
+    r._exceptRequest.version = "HTTP/1.1 extra";
     t.push_back(r);
     clearUri(r);
 }
diff --git a/test/http/request_parse/test_main.cpp b/test/http/request_parse/test_main.cpp
new file mode 100644
index 0000000..c4afb23
--- /dev/null
+++ b/test/http/request_parse/test_main.cpp
@@ -0,0 +1,17 @@
+// Copyright 2025 Ideal Broccoli
+
+#include "../../../toolbox/stepmark.hpp"
+
+void requestLineTest();
+void fieldTest();
+void testRequestParser();
+
+int main(void) {
+    toolbox::logger::StepMark::setLogFile("request_test.log");
+    toolbox::logger::StepMark::setLevel(toolbox::logger::DEBUG);
+    requestLineTest();
+    fieldTest();
+    testRequestParser();
+
+    return 0;
+}


Reviews:
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

<details>
<summary>Commits</summary>
Files that changed from the base of the PR and between 7898c8e0abd874114249068c24e54a20b3a130d7 and d92f8a95e05131e8cbe116ecf10841a6aa103e0c commits.
</details>

<details>
<summary>Files selected (8)</summary>

* src/http/http_namespace.cpp (1)
* src/http/http_namespace.hpp (1)
* src/http/parsing/base_parser.hpp (2)
* src/http/parsing/field_validator.cpp (1)
* src/http/request/http_request.hpp (2)
* src/http/request/recv_request.cpp (1)
* src/http/request/request_parser.cpp (5)
* test/http/request_parse/Makefile (1)
</details>








<details>
<summary>Review comments generated (12)</summary>

* Review: 4
* LGTM: 8

</details>

---

<details>
<summary>Tips</summary>

### Chat with <img src="https://avatars.githubusercontent.com/in/347564?s=41&u=fad245b8b4c7254fe63dd4dcd4d662ace122757e&v=4" alt="Image description" width="20" height="20">  CodeRabbit Bot (`@coderabbitai`)
- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
- Invite the bot into a review comment chain by tagging `@coderabbitai` in a reply.

### Code suggestions
- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. 
- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

### Pausing incremental reviews
- Add `@coderabbitai: ignore` anywhere in the PR description to pause further reviews from the bot.

</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T07:15:42Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors how incoming HTTP requests are received and parsed, centralizing recv logic in `recvRequest` and updating chunked‐body handling in the parser.

- Added `recvRequest()` to read from socket, handle disconnection, and enforce `clientMaxBodySize` before delegating to `RequestParser`.
- Refactored `RequestParser::processBody()` and `parseChunkedEncoding()` to use `receivedLength`, detect final chunks, and defer size checks.
- Updated test Makefile to explicitly include parser sources and removed legacy `MAX_BODY_SIZE` definitions and related checks.

### Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

<details>
<summary>Show a summary per file</summary>

| File                                   | Description                                                  |
|----------------------------------------|--------------------------------------------------------------|
| test/http/request_parse/Makefile      | Switched from wildcard to explicit parser source listings    |
| src/http/request/request_parser.cpp    | Renamed `recvedLength`→`receivedLength`, added `hasLastChunk`, and restructured body parsing |
| src/http/request/recv_request.cpp      | Introduced `recvRequest()` for socket reads and size checks  |
| src/http/request/http_request.hpp      | Initialized `contentLength` to `max` and renamed `receivedLength` |
| src/http/parsing/field_validator.cpp   | Removed old `MAX_BODY_SIZE` check in `validateContentLength` |
| src/http/parsing/base_parser.hpp       | Exposed `getValidatePos()` as public                        |
| src/http/http_namespace.{hpp,cpp}      | Removed `MAX_BODY_SIZE` constant                             |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/http/request/request_parser.cpp:414**
* The size check against `MAX_BODY_SIZE` was removed, allowing unbounded chunked bodies. Reintroduce a limit to prevent excessive memory use or DoS.
```
// if (_request.body.recvedLength + chunkSize > fields::MAX_BODY_SIZE) {
```
**test/http/request_parse/Makefile:6**
* [nitpick] Switching to an explicit file list will require constant updates as sources change. Consider using the wildcard and excluding generated or unwanted files instead.
```
-			$(wildcard $(SRC_DIRS)/http/request/*.cpp)\
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T07:16:02Z
user: login: yu-432
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-25T07:19:22Z
user: login: yu-432
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-25T07:20:16Z
user: login: yu-432
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-25T07:23:48Z
user: login: yu-432
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-25T07:25:28Z
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

<details>
<summary>Commits</summary>
Files that changed from the base of the PR and between d92f8a95e05131e8cbe116ecf10841a6aa103e0c and 7a638287564acd9de144d3038c3dbb1c119d8311 commits.
</details>

<details>
<summary>Files selected (1)</summary>

* src/http/request/request_parser.cpp (5)
</details>








<details>
<summary>Review comments generated (0)</summary>

* Review: 0
* LGTM: 0

</details>

---

<details>
<summary>Tips</summary>

### Chat with <img src="https://avatars.githubusercontent.com/in/347564?s=41&u=fad245b8b4c7254fe63dd4dcd4d662ace122757e&v=4" alt="Image description" width="20" height="20">  CodeRabbit Bot (`@coderabbitai`)
- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
- Invite the bot into a review comment chain by tagging `@coderabbitai` in a reply.

### Code suggestions
- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. 
- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

### Pausing incremental reviews
- Add `@coderabbitai: ignore` anywhere in the PR description to pause further reviews from the bot.

</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T07:30:19Z
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

<details>
<summary>Commits</summary>
Files that changed from the base of the PR and between 7a638287564acd9de144d3038c3dbb1c119d8311 and 64ee765b0a47bb35b3909c69670d67072706ebe0 commits.
</details>

<details>
<summary>Files selected (1)</summary>

* src/http/request/recv_request.cpp (1)
</details>








<details>
<summary>Review comments generated (1)</summary>

* Review: 1
* LGTM: 0

</details>

---

<details>
<summary>Tips</summary>

### Chat with <img src="https://avatars.githubusercontent.com/in/347564?s=41&u=fad245b8b4c7254fe63dd4dcd4d662ace122757e&v=4" alt="Image description" width="20" height="20">  CodeRabbit Bot (`@coderabbitai`)
- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
- Invite the bot into a review comment chain by tagging `@coderabbitai` in a reply.

### Code suggestions
- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned. 
- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

### Pausing incremental reviews
- Add `@coderabbitai: ignore` anywhere in the PR description to pause further reviews from the bot.

</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T07:35:32Z
user: login: kurrrru
body: ありがとうございます
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-25T10:31:22Z
user: login: oonisidesu
body: ありがとうございます
校舎で動作確認して
1件のみテスト失敗で他成功で想定通り問題ありませんでした
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-26T04:32:41Z

Review comments:
diff_hunk: @@ -83,12 +83,6 @@ HttpStatus::EHttpStatus FieldValidator::validateContentLength(
             return HttpStatus::BAD_REQUEST;
         }
     }
-    std::size_t size = std::strtol(contentLength->second[0].c_str(), NULL, 10);
-    if (size > fields::MAX_BODY_SIZE) {
-        toolbox::logger::StepMark::info
-            ("FieldValidator: content-length too large");
-        return HttpStatus::PAYLOAD_TOO_LARGE;
-    }
     return HttpStatus::OK;
 }
path: src/http/parsing/field_validator.cpp
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

`FieldValidator::validateContentLength`関数から`content-length`が`MAX_BODY_SIZE`を超えるかどうかのチェックが削除されています。この変更により、リクエストボディのサイズ制限が適切に処理されない可能性があります。もしこのチェックが他の場所で行われていない場合、セキュリティ上の問題やサーバーの負荷増大につながる可能性がありますので、確認してください。
```diff
+    std::size_t size = std::strtol(contentLength->second[0].c_str(), NULL, 10);
+    if (size > fields::MAX_BODY_SIZE) {
+        toolbox::logger::StepMark::info
+            ("FieldValidator: content-length too large");
+        return HttpStatus::PAYLOAD_TOO_LARGE;
+    }
```


<!-- This is an auto-generated comment by OSS CodeRabbit -->
created_at: 2025-05-25T07:15:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
+        }
+    } else if (bytesReceived == -1) {
+        toolbox::logger::StepMark::error("Request: recvRequest: recv failed "
+            "in recv from " + toolbox::to_string(_client->getFd()));
+        _response.setStatus(HttpStatus::INTERNAL_SERVER_ERROR);
+        return false;
+    }
+
+    buffer[bytesReceived] = '\0';
+
+    _parsedRequest.run(static_cast<std::string>(buffer));
+
+    std::size_t clientMaxBodySize = 0;
+
+    BaseParser::ValidatePos validatePos = _parsedRequest.getValidatePos();
+    bool hasContentLength = _parsedRequest.get().body.contentLength != 
+                           std::numeric_limits<std::size_t>::max();
+
+    if ((validatePos == BaseParser::V_BODY ||
+        validatePos == BaseParser::V_COMPLETED) && hasContentLength) {
+        if (_config.getPath().empty()) {  // If path is empty, fetch the config
+            fetchConfig();
+        }
+
+        clientMaxBodySize = _config.getClientMaxBodySize();
+
+        std::size_t contentLength = _parsedRequest.get().body.contentLength;
+        if (contentLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: content length exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+
+        std::size_t receivedLength = _parsedRequest.get().body.receivedLength;
+        if (receivedLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: receivedLength exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+    }
+    return true;
+}
path: src/http/request/recv_request.cpp
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

`recvRequest`関数内で、`recv`の戻り値が0の場合にクライアントが切断されたと判断していますが、その際に`_parsedRequest.getValidatePos()`が`BaseParser::V_COMPLETED`であるかどうかを確認しているロジックがあります。この部分は、リクエストが完全にパースされていない場合でも`HttpStatus::OK`を返す可能性があるため、セキュリティ上の問題を引き起こす可能性があります。リクエストが不完全な場合には、常に`BAD_REQUEST`を返すべきです。

```diff


<!-- This is an auto-generated comment by OSS CodeRabbit -->
created_at: 2025-05-25T07:15:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
path: src/http/request/recv_request.cpp
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
            _response.setStatus(HttpStatus::OK);
            return true;
        } else {
            _response.setStatus(HttpStatus::BAD_REQUEST);
            return false;
        }
```

このコードを以下のように修正することをお勧めします：

```diff


<!-- This is an auto-generated comment by OSS CodeRabbit -->
created_at: 2025-05-25T07:15:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
path: src/http/request/recv_request.cpp
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

        if (_parsedRequest.getValidatePos() != BaseParser::V_COMPLETED) {
            _response.setStatus(HttpStatus::BAD_REQUEST);
            return false;
        }
        _response.setStatus(HttpStatus::OK);
        return true;
```
これにより、リクエストが完全にパースされていない場合には常に`BAD_REQUEST`を返すようになります。


<!-- This is an auto-generated comment by OSS CodeRabbit -->
created_at: 2025-05-25T07:15:41Z
author_association: NONE
diff_hunk: @@ -341,13 +364,13 @@ BaseParser::ParseStatus RequestParser::processBody() {
             _request.body.contentLength = std::atoi(contentLen.front().c_str());
         }
     }
-    if (_request.body.contentLength > _request.body.recvedLength) {
-        std::size_t remainLen = _request.body.contentLength - _request.body.recvedLength;
+    if (_request.body.contentLength > _request.body.receivedLength) {
+        std::size_t remainLen = _request.body.contentLength - _request.body.receivedLength;
 
         _request.body.content += getBuf()->substr(0, remainLen);
-        _request.body.recvedLength += _request.body.content.size();
+        _request.body.receivedLength += _request.body.content.size();
path: src/http/request/request_parser.cpp
user: login: Copilot
body: Updating `receivedLength` by `content.size()` will double-count previously received data. Use the length of the newly appended substring (`remainLen`) instead.
```suggestion
        _request.body.receivedLength += remainLen;
```
created_at: 2025-05-25T07:16:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
+        }
+    } else if (bytesReceived == -1) {
+        toolbox::logger::StepMark::error("Request: recvRequest: recv failed "
+            "in recv from " + toolbox::to_string(_client->getFd()));
+        _response.setStatus(HttpStatus::INTERNAL_SERVER_ERROR);
+        return false;
+    }
+
+    buffer[bytesReceived] = '\0';
+
+    _parsedRequest.run(static_cast<std::string>(buffer));
path: src/http/request/recv_request.cpp
user: login: Copilot
body: Constructing a `std::string` from a null-terminated buffer will truncate binary data at any `\0`. Use `std::string(buffer, bytesReceived)` to preserve the full payload.
```suggestion
    _parsedRequest.run(std::string(buffer, bytesReceived));
```
created_at: 2025-05-25T07:16:02Z
author_association: NONE
diff_hunk: @@ -31,6 +32,22 @@ std::string removeConsecutiveSpaces(const std::string& str) {
     return result;
 }
 
+bool hasLastChunk(const std::string* buf) {
path: src/http/request/request_parser.cpp
user: login: Copilot
body: `hasLastChunk` compares lines to "0" but doesn’t strip trailing '\r', so it may miss the final chunk in CRLF-delimited streams. Trim '\r' before comparison.
created_at: 2025-05-25T07:16:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
path: src/http/request/recv_request.cpp
user: login: Copilot
body: [nitpick] Relative includes can break as the directory structure evolves. Prefer project-rooted or installed include paths (e.g., `<toolbox/stepmark.hpp>`).
```suggestion
#include <toolbox/stepmark.hpp>
```
created_at: 2025-05-25T07:16:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
+        }
+    } else if (bytesReceived == -1) {
+        toolbox::logger::StepMark::error("Request: recvRequest: recv failed "
+            "in recv from " + toolbox::to_string(_client->getFd()));
+        _response.setStatus(HttpStatus::INTERNAL_SERVER_ERROR);
+        return false;
+    }
+
+    buffer[bytesReceived] = '\0';
+
+    _parsedRequest.run(static_cast<std::string>(buffer));
+
+    std::size_t clientMaxBodySize = 0;
+
+    BaseParser::ValidatePos validatePos = _parsedRequest.getValidatePos();
+    bool hasContentLength = _parsedRequest.get().body.contentLength != 
+                           std::numeric_limits<std::size_t>::max();
+
+    if ((validatePos == BaseParser::V_BODY ||
+        validatePos == BaseParser::V_COMPLETED) && hasContentLength) {
+        if (_config.getPath().empty()) {  // If path is empty, fetch the config
+            fetchConfig();
+        }
+
+        clientMaxBodySize = _config.getClientMaxBodySize();
+
+        std::size_t contentLength = _parsedRequest.get().body.contentLength;
+        if (contentLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: content length exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+
+        std::size_t receivedLength = _parsedRequest.get().body.receivedLength;
+        if (receivedLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: receivedLength exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+    }
+    return true;
+}
path: src/http/request/recv_request.cpp
user: login: yu-432
body: V_COMPLETED(validate完了) == P_COMPLETED(parse完了)のためこの条件文で問題ありません
created_at: 2025-05-25T07:19:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
path: src/http/request/recv_request.cpp
user: login: yu-432
body: 同じです

created_at: 2025-05-25T07:20:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
path: src/http/request/recv_request.cpp
user: login: yu-432
body: recvするものがない場合にはそれまでのbufを使用してparseが完了しているはずなので、この段階でV＿COMPLETEDになっていない場合は失敗と皆して良いと考えています。
動いてみないことにには確認できないため一旦現状のまま進めます。
created_at: 2025-05-25T07:23:48Z
author_association: COLLABORATOR
diff_hunk: @@ -341,13 +364,13 @@ BaseParser::ParseStatus RequestParser::processBody() {
             _request.body.contentLength = std::atoi(contentLen.front().c_str());
         }
     }
-    if (_request.body.contentLength > _request.body.recvedLength) {
-        std::size_t remainLen = _request.body.contentLength - _request.body.recvedLength;
+    if (_request.body.contentLength > _request.body.receivedLength) {
+        std::size_t remainLen = _request.body.contentLength - _request.body.receivedLength;
 
         _request.body.content += getBuf()->substr(0, remainLen);
-        _request.body.recvedLength += _request.body.content.size();
+        _request.body.receivedLength += _request.body.content.size();
path: src/http/request/request_parser.cpp
user: login: yu-432
body: 修正します
created_at: 2025-05-25T07:25:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,73 @@
+#include <string>
+#include <limits>
+
+#include "../../../toolbox/stepmark.hpp"
+#include "../../core/client.hpp"
+#include "request_parser.hpp"
+#include "request.hpp"
+
+namespace http {
+
+namespace {
+const std::size_t BUFFER_SIZE = 2048;
+}
+
+bool Request::recvRequest() {
+    char buffer[BUFFER_SIZE];
+
+    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
+    if (bytesReceived == 0) {
+        toolbox::logger::StepMark::info("Request: recvRequest: client "
+            "disconnected " + toolbox::to_string(_client->getFd()));
+        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
+            _response.setStatus(HttpStatus::OK);
+            return true;
+        } else {
+            _response.setStatus(HttpStatus::BAD_REQUEST);
+            return false;
+        }
+    } else if (bytesReceived == -1) {
+        toolbox::logger::StepMark::error("Request: recvRequest: recv failed "
+            "in recv from " + toolbox::to_string(_client->getFd()));
+        _response.setStatus(HttpStatus::INTERNAL_SERVER_ERROR);
+        return false;
+    }
+
+    _parsedRequest.run(std::string(buffer, bytesReceived));
+
+    std::size_t clientMaxBodySize = 0;
+
+    BaseParser::ValidatePos validatePos = _parsedRequest.getValidatePos();
+    bool hasContentLength = _parsedRequest.get().body.contentLength != 
+                           std::numeric_limits<std::size_t>::max();
+
+    if ((validatePos == BaseParser::V_BODY ||
+        validatePos == BaseParser::V_COMPLETED) && hasContentLength) {
+        if (_config.getPath().empty()) {  // If path is empty, fetch the config
+            fetchConfig();
+        }
+
+        clientMaxBodySize = _config.getClientMaxBodySize();
+
+        std::size_t contentLength = _parsedRequest.get().body.contentLength;
+        if (contentLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: content length exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+
+        std::size_t receivedLength = _parsedRequest.get().body.receivedLength;
+        if (receivedLength > clientMaxBodySize) {
+            toolbox::logger::StepMark::info(
+                "Request: recvRequest: receivedLength exceeds "
+                "client max body size");
+            _response.setStatus(HttpStatus::PAYLOAD_TOO_LARGE);
+            return false;
+        }
+    }
+    return true;
+}
path: src/http/request/recv_request.cpp
user: login: github-actions[bot]
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

`recvRequest`関数内で、`recv`の戻り値が0の場合にクライアントが切断されたと判断していますが、この場合はHTTPステータスを設定する前に、接続が本当に終了したかどうかを確認するために追加のチェックを行うことを検討してください。例えば、`errno`を確認して、`EAGAIN`や`EWOULDBLOCK`などの一時的なエラーではないことを確認することができます。

また、`recv`のエラーハンドリング部分で、`errno`をログに含めることで、デバッグ時により詳細な情報を得ることができます。

```diff
    int bytesReceived = recv(_client->getFd(), buffer, sizeof(buffer) - 1, 0);
    if (bytesReceived == 0) {
        toolbox::logger::StepMark::info("Request: recvRequest: client "
            "disconnected " + toolbox::to_string(_client->getFd()));
        if (_parsedRequest.getValidatePos() == BaseParser::V_COMPLETED) {
            _response.setStatus(HttpStatus::OK);
            return true;
        } else {
            _response.setStatus(HttpStatus::BAD_REQUEST);
            return false;
        }
    } else if (bytesReceived == -1) {
        toolbox::logger::StepMark::error("Request: recvRequest: recv failed "
            "in recv from " + toolbox::to_string(_client->getFd()) +
            ", errno: " + toolbox::to_string(errno));
        _response.setStatus(HttpStatus::INTERNAL_SERVER_ERROR);
        return false;
    }
```

この修正により、エラーログがより詳細になり、問題の診断が容易になります。


<!-- This is an auto-generated comment by OSS CodeRabbit -->
created_at: 2025-05-25T07:35:31Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-25T07:14:44Z
body: <img src="https://avatars.githubusercontent.com/in/347564?s=41" alt="Image description" width="20" height="20">   CodeRabbit

```
*ウォークスルー*: この変更セットでは、HTTPリクエスト処理の改善が行われています。特に、`MAX_BODY_SIZE`の削除と`content-length`チェックの調整、チャンク転送エンコーディングの処理改善、受信データの解析ロジックの強化が含まれます。また、テスト用Makefileの更新により、コンパイル対象ファイルが明示的に指定されています。

*変更点*:
- **src/http/http_namespace.cpp, src/http/http_namespace.hpp, src/http/parsing/field_validator.cpp**: `MAX_BODY_SIZE`削除、`content-length`チェック削除。
- **src/http/parsing/base_parser.hpp**: `getValidatePos`をconstメソッドとして再定義。
- **src/http/request/http_request.hpp, src/http/request/request_parser.cpp, src/http/request/recv_request.cpp**: `HTTPRequest::Body`のフィールド名変更、ボディ処理ロジック改善、エラー処理追加。
- **test/http/request_parse/Makefile**: テスト対象ファイルを明示的に指定。
```
<!-- This is an auto-generated comment: raw summary by OSS CodeRabbit -->
<!--

以下は、関連する変更をグループ化した更新された変更セットです。

---
src/http/http_namespace.cpp, src/http/http_namespace.hpp, src/http/parsing/field_validator.cpp: この差分では、`fields`名前空間内のグローバル定数`MAX_BODY_SIZE`が削除され、`FieldValidator::validateContentLength`関数から`content-length`が`MAX_BODY_SIZE`を超えるかどうかのチェックが削除されています。この変更は、外部インターフェースやコードの動作に影響を与える可能性があります。
---
src/http/parsing/base_parser.hpp: この差分では、`BaseParser`クラスのメンバー関数`getValidatePos`がconstメソッドとして再定義されました。また、非constバージョンの`getValidatePos`が削除されています。この変更により、外部インターフェースに影響を与える可能性があります。
---
src/http/request/http_request.hpp, src/http/request/request_parser.cpp, src/http/request/recv_request.cpp: この差分では、`HTTPRequest::Body`構造体の`recvedLength`が`receivedLength`にリネームされ、`contentLength`が`std::numeric_limits<std::size_t>::max()`で初期化されるようになりました。`RequestParser`では、リクエストボディ処理ロジックが修正され、特にチャンク転送エンコーディングの処理が改善されています。`hasLastChunk`関数が追加され、最後のチャンクを検出するために使用されます。また、`Request::recvRequest`関数が追加され、受信データをバッファに格納し、`RequestParser`を使用して解析します。`Content-Length`と`clientMaxBodySize`の比較を行い、制限を超えた場合は適切なHTTPステータスを設定します。受信中にエラーが発生した場合やクライアントが切断された場合の処理も含まれています。
---
test/http/request_parse/Makefile: The diff updates the test source files list by explicitly including specific request-related source files (`http_fields.cpp`, `request_field_parser.cpp`, and `request_parser.cpp`) instead of using a wildcard for all files in the `request` directory. This change may affect which files are compiled during testing.
-->
<!-- end of auto-generated comment: raw summary by OSS CodeRabbit -->
<!-- This is an auto-generated comment: short summary by OSS CodeRabbit -->
<!--

この変更セットでは、以下の更新が行われています：

1. `src/http/http_namespace.cpp`, `src/http/http_namespace.hpp`, `src/http/parsing/field_validator.cpp`:
   - `fields`名前空間内のグローバル定数`MAX_BODY_SIZE`が削除されました。
   - `FieldValidator::validateContentLength`関数から、`content-length`が`MAX_BODY_SIZE`を超えるかどうかのチェックが削除されました。

2. `src/http/parsing/base_parser.hpp`:
   - `BaseParser`クラスのメンバー関数`getValidatePos`がconstメソッドとして再定義されました。
   - 非constバージョンの`getValidatePos`が削除されました。

3. `src/http/request/http_request.hpp`, `src/http/request/request_parser.cpp`, `src/http/request/recv_request.cpp`:
   - `HTTPRequest::Body`構造体の`recvedLength`が`receivedLength`にリネームされました。
   - `contentLength`が`std::numeric_limits<std::size_t>::max()`で初期化されるようになりました。
   - リクエストボディ処理ロジックが修正され、特にチャンク転送エンコーディングの処理が改善されました。
   - `hasLastChunk`関数が追加され、最後のチャンクを検出するために使用されます。
   - `Request::recvRequest`関数が追加され、受信データをバッファに格納し、`RequestParser`を使用して解析します。
   - `Content-Length`と`clientMaxBodySize`の比較を行い、制限を超えた場合は適切なHTTPステータスを設定します。
   - 受信中にエラーが発生した場合やクライアントが切断された場合の処理も含まれています。

4. `test/http/request_parse/Makefile`:
   - テストソースファイルリストが更新され、`request`ディレクトリ内のすべてのファイルをワイルドカードで指定する代わりに、特定のリクエスト関連ソースファイル（`http_fields.cpp`, `request_field_parser.cpp`, `request_parser.cpp`）が明示的に含まれるようになりました。
-->
<!-- end of auto-generated comment: short summary by OSS CodeRabbit -->

---

<details>
<summary>Uplevel your code reviews with CodeRabbit Pro</summary>

### CodeRabbit Pro

If you like this project, please support us by purchasing the [Pro version](https://coderabbit.ai). The Pro version has advanced context, superior noise reduction and several proprietary improvements compared to the open source version. Moreover, CodeRabbit Pro is free for open source projects.

</details>

<!-- commit_ids_reviewed_start -->
<!-- d92f8a95e05131e8cbe116ecf10841a6aa103e0c -->
<!-- 7a638287564acd9de144d3038c3dbb1c119d8311 -->
<!-- 64ee765b0a47bb35b3909c69670d67072706ebe0 -->
<!-- commit_ids_reviewed_end -->

<!-- This is an auto-generated comment: summarize by OSS CodeRabbit -->
author_association: NONE
user: login: oonisidesu
created_at: 2025-05-25T07:24:29Z
body: ご対応ありがとうございます！
ちょっと今日は校舎行けないので明日動作確認します
author_association: OWNER
user: login: oonisidesu
created_at: 2025-05-25T08:07:12Z
body: > src/http/request/request.hppがうまく取り込めず？

#115 のマージ後にdevelopブランチから新しいブランチ切るとコンフリクト起きなかったかもですね〜
（マージ後にdevelopブランチから新しいブランチ切ったけどfecthしてないdevelopが対象だったとかですかね）
現状、今のPRには以下のコードが存在していません
- request_impl_config.cpp
- request_impl_exec.cpp
- request_impl_parse.cpp
- request_impl_response.cpp
- request.hpp

> この後CGIのissueを進める際に関数分割する予定です

ありがとうございます
author_association: OWNER
user: login: yu-432
created_at: 2025-05-25T09:03:41Z
body: ブランチを切るタイミングとmergeするタイミングがあんまり良くなかったみたいでコンフリクトが発生してしまったので、コンフリクトしたファイルを一旦削除してプルリクを出してます。なんとなくは原因がわかったので次回気をつけます。
author_association: COLLABORATOR