]> git.leonardobizzoni.com Git - http-lib/commitdiff
Request parsing and dispatch
authorLeonardoBizzoni <leo2002714@gmail.com>
Thu, 15 Aug 2024 16:42:57 +0000 (18:42 +0200)
committerLeonardoBizzoni <leo2002714@gmail.com>
Thu, 15 Aug 2024 16:42:57 +0000 (18:42 +0200)
Missing sending the `Response` to the client.
There is also an error when its given a query string alongside a
path eg. `example.com/eg?meaning_of_life=42`. This puts inside
`Request.query` `/eg?meaning_of_life=42` instead of just `/eg`.

12 files changed:
src/const_definitions.h [new file with mode: 0644]
src/error.h
src/http.h
src/http_version.h [new file with mode: 0644]
src/listener.cpp
src/listener.h
src/main.cpp
src/method.h
src/request.cpp
src/request.h
src/response.cpp
src/response.h

diff --git a/src/const_definitions.h b/src/const_definitions.h
new file mode 100644 (file)
index 0000000..507854e
--- /dev/null
@@ -0,0 +1,145 @@
+#pragma once
+
+#include <cstdint>
+#include <string_view>
+#include <unordered_map>
+
+#define Err(error) std::unexpected(error)
+
+namespace http {
+  constexpr std::string_view NEW_LINE = "\r\n";
+
+  enum class Status {
+    CONTINUE = 100,
+    SWITCHING_PROTOCOLS = 101,
+    PROCESSING = 102,
+
+    OK = 200,
+    CREATED,
+    ACCEPTED,
+    NON_AUTHORITATIVE_INFORMATION,
+    NO_CONTENT,
+    PARTIAL_CONTENT,
+    ALREADY_REPORTED,
+    IM_USED,
+
+    MULTIPLE_CHOICES = 300,
+    MOVED_PERMANENTLY,
+    FOUND,
+    SEE_OTHER,
+    NOT_MODIFIED,
+    USE_PROXY,
+    UNUSED,
+    TEMPORARY_REDIRECT,
+    PERMANENT_REDIRECT,
+
+    BAD_REQUEST = 400,
+    UNAUTHORIZED,
+    PAYMENT_REQUIRED,
+    FORBIDDEN,
+    NOT_FOUND,
+    METHOD_NOT_ALLOWED,
+    NOT_ACCEPTABLE,
+    PROXY_AUTHENTICATION_REQUIRED,
+    REQUEST_TIMEOUT,
+    CONFLICT,
+    GONE,
+    LENGTH_REQUIRED,
+    PRECONDITION_FAILED,
+    PAYLOAD_TOO_LARGE,
+    URI_TOO_LONG,
+    UNSUPPORTED_MEDIA_TYPE,
+    RANGE_NOT_SATISFIABLE,
+    EXPECTATION_FAILED,
+    IM_A_TEAPOT,
+    MISDIRECTED_REQUEST = 421,
+    UNPROCESSABLE_CONTENT,
+    LOCKED,
+    FAILED_DEPENDENCY,
+    TOO_EARLY,
+    UPGRADE_REQUIRED,
+    PRECONDITION_REQUIRED = 428,
+    TOO_MANY_REQUESTS,
+    REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
+    UNAVAILABLE_FOR_LEGAL_REASONS = 451,
+
+    INTERNAL_SERVER_ERROR = 500,
+    NOT_IMPLEMENTED,
+    BAD_GATEWAY,
+    SERVICE_UNAVAILABLE,
+    GATEWAY_TIMEOUT,
+    HTTP_VERSION_NOT_SUPPORTED,
+    VARIANT_ALSO_NEGOTIATES,
+    INSUFFICIENT_STORAGE,
+    LOOP_DETECTED,
+    NOT_EXTENDED,
+    NETWORK_AUTHENTICATION_REQUIRED,
+  };
+
+  const std::unordered_map<uint16_t, Status> status_map = {
+      {100, Status::CONTINUE},
+      {101, Status::SWITCHING_PROTOCOLS},
+      {102, Status::PROCESSING},
+
+      {200, Status::OK},
+      {201, Status::CREATED},
+      {202, Status::ACCEPTED},
+      {203, Status::NON_AUTHORITATIVE_INFORMATION},
+      {204, Status::NO_CONTENT},
+      {205, Status::PARTIAL_CONTENT},
+      {208, Status::ALREADY_REPORTED},
+      {226, Status::IM_USED},
+
+      {300, Status::MULTIPLE_CHOICES},
+      {301, Status::MOVED_PERMANENTLY},
+      {302, Status::FOUND},
+      {303, Status::SEE_OTHER},
+      {304, Status::NOT_MODIFIED},
+      {305, Status::USE_PROXY},
+      {306, Status::UNUSED},
+      {307, Status::TEMPORARY_REDIRECT},
+      {308, Status::PERMANENT_REDIRECT},
+
+      {400, Status::BAD_REQUEST},
+      {401, Status::UNAUTHORIZED},
+      {402, Status::PAYMENT_REQUIRED},
+      {403, Status::FORBIDDEN},
+      {404, Status::NOT_FOUND},
+      {405, Status::METHOD_NOT_ALLOWED},
+      {406, Status::NOT_ACCEPTABLE},
+      {407, Status::PROXY_AUTHENTICATION_REQUIRED},
+      {408, Status::REQUEST_TIMEOUT},
+      {409, Status::CONFLICT},
+      {410, Status::GONE},
+      {411, Status::LENGTH_REQUIRED},
+      {412, Status::PRECONDITION_FAILED},
+      {413, Status::PAYLOAD_TOO_LARGE},
+      {414, Status::URI_TOO_LONG},
+      {415, Status::UNSUPPORTED_MEDIA_TYPE},
+      {416, Status::RANGE_NOT_SATISFIABLE},
+      {417, Status::EXPECTATION_FAILED},
+      {418, Status::IM_A_TEAPOT},
+      {421, Status::MISDIRECTED_REQUEST},
+      {422, Status::UNPROCESSABLE_CONTENT},
+      {423, Status::LOCKED},
+      {424, Status::FAILED_DEPENDENCY},
+      {425, Status::TOO_EARLY},
+      {426, Status::UPGRADE_REQUIRED},
+      {428, Status::PRECONDITION_REQUIRED},
+      {429, Status::TOO_MANY_REQUESTS},
+      {431, Status::REQUEST_HEADER_FIELDS_TOO_LARGE},
+      {451, Status::UNAVAILABLE_FOR_LEGAL_REASONS},
+
+      {500, Status::INTERNAL_SERVER_ERROR},
+      {501, Status::NOT_IMPLEMENTED},
+      {502, Status::BAD_GATEWAY},
+      {503, Status::SERVICE_UNAVAILABLE},
+      {504, Status::GATEWAY_TIMEOUT},
+      {505, Status::HTTP_VERSION_NOT_SUPPORTED},
+      {506, Status::VARIANT_ALSO_NEGOTIATES},
+      {507, Status::INSUFFICIENT_STORAGE},
+      {508, Status::LOOP_DETECTED},
+      {510, Status::NOT_EXTENDED},
+      {511, Status::NETWORK_AUTHENTICATION_REQUIRED},
+  };
+}
index f67be2451bcea4b800f952c7c357b46a841f6df9..0d52dec3ddd89a8651d845c0046d871c701f645f 100644 (file)
@@ -8,9 +8,12 @@ namespace http {
     SocketConnection,
     DNSResolution,
     ServerNotFound,
-    InvalidRead,
     InvalidResponse,
-    InvalidResponseHTTPVersion,
+    InvalidRequest,
+    InvalidRequestMethod,
+    InvalidRequestPath,
+    InvalidQueryRequest,
+    InvalidHTTPVersion,
     InvalidResponseStatusCode,
   };
 }
index 87d16f3b11a308d1ab9da072dca5d286ea25ad90..efef966fb5e7e93db98b984adc1fc6527044fc1c 100644 (file)
@@ -2,17 +2,14 @@
 
 #include <future>
 
+#include "const_definitions.h"
 #include "error.h"
 #include "method.h"
 #include "request.h"
 #include "response.h"
 #include "listener.h"
 
-#define Err(error) std::unexpected(error)
-
 namespace http {
-  constexpr std::string_view NEW_LINE = "\r\n";
-
   std::expected<Response, Error> send(Method, const Request& req);
 
   std::expected<int8_t, Error> connect(const std::string_view& domain_name, const uint16_t port = 80);
diff --git a/src/http_version.h b/src/http_version.h
new file mode 100644 (file)
index 0000000..7bc19ba
--- /dev/null
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <cstdint>
+
+namespace http {
+  struct http_version {
+    uint8_t major = 1;
+    uint8_t minor = 1;
+  };
+}  // namespace http
index 31bb3fe3d1e9edbde60542e4630d185595a723c9..8c801b7344a246bea28e78fc6542cf953e05a1a2 100644 (file)
@@ -5,8 +5,8 @@
 #include <unistd.h>
 
 #include <cstdio>
-#include <iostream>
 
+#include "const_definitions.h"
 #include "error.h"
 #include "http.h"
 
@@ -39,18 +39,32 @@ namespace http {
 
   std::expected<void, Error> Listener::serve() {
     socklen_t addrsize = sizeof(this->addr);
-    int8_t clientfd = ::accept(this->socketfd, (struct sockaddr *)&this->addr, &addrsize);
-    if (clientfd < 0) {
-      return Err(Error::SocketConnection);
-    }
 
-    auto raw_req = read_raw_message(clientfd);
-    if (raw_req.has_value()) {
-      std::cout << raw_req.value() << std::endl;
+    while (true) {
+      int8_t clientfd = ::accept(this->socketfd, (struct sockaddr *)&this->addr, &addrsize);
+      if (clientfd < 0) {
+       continue;
+      }
+
+      Response resp;
+      auto raw_req = read_raw_message(clientfd);
+      if (raw_req.has_value()) {
+       auto maybe_req = Request::build(raw_req.value());
+
+       if (maybe_req.has_value()) {
+         try {
+           resp = this->routes.at(maybe_req.value().query)(maybe_req.value());
+         } catch (...) {
+         }
+       }
+      }
+
+      // Send `resp` to client
+
+      ::shutdown(clientfd, SHUT_RDWR);
+      ::close(clientfd);
     }
 
-    ::shutdown(clientfd, SHUT_RDWR);
-    ::close(clientfd);
     return {};
   }
 }  // namespace http
index 00bc6d2a83269179bc50c5f0b879d8c6a1e20a10..5fa08463cbd2b9013206526bd7285580bd23e049 100644 (file)
@@ -10,6 +10,7 @@
 
 #include "error.h"
 #include "request.h"
+#include "response.h"
 
 namespace http {
   struct Listener {
@@ -34,7 +35,7 @@ namespace http {
                                             uint16_t backlog = 4096);
 
   public:
-    std::unordered_map<std::string, std::function<void(const Request &)>> routes;
+    std::unordered_map<std::string, std::function<Response(const Request &)>> routes;
 
   private:
     int8_t socketfd;
index 5fdd54fb2b6231de47bb407e6fb93f628ed62fdd..ace77e48b4245bc14ee3b5cbd5ca82ac68dd21c8 100644 (file)
@@ -1,13 +1,19 @@
 #include <iostream>
 
 #include "http.h"
+#include "request.h"
+#include "response.h"
 
 int main() {
-  auto server = http::Listener::create_on_local(8800);
+  auto server = http::Listener::on_local(8800);
+  server.value().routes["/"] = [](const http::Request &req) -> http::Response {
+    std::cout << req << std::endl;
+    return http::Response();
+  };
+  server.value().routes["/prova"] = [](const http::Request &req) -> http::Response {
+    std::cout << "Hello, World!" << std::endl;
+    auto _ = req.body;
+    return http::Response();
+  };
   server.value().serve();
-
-  server.value().routes["prova"] = [](const http::Request &req) { std::cout << req << std::endl; };
-
-  http::Request request;
-  server.value().routes["prova"](request);
 }
index 3b787c1da70df5b03cb48c8950fd936f83152c45..fd980fa1dda63780dd6f9e4fbb688a84636d7cc5 100644 (file)
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <ostream>
+#include <unordered_map>
 namespace http {
   enum class Method {
     GET,
@@ -14,6 +15,13 @@ namespace http {
     PATCH,
     UPDATE,
   };
+
+  const std::unordered_map<std::string, Method> method_map = {
+      {"get", Method::GET},        {"head", Method::HEAD},     {"post", Method::POST},
+      {"put", Method::PUT},        {"delete", Method::DELETE}, {"connect", Method::CONNECT},
+      {"options", Method::OPTIONS}, {"trace", Method::TRACE},  {"patch", Method::PATCH},
+      {"update", Method::UPDATE},
+  };
 }  // namespace http
 
 std::ostream &operator<<(std::ostream &os, const http::Method &method);
index 0b194345725abe8e4c1ad50b892640eebc83cf50..4758d519a3d0dc69dcc466c14e2e3baf54d3a3ee 100644 (file)
@@ -1,8 +1,96 @@
 #include "request.h"
 
+#include <algorithm>
+#include <cctype>
 #include <iomanip>
+#include <iostream>
+#include <iterator>
+#include <ranges>
+
+#include "const_definitions.h"
+#include "method.h"
 
 std::ostream &operator<<(std::ostream &os, const http::Request &req) {
-  return os << "Request { domain: " << std::quoted(req.domain_name) << ", port: " << req.port
-           << ", query: " << std::quoted(req.query) << ", body: " << std::quoted(req.body) << " }";
+  os << "Request { domain: " << std::quoted(req.domain_name) << ", port: " << req.port
+     << ", query: " << std::quoted(req.query) << ", method: " << req.method
+     << ", version: " << (int)req.version.major << "." << (int)req.version.minor << ", headers: [ ";
+
+  for (const auto &header : req.optheaders) {
+    os << header.first << " = " << header.second << ",";
+  }
+
+  os << " ] }";
+  return os;
 }
+
+namespace http {
+  std::expected<Request, Error> Request::build(std::string_view raw_request) noexcept {
+    if (raw_request.empty()) {
+      return Err(Error::InvalidRequest);
+    }
+
+    Request req;
+
+    auto lines_view = raw_request | std::views::split(NEW_LINE);
+    auto lines_view_iter = lines_view.begin();
+
+    auto words = *lines_view_iter++ | std::views::split(' ');
+    auto word_iter = words.begin();
+
+    std::string_view method_view(*word_iter++);
+    std::string method(method_view);
+    std::transform(method_view.cbegin(), method_view.cend(), method.begin(),
+                  [](unsigned char ch) { return std::tolower(ch); });
+
+    try {
+      req.method = method_map.at(method);
+    } catch (...) {
+      return Err(Error::InvalidRequestMethod);
+    }
+
+    std::string_view query(*word_iter++);
+    std::cout << "\t" << query << std::endl;
+    if (query.empty() || !query.starts_with('/')) {
+      return Err(Error::InvalidQueryRequest);
+    } else {
+      req.query = query;
+    }
+
+    std::string_view version(*word_iter);
+    auto number = version.substr(version.find_first_of('/') + 1);
+    auto major = number.substr(0, number.find_first_of('.'));
+    auto minor = number.substr(number.find_first_of('.') + 1);
+
+    try {
+      req.version = {.major = (uint8_t)std::stoi(major.data()),
+                    .minor = (uint8_t)std::stoi(minor.data())};
+    } catch (...) {
+      return Err(Error::InvalidHTTPVersion);
+    }
+
+    for (std::string_view line; lines_view_iter != lines_view.end() &&
+                               !(line = std::string_view(*lines_view_iter)).empty();
+        ++lines_view_iter) {
+      auto words = line | std::views::split(std::string_view{":"});
+      auto it = words.begin();
+      std::string field_name((*it).begin(), (*it).end());
+      ++it;
+
+      if ((*it)[0] == ' ') {
+       req.optheaders[field_name] = std::string((*it).begin() + 1, (*it).end());
+      } else {
+       req.optheaders[field_name] = std::string((*it).begin(), (*it).end());
+      }
+    }
+
+    while (++lines_view_iter != lines_view.end()) {
+      req.body += std::string((*lines_view_iter).begin(), (*lines_view_iter).end());
+
+      if (std::next(lines_view_iter) != lines_view.end()) {
+       req.body += "\n";
+      }
+    }
+
+    return req;
+  }
+}  // namespace http
index 270511316188c62d3af8561a79ab9035f80564a7..913abc8da3a46cde4e8ec57dc1b22ce4510af705 100644 (file)
@@ -1,22 +1,27 @@
 #pragma once
 
 #include <cstdint>
-#include <string_view>
+#include <expected>
 #include <ostream>
+#include <string_view>
 #include <unordered_map>
 
-namespace http {
-  struct http_version {
-    uint8_t major = 1;
-    uint8_t minor = 1;
-  };
+#include "error.h"
+#include "http_version.h"
+#include "method.h"
 
+namespace http {
   struct Request {
+  public:
+    static std::expected<Request, Error> build(std::string_view raw_response) noexcept;
+
+  public:
     uint16_t port = 80;
     std::string_view domain_name;
-    std::string_view query = "/";
-    std::string_view body = "";
+    std::string query = "/";
+    std::string body = "";
 
+    Method method;
     http_version version = {.major = 1, .minor = 1};
 
     std::unordered_map<std::string, std::string> optheaders = {};
index 4b02413119e7089fb1d7523ef998a732c33c3401..d3be232dc29a153ae72ed23d4f206599c846f7a1 100644 (file)
@@ -1,10 +1,9 @@
+#include "response.h"
+#include <expected>
 #include <iostream>
 #include <ranges>
-#include <string_view>
 #include <utility>
 
-#include "http.h"
-
 namespace http {
   std::expected<Response, Error> Response::build(std::string_view raw_response) noexcept {
     if (raw_response.empty()) {
@@ -27,12 +26,12 @@ namespace http {
       resp.version = {.major = (uint8_t)std::stoi(major.data()),
                      .minor = (uint8_t)std::stoi(minor.data())};
     } catch (...) {
-      return Err(Error::InvalidResponseHTTPVersion);
+      return Err(Error::InvalidHTTPVersion);
     }
 
     try {
       std::string_view status(*++word_iter);
-      resp.status = status_map[std::stoi(status.data())];
+      resp.status = status_map.at(std::stoi(status.data()));
     } catch (...) {
       return Err(Error::InvalidResponseStatusCode);
     }
index 2c54b62cd47aca063d0adbc33405f7954212a989..11d64eeb74efa101f8bfc1f51d3efe4a4d11693d 100644 (file)
 #pragma once
 
-#include <cstdint>
 #include <expected>
 #include <string>
-#include <string_view>
-#include <unordered_map>
 
+#include "const_definitions.h"
 #include "error.h"
-#include "request.h"
+#include "http_version.h"
 
 namespace http {
-  enum class Status {
-    CONTINUE = 100,
-    SWITCHING_PROTOCOLS = 101,
-    PROCESSING = 102,
-
-    OK = 200,
-    CREATED,
-    ACCEPTED,
-    NON_AUTHORITATIVE_INFORMATION,
-    NO_CONTENT,
-    PARTIAL_CONTENT,
-    ALREADY_REPORTED,
-    IM_USED,
-
-    MULTIPLE_CHOICES = 300,
-    MOVED_PERMANENTLY,
-    FOUND,
-    SEE_OTHER,
-    NOT_MODIFIED,
-    USE_PROXY,
-    UNUSED,
-    TEMPORARY_REDIRECT,
-    PERMANENT_REDIRECT,
-
-    BAD_REQUEST = 400,
-    UNAUTHORIZED,
-    PAYMENT_REQUIRED,
-    FORBIDDEN,
-    NOT_FOUND,
-    METHOD_NOT_ALLOWED,
-    NOT_ACCEPTABLE,
-    PROXY_AUTHENTICATION_REQUIRED,
-    REQUEST_TIMEOUT,
-    CONFLICT,
-    GONE,
-    LENGTH_REQUIRED,
-    PRECONDITION_FAILED,
-    PAYLOAD_TOO_LARGE,
-    URI_TOO_LONG,
-    UNSUPPORTED_MEDIA_TYPE,
-    RANGE_NOT_SATISFIABLE,
-    EXPECTATION_FAILED,
-    IM_A_TEAPOT,
-    MISDIRECTED_REQUEST = 421,
-    UNPROCESSABLE_CONTENT,
-    LOCKED,
-    FAILED_DEPENDENCY,
-    TOO_EARLY,
-    UPGRADE_REQUIRED,
-    PRECONDITION_REQUIRED = 428,
-    TOO_MANY_REQUESTS,
-    REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
-    UNAVAILABLE_FOR_LEGAL_REASONS = 451,
-
-    INTERNAL_SERVER_ERROR = 500,
-    NOT_IMPLEMENTED,
-    BAD_GATEWAY,
-    SERVICE_UNAVAILABLE,
-    GATEWAY_TIMEOUT,
-    HTTP_VERSION_NOT_SUPPORTED,
-    VARIANT_ALSO_NEGOTIATES,
-    INSUFFICIENT_STORAGE,
-    LOOP_DETECTED,
-    NOT_EXTENDED,
-    NETWORK_AUTHENTICATION_REQUIRED,
-  };
-
-  static std::unordered_map<uint16_t, Status> status_map = {
-      {100, Status::CONTINUE},
-      {101, Status::SWITCHING_PROTOCOLS},
-      {102, Status::PROCESSING},
-
-      {200, Status::OK},
-      {201, Status::CREATED},
-      {202, Status::ACCEPTED},
-      {203, Status::NON_AUTHORITATIVE_INFORMATION},
-      {204, Status::NO_CONTENT},
-      {205, Status::PARTIAL_CONTENT},
-      {208, Status::ALREADY_REPORTED},
-      {226, Status::IM_USED},
-
-      {300, Status::MULTIPLE_CHOICES},
-      {301, Status::MOVED_PERMANENTLY},
-      {302, Status::FOUND},
-      {303, Status::SEE_OTHER},
-      {304, Status::NOT_MODIFIED},
-      {305, Status::USE_PROXY},
-      {306, Status::UNUSED},
-      {307, Status::TEMPORARY_REDIRECT},
-      {308, Status::PERMANENT_REDIRECT},
-
-      {400, Status::BAD_REQUEST},
-      {401, Status::UNAUTHORIZED},
-      {402, Status::PAYMENT_REQUIRED},
-      {403, Status::FORBIDDEN},
-      {404, Status::NOT_FOUND},
-      {405, Status::METHOD_NOT_ALLOWED},
-      {406, Status::NOT_ACCEPTABLE},
-      {407, Status::PROXY_AUTHENTICATION_REQUIRED},
-      {408, Status::REQUEST_TIMEOUT},
-      {409, Status::CONFLICT},
-      {410, Status::GONE},
-      {411, Status::LENGTH_REQUIRED},
-      {412, Status::PRECONDITION_FAILED},
-      {413, Status::PAYLOAD_TOO_LARGE},
-      {414, Status::URI_TOO_LONG},
-      {415, Status::UNSUPPORTED_MEDIA_TYPE},
-      {416, Status::RANGE_NOT_SATISFIABLE},
-      {417, Status::EXPECTATION_FAILED},
-      {418, Status::IM_A_TEAPOT},
-      {421, Status::MISDIRECTED_REQUEST},
-      {422, Status::UNPROCESSABLE_CONTENT},
-      {423, Status::LOCKED},
-      {424, Status::FAILED_DEPENDENCY},
-      {425, Status::TOO_EARLY},
-      {426, Status::UPGRADE_REQUIRED},
-      {428, Status::PRECONDITION_REQUIRED},
-      {429, Status::TOO_MANY_REQUESTS},
-      {431, Status::REQUEST_HEADER_FIELDS_TOO_LARGE},
-      {451, Status::UNAVAILABLE_FOR_LEGAL_REASONS},
-
-      {500, Status::INTERNAL_SERVER_ERROR},
-      {501, Status::NOT_IMPLEMENTED},
-      {502, Status::BAD_GATEWAY},
-      {503, Status::SERVICE_UNAVAILABLE},
-      {504, Status::GATEWAY_TIMEOUT},
-      {505, Status::HTTP_VERSION_NOT_SUPPORTED},
-      {506, Status::VARIANT_ALSO_NEGOTIATES},
-      {507, Status::INSUFFICIENT_STORAGE},
-      {508, Status::LOOP_DETECTED},
-      {510, Status::NOT_EXTENDED},
-      {511, Status::NETWORK_AUTHENTICATION_REQUIRED},
-  };
-
   struct Response {
     http_version version;
     Status status;
@@ -151,11 +15,10 @@ namespace http {
     std::unordered_map<std::string, std::string> fields;
 
   public:
-    static std::expected<Response, Error> build(std::string_view raw_response) noexcept;
+    Response() : version({1,1}), status(Status::NOT_ACCEPTABLE) {}
     ~Response() {}
 
-  private:
-    Response() {}
+    static std::expected<Response, Error> build(std::string_view raw_response) noexcept;
   };
 }  // namespace http