Skip to content

Commit 00eaf55

Browse files
etrclaude
andcommitted
refactor(dr-014): extract request_pipeline behavior service
Move the MHD re-entrant body-accumulation state machine off webserver_impl into detail::request_pipeline: requests_answer_first_step, requests_answer_second_step, complete_request, and the body_chunk-fire / post-processor anon helpers. It holds const webserver_config& (body-read config), hook_dispatcher& (request_received / body_chunk gates), and request_dispatcher& (complete_request -> finalize_answer); friend of http_request (constructs it + writes its per-request fields). answer_to_connection stays a webserver_impl static MHD trampoline (per- request setup) and forwards into impl_->pipeline_. webserver_body_pipeline.cpp retains only the process-wide debug-dump opt-in helpers (shared with webserver::start). The redundant belt-and-suspenders `mr->ws = parent` refresh in the post-processor helper is dropped (answer_to_connection already set it; the comment noted it never changes the value). This completes the seven-service behavior decomposition (DR-014): error_pages, hook_dispatcher, response_materializer, upload_pipeline, websocket_upgrader, request_dispatcher, request_pipeline. 113/113 tests pass; cpplint/complexity/file-size gates green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ee04989 commit 00eaf55

9 files changed

Lines changed: 364 additions & 200 deletions

src/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ lib_LTLIBRARIES = libhttpserver.la
2525
# builds. The WS-off branch in websocket_handler.cpp provides stub
2626
# definitions (every member throws feature_unavailable except is_valid()
2727
# which returns false).
28-
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp webserver_add_hook.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_response_factories.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp cookie.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/ip_access_control.cpp detail/ws_registry.cpp detail/hook_bus.cpp detail/route_table.cpp detail/daemon_lifecycle.cpp detail/dispatch_util.cpp detail/error_pages.cpp detail/hook_dispatcher.cpp detail/http_request_impl.cpp detail/http_request_impl_args.cpp detail/http_request_impl_tls.cpp detail/request_dispatcher.cpp detail/response_materializer.cpp detail/upload_pipeline.cpp detail/websocket_upgrader.cpp detail/webserver_lifecycle.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_routes_upsert.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_response_queue.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_hook_firing.cpp detail/hook_phase_dispatch.cpp
28+
libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp webserver_add_hook.cpp http_utils.cpp file_info.cpp http_request.cpp http_request_auth.cpp http_response.cpp http_response_factories.cpp http_resource.cpp create_webserver.cpp create_test_request.cpp websocket_handler.cpp hook_handle.cpp peer_address.cpp resource_hook_table.cpp cookie.cpp detail/http_endpoint.cpp detail/body.cpp detail/ip_representation.cpp detail/ip_access_control.cpp detail/ws_registry.cpp detail/hook_bus.cpp detail/route_table.cpp detail/daemon_lifecycle.cpp detail/dispatch_util.cpp detail/error_pages.cpp detail/hook_dispatcher.cpp detail/http_request_impl.cpp detail/http_request_impl_args.cpp detail/http_request_impl_tls.cpp detail/request_dispatcher.cpp detail/request_pipeline.cpp detail/response_materializer.cpp detail/upload_pipeline.cpp detail/websocket_upgrader.cpp detail/webserver_lifecycle.cpp detail/webserver_register.cpp detail/webserver_routes.cpp detail/webserver_routes_upsert.cpp detail/webserver_callbacks.cpp detail/webserver_callbacks_lifecycle.cpp detail/webserver_dispatch.cpp detail/webserver_request.cpp detail/webserver_response_queue.cpp detail/webserver_body_pipeline.cpp detail/webserver_error_pages.cpp detail/webserver_aliases.cpp detail/webserver_hook_firing.cpp detail/hook_phase_dispatch.cpp
2929
# noinst_HEADERS: shipped in the tarball but NEVER installed under $prefix/include.
3030
# Detail headers (httpserver/detail/*.hpp) live here so they cannot leak to
3131
# downstream consumers — the public surface comes in through <httpserver.hpp>.
32-
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/webserver_impl_dispatch.hpp httpserver/detail/connection_state.hpp httpserver/detail/ip_access_control.hpp httpserver/detail/ws_registry.hpp httpserver/detail/hook_bus.hpp httpserver/detail/route_table.hpp httpserver/detail/daemon_lifecycle.hpp httpserver/detail/dispatch_util.hpp httpserver/detail/error_pages.hpp httpserver/detail/hook_dispatcher.hpp httpserver/detail/request_dispatcher.hpp httpserver/detail/response_materializer.hpp httpserver/detail/upload_pipeline.hpp httpserver/detail/websocket_upgrader.hpp httpserver/detail/secure_zero.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/segment_trie.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp httpserver/detail/unescape_helpers.hpp gettext.h
32+
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/webserver_impl_dispatch.hpp httpserver/detail/connection_state.hpp httpserver/detail/ip_access_control.hpp httpserver/detail/ws_registry.hpp httpserver/detail/hook_bus.hpp httpserver/detail/route_table.hpp httpserver/detail/daemon_lifecycle.hpp httpserver/detail/dispatch_util.hpp httpserver/detail/error_pages.hpp httpserver/detail/hook_dispatcher.hpp httpserver/detail/request_dispatcher.hpp httpserver/detail/request_pipeline.hpp httpserver/detail/response_materializer.hpp httpserver/detail/upload_pipeline.hpp httpserver/detail/websocket_upgrader.hpp httpserver/detail/secure_zero.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/resource_hook_table.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp httpserver/detail/segment_trie.hpp httpserver/detail/route_cache.hpp httpserver/detail/route_tier.hpp httpserver/detail/unescape_helpers.hpp gettext.h
3333
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/cookie.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/create_webserver_setters.hpp httpserver/create_test_request.hpp httpserver/webserver.hpp httpserver/webserver_routes.hpp httpserver/webserver_runtime.hpp httpserver/webserver_websocket.hpp httpserver/webserver_hooks.hpp httpserver/websocket_handler.hpp httpserver/http_utils.hpp httpserver/http_utils_helpers.hpp httpserver/ip_representation.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/http_arg_value.hpp httpserver/http_method.hpp httpserver/hook_phase.hpp httpserver/hook_action.hpp httpserver/hook_handle.hpp httpserver/hook_context.hpp
3434

3535
AM_CXXFLAGS += -fPIC -Wall

src/detail/request_pipeline.cpp

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2026 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// request_pipeline behavior service (DR-014 §4.11). The two body-pipeline
22+
// stages + complete_request moved verbatim out of
23+
// detail/webserver_body_pipeline.cpp / webserver_request.cpp (which keep the
24+
// debug-dump free functions and the answer_to_connection trampoline /
25+
// resolve_method_callback / should_skip_auth respectively). Rewiring:
26+
// parent->config.* becomes config_.*, hook gates go through hooks_
27+
// (hook_dispatcher), and complete_request hands off to dispatcher_
28+
// (request_dispatcher). The redundant belt-and-suspenders `mr->ws = parent`
29+
// refresh in the post-processor helper is dropped (answer_to_connection
30+
// already set it and the comment noted it never changes the value).
31+
32+
#include "httpserver/detail/request_pipeline.hpp"
33+
34+
#include <microhttpd.h>
35+
#include <strings.h>
36+
37+
#include <chrono>
38+
#include <cstddef>
39+
#include <cstring>
40+
#include <iostream>
41+
#include <optional>
42+
#include <span>
43+
#include <string_view>
44+
#include <utility>
45+
46+
#include "httpserver/create_webserver.hpp"
47+
#include "httpserver/hook_action.hpp"
48+
#include "httpserver/hook_context.hpp"
49+
#include "httpserver/hook_phase.hpp"
50+
#include "httpserver/http_request.hpp"
51+
#include "httpserver/http_response.hpp"
52+
#include "httpserver/http_utils.hpp"
53+
#include "httpserver/detail/hook_dispatcher.hpp"
54+
#include "httpserver/detail/modded_request.hpp"
55+
#include "httpserver/detail/request_dispatcher.hpp"
56+
#include "httpserver/detail/webserver_impl.hpp"
57+
58+
namespace httpserver {
59+
60+
using httpserver::http::http_utils;
61+
62+
namespace detail {
63+
64+
namespace {
65+
66+
// Wrap the body_chunk firing site so requests_answer_second_step stays a
67+
// flat sequence of small steps. Returns true iff a hook short-circuited and
68+
// the caller should signal MHD the chunk was consumed. Side effects on
69+
// short-circuit: mr->response populated, mr->skip_handler set, any in-flight
70+
// post-processor destroyed.
71+
bool fire_and_maybe_short_circuit_body_chunk(hook_dispatcher& hooks,
72+
modded_request* mr,
73+
const char* upload_data,
74+
size_t upload_data_size) {
75+
// ctx.offset is sourced from body_bytes_seen (not get_content().size())
76+
// so it accumulates correctly even when put_processed_data_to_content is
77+
// false and a post-processor is active. ctx.is_final is hard-coded false:
78+
// end-of-body is signalled by MHD's zero-size upload callback, which
79+
// routes to complete_request and never reaches this fire site.
80+
::httpserver::body_chunk_ctx ctx{
81+
mr->request.get(),
82+
std::as_bytes(std::span<const char>(upload_data, upload_data_size)),
83+
mr->body_bytes_seen,
84+
/*is_final=*/false};
85+
mr->body_bytes_seen += upload_data_size;
86+
auto sc = hooks.fire_body_chunk(ctx);
87+
if (!sc) return false;
88+
mr->response.emplace(std::move(*sc));
89+
mr->skip_handler = true;
90+
if (mr->pp != nullptr) {
91+
MHD_destroy_post_processor(mr->pp);
92+
mr->pp = nullptr;
93+
}
94+
return true;
95+
}
96+
97+
// Feed @p upload_data through MHD's post processor (when one is attached)
98+
// and close any open upload-target stream.
99+
void run_post_processor_if_attached(modded_request* mr, const char* upload_data,
100+
size_t upload_data_size) {
101+
if (mr->pp == nullptr) return;
102+
MHD_post_process(mr->pp, upload_data, upload_data_size);
103+
if (mr->upload_ostrm != nullptr && mr->upload_ostrm->is_open()) {
104+
mr->upload_ostrm->close();
105+
}
106+
}
107+
108+
} // namespace
109+
110+
MHD_Result request_pipeline::requests_answer_first_step(
111+
MHD_Connection* connection, struct detail::modded_request* mr) {
112+
// The http_request constructor calls pick_resource(connection) internally
113+
// to locate the per-connection arena installed by connection_notify, then
114+
// allocates the http_request_impl from that arena.
115+
mr->request.reset(new http_request(connection, config_.unescaper));
116+
mr->request->set_file_cleanup_callback(config_.file_cleanup_callback);
117+
// Propagate the redaction-bypass bit so operator<< honours the builder
118+
// opt-in for every request the webserver dispatches.
119+
mr->request->set_expose_credentials_in_logs(
120+
config_.expose_credentials_in_logs);
121+
122+
// request_received hook. Fires after the http_request is populated but
123+
// before any body bytes are read (and before any post-processor is
124+
// created). Short-circuit: stash the response, mark skip-to-finalize, and
125+
// return MHD_YES; MHD calls back into second_step with
126+
// *upload_data_size == 0, which routes through complete_request ->
127+
// finalize_answer's skip_handler branch.
128+
if (hooks_.has_hooks_for(::httpserver::hook_phase::request_received)) {
129+
::httpserver::request_received_ctx ctx{
130+
mr->request.get(),
131+
std::chrono::steady_clock::now()};
132+
if (auto sc = hooks_.fire_request_received(ctx)) {
133+
mr->response.emplace(std::move(*sc));
134+
mr->skip_handler = true;
135+
return MHD_YES;
136+
}
137+
}
138+
139+
if (!mr->has_body) {
140+
return MHD_YES;
141+
}
142+
143+
mr->request->set_content_size_limit(config_.content_size_limit);
144+
const char *encoding = MHD_lookup_connection_value(connection,
145+
MHD_HEADER_KIND, http_utils::http_header_content_type);
146+
147+
if (config_.post_process_enabled &&
148+
(nullptr != encoding &&
149+
((0 == strncasecmp(http_utils::http_post_encoding_form_urlencoded, encoding, strlen(http_utils::http_post_encoding_form_urlencoded))) ||
150+
(0 == strncasecmp(http_utils::http_post_encoding_multipart_formdata, encoding, strlen(http_utils::http_post_encoding_multipart_formdata)))))) {
151+
const size_t post_memory_limit(32 * 1024); // Same as #MHD_POOL_SIZE_DEFAULT
152+
mr->pp = MHD_create_post_processor(connection, post_memory_limit, &webserver_impl::post_iterator, mr);
153+
} else {
154+
mr->pp = nullptr;
155+
}
156+
return MHD_YES;
157+
}
158+
159+
MHD_Result request_pipeline::requests_answer_second_step(
160+
MHD_Connection* connection, const char* method, const char* version,
161+
const char* upload_data, size_t* upload_data_size,
162+
struct detail::modded_request* mr) {
163+
if (0 == *upload_data_size) return complete_request(connection, mr, version, method);
164+
165+
if (!mr->has_body) {
166+
*upload_data_size = 0;
167+
return MHD_YES;
168+
}
169+
170+
// A prior pre-handler short-circuit (request_received in first_step, or
171+
// body_chunk on an earlier chunk) already populated mr->response. Consume
172+
// the chunk so MHD advances; the next *upload_data_size == 0 callback
173+
// routes to finalize_answer's skip_handler branch.
174+
if (mr->skip_handler) {
175+
*upload_data_size = 0;
176+
return MHD_YES;
177+
}
178+
179+
// body_chunk hook fires per chunk BEFORE the bytes are appended to
180+
// mr->request / fed to MHD_post_process.
181+
if (hooks_.has_hooks_for(::httpserver::hook_phase::body_chunk)) {
182+
if (fire_and_maybe_short_circuit_body_chunk(
183+
hooks_, mr, upload_data, *upload_data_size)) {
184+
*upload_data_size = 0;
185+
return MHD_YES;
186+
}
187+
}
188+
189+
// Raw request-body dump, opt-in via LIBHTTPSERVER_DEBUG_DUMP_REQUEST_BODY.
190+
// Default behaviour is silent on RELEASE *and* DEBUG builds. The
191+
// operator<< redaction policy does NOT cover this path: raw bytes are
192+
// written verbatim. See docs/debug-env-vars.md.
193+
if (debug_dump_request_body_opted_in()) {
194+
std::cout << "Writing content: ";
195+
std::cout.write(upload_data,
196+
static_cast<std::streamsize>(*upload_data_size));
197+
std::cout << std::endl;
198+
}
199+
// The post iterator is only created for multipart/form-data and
200+
// application/x-www-form-urlencoded; all other content (mr->pp == nullptr)
201+
// must be put to the content even if put_processed_data_to_content is false.
202+
if (mr->pp == nullptr || config_.put_processed_data_to_content) {
203+
mr->request->grow_content(upload_data, *upload_data_size);
204+
}
205+
run_post_processor_if_attached(mr, upload_data, *upload_data_size);
206+
207+
*upload_data_size = 0;
208+
return MHD_YES;
209+
}
210+
211+
MHD_Result request_pipeline::complete_request(MHD_Connection* connection,
212+
struct detail::modded_request* mr, const char* version,
213+
const char* method) {
214+
// mr->ws is pre-populated in answer_to_connection (hoisted there for
215+
// early-path request_completed coverage); no need to set it again here.
216+
mr->request->set_path(mr->standardized_url);
217+
mr->request->set_method(method);
218+
mr->request->set_version(version);
219+
220+
return dispatcher_.finalize_answer(connection, mr);
221+
}
222+
223+
} // namespace detail
224+
} // namespace httpserver

0 commit comments

Comments
 (0)