#include "http_handler.hpp" #include #include #include #include "api_url_parser.hpp" #include "arena.hpp" #include "connection.hpp" #include "format.hpp" #include "json_commit_request_parser.hpp" #include "metric.hpp" #include "pipeline_entry.hpp" auto requests_counter_family = metric::create_counter( "weaseldb_http_requests_total", "Total http requests"); thread_local auto metrics_counter = requests_counter_family.create({{"path", "/metrics"}}); // API endpoint request counters thread_local auto commit_counter = requests_counter_family.create({{"path", "/v1/commit"}}); thread_local auto status_counter = requests_counter_family.create({{"path", "/v1/status"}}); thread_local auto version_counter = requests_counter_family.create({{"path", "/v1/version"}}); thread_local auto ok_counter = requests_counter_family.create({{"path", "/ok"}}); thread_local auto not_found_counter = requests_counter_family.create({{"path", "not_found"}}); HttpConnectionState::HttpConnectionState() { llhttp_settings_init(&settings); // Set up llhttp callbacks settings.on_url = HttpHandler::onUrl; settings.on_header_field = HttpHandler::onHeaderField; settings.on_header_field_complete = HttpHandler::onHeaderFieldComplete; settings.on_header_value = HttpHandler::onHeaderValue; settings.on_header_value_complete = HttpHandler::onHeaderValueComplete; settings.on_headers_complete = HttpHandler::onHeadersComplete; settings.on_body = HttpHandler::onBody; settings.on_message_complete = HttpHandler::onMessageComplete; llhttp_init(&parser, HTTP_REQUEST, &settings); parser.data = &pending; } // HttpConnectionState implementation HttpRequestState::HttpRequestState() : url(ArenaStlAllocator(&arena)), current_header_field_buf(ArenaStlAllocator(&arena)), current_header_value_buf(ArenaStlAllocator(&arena)) {} // HttpHandler implementation void HttpHandler::on_connection_established(Connection &conn) { // Allocate HTTP state using server-provided arena for connection lifecycle auto *state = new HttpConnectionState(); conn.user_data = state; } void HttpHandler::on_connection_closed(Connection &conn) { auto *state = static_cast(conn.user_data); delete state; conn.user_data = nullptr; } void HttpHandler::on_preprocess_writes( Connection &conn, std::span pending_responses) { auto *state = static_cast(conn.user_data); // Process incoming responses and add to reorder queue { for (auto &pending : pending_responses) { auto *ctx = static_cast(pending.protocol_context); // Determine HTTP status code and content type from response content int status_code = 200; std::string_view content_type = "application/json"; // For health checks, detect plain text responses if (pending.response_json == "OK") { content_type = "text/plain"; } // For metrics, detect Prometheus format (starts with # or contains metric // names) else if (pending.response_json.starts_with("#") || pending.response_json.find("_total") != std::string_view::npos || pending.response_json.find("_counter") != std::string_view::npos) { content_type = "text/plain; version=0.0.4"; } // Format HTTP response from JSON auto http_response = format_response( status_code, content_type, pending.response_json, pending.arena, ctx->http_request_id, ctx->connection_close); state->send_ordered_response(conn, ctx->sequence_id, http_response, std::move(pending.arena), ctx->connection_close); } } } static thread_local std::vector g_batch_entries; void HttpHandler::on_batch_complete(std::span batch) { // Count commit, status, and health check requests for (auto conn : batch) { auto *state = static_cast(conn->user_data); for (auto &req : state->queue) { // Assign sequence ID for response ordering int64_t sequence_id = state->get_next_sequence_id(); req.sequence_id = sequence_id; // Create HttpResponseContext for this request auto *ctx = req.arena.allocate(1); ctx->sequence_id = sequence_id; ctx->http_request_id = req.http_request_id; ctx->connection_close = req.connection_close; RouteMatch route_match; auto parse_result = ApiUrlParser::parse(req.method, const_cast(req.url.data()), static_cast(req.url.size()), route_match); if (parse_result != ParseResult::Success) { // Handle malformed URL encoding auto json_response = R"({"error":"Malformed URL encoding"})"; auto http_response = format_json_response(400, json_response, req.arena, 0, true); state->send_ordered_response(*conn, ctx->sequence_id, http_response, std::move(req.arena), ctx->connection_close); break; } req.route = route_match.route; // Route to appropriate handler switch (req.route) { case HttpRoute::GetVersion: handle_get_version(*conn, req); break; case HttpRoute::PostCommit: handle_post_commit(*conn, req); break; case HttpRoute::GetSubscribe: handle_get_subscribe(*conn, req); break; case HttpRoute::GetStatus: handle_get_status(*conn, req, route_match); break; case HttpRoute::PutRetention: handle_put_retention(*conn, req, route_match); break; case HttpRoute::GetRetention: handle_get_retention(*conn, req, route_match); break; case HttpRoute::DeleteRetention: handle_delete_retention(*conn, req, route_match); break; case HttpRoute::GetMetrics: handle_get_metrics(*conn, req); break; case HttpRoute::GetOk: handle_get_ok(*conn, req); break; case HttpRoute::NotFound: default: handle_not_found(*conn, req); break; } // Create CommitEntry for commit requests if (req.route == HttpRoute::PostCommit && req.commit_request && req.parsing_commit && req.basic_validation_passed) { g_batch_entries.emplace_back(CommitEntry(conn->get_weak_ref(), ctx, req.commit_request.get(), std::move(req.arena))); } // Create StatusEntry for status requests else if (req.route == HttpRoute::GetStatus) { g_batch_entries.emplace_back(StatusEntry(conn->get_weak_ref(), ctx, req.status_request_id, std::move(req.arena))); } // Create HealthCheckEntry for health check requests else if (req.route == HttpRoute::GetOk) { g_batch_entries.emplace_back( HealthCheckEntry(conn->get_weak_ref(), ctx, std::move(req.arena))); } // Create GetVersionEntry for version requests else if (req.route == HttpRoute::GetVersion) { g_batch_entries.emplace_back( GetVersionEntry(conn->get_weak_ref(), ctx, std::move(req.arena), commit_pipeline_.get_committed_version())); } } state->queue.clear(); } // Send requests to commit pipeline in batch. Batching here reduces // contention on the way into the pipeline. if (!g_batch_entries.empty()) { commit_pipeline_.submit_batch(g_batch_entries); } g_batch_entries.clear(); } void HttpHandler::on_data_arrived(std::string_view data, Connection &conn) { auto *state = static_cast(conn.user_data); assert(state); // TODO: Enforce the configured max_request_size_bytes limit here. // Should track cumulative bytes received for the current HTTP request // and send 413 Request Entity Too Large if limit is exceeded. // This prevents DoS attacks via oversized HTTP requests. // Parse HTTP data with llhttp for (;;) { enum llhttp_errno err = llhttp_execute(&state->parser, data.data(), data.size()); if (err == HPE_PAUSED) { assert(state->pending.message_complete); state->queue.push_back(std::move(state->pending)); state->pending = {}; int consumed = llhttp_get_error_pos(&state->parser) - data.data(); data = data.substr(consumed, data.size() - consumed); llhttp_resume(&state->parser); continue; } if (err == HPE_OK) { break; } // Parse error - send response directly since this is before sequence // assignment auto json_response = R"({"error":"Bad request"})"; auto http_response = format_json_response(400, json_response, state->pending.arena, 0, true); state->send_ordered_response(conn, state->get_next_sequence_id(), http_response, std::move(state->pending.arena), true); return; } } // Route handlers (basic implementations) void HttpHandler::handle_get_version(Connection &, HttpRequestState &) { version_counter.inc(); // Sent to commit pipeline } void HttpHandler::handle_post_commit(Connection &conn, HttpRequestState &state) { commit_counter.inc(); // Check if streaming parse was successful if (!state.commit_request || !state.parsing_commit) { auto json_response = R"({"error":"Parse failed"})"; auto http_response = format_json_response(400, json_response, state.arena, state.http_request_id, state.connection_close); auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); return; } const CommitRequest &commit_request = *state.commit_request; // Perform basic validation that doesn't require serialization (done on I/O // threads) bool valid = true; std::string_view error_msg; // Check that we have at least one operation if (commit_request.operations().empty()) { valid = false; error_msg = "Commit request must contain at least one operation"; } // Check leader_id is not empty if (valid && commit_request.leader_id().empty()) { valid = false; error_msg = "Commit request must specify a leader_id"; } // Check operations are well-formed if (valid) { for (const auto &op : commit_request.operations()) { if (op.param1.empty()) { valid = false; error_msg = "Operation key cannot be empty"; break; } if (op.type == Operation::Type::Write && op.param2.empty()) { valid = false; error_msg = "Write operation value cannot be empty"; break; } } } if (!valid) { auto json_response = format(state.arena, R"({"error":"%.*s"})", static_cast(error_msg.size()), error_msg.data()); auto http_response = format_json_response(400, json_response, state.arena, state.http_request_id, state.connection_close); auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); return; } // Basic validation passed - mark for 4-stage pipeline processing state.basic_validation_passed = true; // Response will be sent after 4-stage pipeline processing is complete } void HttpHandler::handle_get_subscribe(Connection &conn, HttpRequestState &state) { // TODO: Implement subscription streaming auto json_response = R"({"message":"Subscription endpoint - streaming not yet implemented"})"; auto http_response = format_json_response(200, json_response, state.arena, state.http_request_id, state.connection_close); auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); } void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state, const RouteMatch &route_match) { status_counter.inc(); // Status requests are processed through the pipeline // Response will be generated in the sequence stage // This handler extracts request_id from query parameters and prepares for // pipeline processing const auto &request_id = route_match.params[static_cast(ApiParameterKey::RequestId)]; if (!request_id) { auto json_response = R"({"error":"Missing required query parameter: request_id"})"; auto http_response = format_json_response(400, json_response, state.arena, state.http_request_id, state.connection_close); // Add directly to response queue with proper sequencing auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); return; } if (request_id->empty()) { auto json_response = R"({"error":"Empty request_id parameter"})"; auto http_response = format_json_response(400, json_response, state.arena, state.http_request_id, state.connection_close); // Add directly to response queue with proper sequencing auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); return; } // Store the request_id in the state for the pipeline state.status_request_id = *request_id; // Ready for pipeline processing } void HttpHandler::handle_put_retention(Connection &conn, HttpRequestState &state, const RouteMatch &) { // TODO: Parse retention policy from body and store auto json_response = R"({"policy_id":"example","status":"created"})"; auto http_response = format_json_response(200, json_response, state.arena, state.http_request_id, state.connection_close); // Send through reorder queue and preprocessing to maintain proper ordering auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); } void HttpHandler::handle_get_retention(Connection &conn, HttpRequestState &state, const RouteMatch &) { // TODO: Extract policy_id from URL or return all policies auto json_response = R"({"policies":[]})"; auto http_response = format_json_response(200, json_response, state.arena, state.http_request_id, state.connection_close); // Send through reorder queue and preprocessing to maintain proper ordering auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); } void HttpHandler::handle_delete_retention(Connection &conn, HttpRequestState &state, const RouteMatch &) { // TODO: Extract policy_id from URL and delete auto json_response = R"({"policy_id":"example","status":"deleted"})"; auto http_response = format_json_response(200, json_response, state.arena, state.http_request_id, state.connection_close); // Send through reorder queue and preprocessing to maintain proper ordering auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); } void HttpHandler::handle_get_metrics(Connection &conn, HttpRequestState &state) { metrics_counter.inc(); auto metrics_span = metric::render(state.arena); // Calculate total size for the response body size_t total_size = 0; for (const auto &sv : metrics_span) { total_size += sv.size(); } // Build HTTP response with metrics data auto result = state.arena.allocate_span(metrics_span.size() + 1); auto out = result.begin(); // Build HTTP headers std::string_view headers; if (state.connection_close) { headers = static_format( state.arena, "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain; version=0.0.4\r\n", "Content-Length: ", static_cast(total_size), "\r\n", "X-Response-ID: ", static_cast(state.http_request_id), "\r\n", "Connection: close\r\n", "\r\n"); } else { headers = static_format( state.arena, "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain; version=0.0.4\r\n", "Content-Length: ", static_cast(total_size), "\r\n", "X-Response-ID: ", static_cast(state.http_request_id), "\r\n", "Connection: keep-alive\r\n", "\r\n"); } *out++ = headers; for (auto sv : metrics_span) { *out++ = sv; } auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, result, std::move(state.arena), state.connection_close); } void HttpHandler::handle_get_ok(Connection &, HttpRequestState &) { ok_counter.inc(); // Health check requests are processed through the pipeline // Response will be generated in the release stage after pipeline processing } void HttpHandler::handle_not_found(Connection &conn, HttpRequestState &state) { not_found_counter.inc(); auto json_response = R"({"error":"Not found"})"; auto http_response = format_json_response(404, json_response, state.arena, state.http_request_id, state.connection_close); auto *conn_state = static_cast(conn.user_data); conn_state->send_ordered_response(conn, state.sequence_id, http_response, std::move(state.arena), state.connection_close); } void HttpConnectionState::send_ordered_response( Connection &conn, int64_t sequence_id, std::span http_response, Arena arena, bool close_connection) { // Add to reorder queue with proper sequencing ready_responses[sequence_id] = ResponseData{http_response, std::move(arena), close_connection}; // Process ready responses in order and send via append_bytes auto iter = ready_responses.begin(); while (iter != ready_responses.end() && iter->first == next_sequence_to_send) { auto &[sequence_id, response_data] = *iter; // Send through append_bytes which handles write interest conn.append_bytes(response_data.data, std::move(response_data.arena), response_data.connection_close ? ConnectionShutdown::WriteOnly : ConnectionShutdown::None); next_sequence_to_send++; iter = ready_responses.erase(iter); } } std::span HttpHandler::format_response(int status_code, std::string_view content_type, std::string_view body, Arena &response_arena, int64_t http_request_id, bool close_connection) { // Status text std::string_view status_text; switch (status_code) { case 200: status_text = "OK"; break; case 400: status_text = "Bad Request"; break; case 404: status_text = "Not Found"; break; case 500: status_text = "Internal Server Error"; break; default: status_text = "Unknown"; break; } const char *connection_header = close_connection ? "close" : "keep-alive"; auto response = response_arena.allocate_span(1); response[0] = format(response_arena, "HTTP/1.1 %d %.*s\r\n" "Content-Type: %.*s\r\n" "Content-Length: %zu\r\n" "X-Response-ID: %ld\r\n" "Connection: %s\r\n" "\r\n%.*s", status_code, static_cast(status_text.size()), status_text.data(), static_cast(content_type.size()), content_type.data(), body.size(), http_request_id, connection_header, static_cast(body.size()), body.data()); return response; } std::span HttpHandler::format_json_response( int status_code, std::string_view json, Arena &response_arena, int64_t http_request_id, bool close_connection) { return format_response(status_code, "application/json", json, response_arena, http_request_id, close_connection); } // llhttp callbacks int HttpHandler::onUrl(llhttp_t *parser, const char *at, size_t length) { auto *state = static_cast(parser->data); // Accumulate URL data state->url.append(at, length); return 0; } int HttpHandler::onHeaderField(llhttp_t *parser, const char *at, size_t length) { auto *state = static_cast(parser->data); // Accumulate header field data state->current_header_field_buf.append(at, length); return 0; } int HttpHandler::onHeaderFieldComplete(llhttp_t *parser) { auto *state = static_cast(parser->data); state->header_field_complete = true; return 0; } int HttpHandler::onHeaderValue(llhttp_t *parser, const char *at, size_t length) { auto *state = static_cast(parser->data); // Accumulate header value data state->current_header_value_buf.append(at, length); return 0; } int HttpHandler::onHeaderValueComplete(llhttp_t *parser) { auto *state = static_cast(parser->data); if (!state->header_field_complete) { // Field is not complete yet, wait return 0; } // Now we have complete header field and value const auto &field = state->current_header_field_buf; const auto &value = state->current_header_value_buf; // Check for Connection header if (field.size() == 10 && strncasecmp(field.data(), "connection", 10) == 0) { if (value.size() == 5 && strncasecmp(value.data(), "close", 5) == 0) { state->connection_close = true; } } // Check for X-Request-Id header if (field.size() == 12 && strncasecmp(field.data(), "x-request-id", 12) == 0) { int64_t id = 0; for (char c : value) { if (c >= '0' && c <= '9') { id = id * 10 + (c - '0'); } } state->http_request_id = id; } // Clear buffers for next header state->current_header_field_buf.clear(); state->current_header_value_buf.clear(); state->header_field_complete = false; return 0; } int HttpHandler::onHeadersComplete(llhttp_t *parser) { auto *state = static_cast(parser->data); state->headers_complete = true; // Get HTTP method const char *method_str = llhttp_method_name(static_cast(parser->method)); state->method = std::string_view(method_str); // Check if this looks like a POST to /v1/commit to initialize streaming // parser if (state->method == "POST" && state->url.find("/v1/commit") == 0) { // Initialize streaming commit request parsing state->commit_parser = state->arena.construct(); state->commit_request = state->arena.construct(); state->parsing_commit = state->commit_parser->begin_streaming_parse(*state->commit_request); if (!state->parsing_commit) { return -1; // Signal parsing error to llhttp } } return 0; } int HttpHandler::onBody(llhttp_t *parser, const char *at, size_t length) { auto *state = static_cast(parser->data); if (state->parsing_commit && state->commit_parser) { // Stream data to commit request parser auto status = state->commit_parser->parse_chunk(const_cast(at), length); if (status == CommitRequestParser::ParseStatus::Error) { return -1; // Signal parsing error to llhttp } } return 0; } int HttpHandler::onMessageComplete(llhttp_t *parser) { auto *state = static_cast(parser->data); state->message_complete = true; return HPE_PAUSED; }