Skip to content

Commit dc3f141

Browse files
etrclaude
andcommitted
refactor(dr-014): extract upload_pipeline behavior service
Move multipart / file-upload handling off webserver_impl into detail::upload_pipeline: handle_post_form_arg, setup_new_upload_file_info, manage_upload_stream, process_file_upload, and the file branch of post_iterator (now iterate_file). Holds only const webserver_config& (file_upload_target / file_upload_dir / generate_random_filename_on_upload). post_iterator stays a static webserver_impl trampoline (its address is registered with MHD_create_post_processor) and forwards here: the no-file form-arg branch calls the static upload_pipeline::handle_post_form_arg so it works without an owning webserver (post_iterator_null_key_test feeds a null-ws modded_request), the file branch routes through impl_->upload_. upload_pipeline is granted friend access to http_request (set_arg / grow_last_arg / set_arg_flat) and http::file_info (the setters + grow_file_size), matching webserver_impl's prior access. 113/113 tests pass; cpplint/complexity/file-size gates green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 741faf4 commit dc3f141

9 files changed

Lines changed: 301 additions & 133 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/response_materializer.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_websocket.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/response_materializer.cpp detail/upload_pipeline.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_websocket.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/response_materializer.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/response_materializer.hpp httpserver/detail/upload_pipeline.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/upload_pipeline.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
// upload_pipeline behavior service (DR-014 §4.11). Logic moved verbatim out
22+
// of detail/webserver_callbacks.cpp (handle_post_form_arg /
23+
// setup_new_upload_file_info / manage_upload_stream / process_file_upload and
24+
// the file branch of post_iterator, here iterate_file). The post_iterator
25+
// static MHD trampoline stays on webserver_impl and forwards here. Rewiring:
26+
// parent->config.* becomes config_.*.
27+
28+
#include "httpserver/detail/upload_pipeline.hpp"
29+
30+
#include <microhttpd.h>
31+
#include <unistd.h>
32+
33+
#include <cstring>
34+
#include <fstream>
35+
#include <memory>
36+
#include <string>
37+
38+
#include "httpserver/create_webserver.hpp"
39+
#include "httpserver/file_info.hpp"
40+
#include "httpserver/http_request.hpp"
41+
#include "httpserver/http_utils.hpp"
42+
#include "httpserver/detail/modded_request.hpp"
43+
44+
namespace httpserver {
45+
46+
using httpserver::http::http_utils;
47+
48+
namespace detail {
49+
50+
MHD_Result upload_pipeline::handle_post_form_arg(detail::modded_request* mr,
51+
const char* key, const char* data, size_t size, uint64_t off) {
52+
// MHD may invoke the post iterator with a null key on a continuation
53+
// chunk (off > 0): the field name was supplied on the first call and is
54+
// not repeated. With no field name there is nothing to store the value
55+
// under, so silently accept the chunk (MHD_YES; MHD_NO would abort the
56+
// request). Guarding here also stops the raw pointer from reaching
57+
// std::string, which throws std::logic_error on null and aborts via
58+
// std::terminate (the throw escapes a C callback). See issue #375.
59+
if (key == nullptr) {
60+
return MHD_YES;
61+
}
62+
// A non-zero @p off means MHD is feeding a continuation chunk of a
63+
// previously-started value, so append rather than replace.
64+
if (off > 0) {
65+
mr->request->grow_last_arg(key, std::string(data, size));
66+
} else {
67+
mr->request->set_arg(key, std::string(data, size));
68+
}
69+
return MHD_YES;
70+
}
71+
72+
bool upload_pipeline::setup_new_upload_file_info(http::file_info& file,
73+
const char* filename, const char* content_type,
74+
const char* transfer_encoding) const {
75+
// First chunk for this (key, filename) pair: choose the on-disk
76+
// destination path (random if generate_random_filename_on_upload,
77+
// otherwise sanitize the client-supplied filename) and prime the
78+
// file_info with content_type / transfer_encoding when MHD gave them.
79+
if (config_.generate_random_filename_on_upload) {
80+
file.set_file_system_file_name(
81+
http_utils::generate_random_upload_filename(config_.file_upload_dir));
82+
} else {
83+
std::string safe_name = http_utils::sanitize_upload_filename(filename);
84+
if (safe_name.empty()) return false;
85+
file.set_file_system_file_name(config_.file_upload_dir + "/" + safe_name);
86+
}
87+
// Avoid appending to a leftover file from a previous request.
88+
unlink(file.get_file_system_file_name().c_str());
89+
if (content_type != nullptr) file.set_content_type(content_type);
90+
if (transfer_encoding != nullptr) file.set_transfer_encoding(transfer_encoding);
91+
return true;
92+
}
93+
94+
void upload_pipeline::manage_upload_stream(detail::modded_request* mr,
95+
const char* filename, const char* key, http::file_info& file) {
96+
// If MHD switches us to a different (filename, key) pair, close the
97+
// previous output stream. The four-way OR covers fresh state (both
98+
// tracking strings empty) and either coordinate changing.
99+
if (mr->upload_filename.empty()
100+
|| mr->upload_key.empty()
101+
|| strcmp(filename, mr->upload_filename.c_str()) != 0
102+
|| strcmp(key, mr->upload_key.c_str()) != 0) {
103+
if (mr->upload_ostrm != nullptr) mr->upload_ostrm->close();
104+
}
105+
// Open a stream when we don't already have one (first chunk, or
106+
// just-closed above).
107+
if (mr->upload_ostrm == nullptr || !mr->upload_ostrm->is_open()) {
108+
mr->upload_key = key;
109+
mr->upload_filename = filename;
110+
mr->upload_ostrm = std::make_unique<std::ofstream>();
111+
mr->upload_ostrm->open(file.get_file_system_file_name(),
112+
std::ios::binary | std::ios::app);
113+
}
114+
}
115+
116+
MHD_Result upload_pipeline::process_file_upload(detail::modded_request* mr,
117+
const char* key, const char* filename, const char* content_type,
118+
const char* transfer_encoding, const char* data, size_t size) const {
119+
http::file_info& file = mr->request->get_or_create_file_info(key, filename);
120+
if (file.get_file_system_file_name().empty()) {
121+
if (!setup_new_upload_file_info(file, filename, content_type,
122+
transfer_encoding)) {
123+
return MHD_NO;
124+
}
125+
}
126+
manage_upload_stream(mr, filename, key, file);
127+
if (size > 0) {
128+
mr->upload_ostrm->write(data, size);
129+
if (!mr->upload_ostrm->good()) return MHD_NO;
130+
}
131+
file.grow_file_size(size);
132+
return MHD_YES;
133+
}
134+
135+
MHD_Result upload_pipeline::iterate_file(detail::modded_request* mr,
136+
const char* key, const char* filename, const char* content_type,
137+
const char* transfer_encoding, const char* data, size_t size) {
138+
try {
139+
if (config_.file_upload_target != FILE_UPLOAD_DISK_ONLY) {
140+
mr->request->set_arg_flat(key,
141+
std::string(mr->request->get_arg(key)) + std::string(data, size));
142+
}
143+
if (*filename != '\0'
144+
&& config_.file_upload_target != FILE_UPLOAD_MEMORY_ONLY) {
145+
MHD_Result r = process_file_upload(
146+
mr, key, filename, content_type, transfer_encoding, data, size);
147+
if (r != MHD_YES) return r;
148+
}
149+
return MHD_YES;
150+
} catch (const http::generateFilenameException&) {
151+
return MHD_NO;
152+
}
153+
}
154+
155+
} // namespace detail
156+
} // namespace httpserver

src/detail/webserver_callbacks.cpp

Lines changed: 12 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
#include "httpserver/detail/http_endpoint.hpp"
6666
#include "httpserver/detail/lambda_resource.hpp"
6767
#include "httpserver/detail/modded_request.hpp"
68+
#include "httpserver/detail/upload_pipeline.hpp"
6869
#include "httpserver/http_request.hpp"
6970
#include "httpserver/http_resource.hpp"
7071
#include "httpserver/http_response.hpp"
@@ -362,94 +363,14 @@ size_t webserver_impl::unescaper_func(void * cls, struct MHD_Connection *c, char
362363
return std::char_traits<char>::length(s);
363364
}
364365

365-
MHD_Result webserver_impl::handle_post_form_arg(detail::modded_request* mr,
366-
const char* key, const char* data, size_t size, uint64_t off) {
367-
// MHD may invoke the post iterator with a null key on a continuation
368-
// chunk (off > 0): the field name was supplied on the first call and
369-
// is not repeated. With no field name there is nothing to store the
370-
// value under, so silently accept the chunk (MHD_YES tells MHD to
371-
// continue; MHD_NO would abort the whole request). Guarding here also
372-
// stops the raw pointer from reaching std::string, which throws
373-
// std::logic_error on null and aborts the process via std::terminate
374-
// because the throw escapes a C callback. See issue #375 (same class
375-
// of bug as the null-uri fix in uri_log, issue #371).
376-
if (key == nullptr) {
377-
return MHD_YES;
378-
}
379-
// No file: set the arg key/value and return. A non-zero @p off
380-
// means MHD is feeding us a continuation chunk of a previously-
381-
// started value, so append rather than replace.
382-
if (off > 0) {
383-
mr->request->grow_last_arg(key, std::string(data, size));
384-
} else {
385-
mr->request->set_arg(key, std::string(data, size));
386-
}
387-
return MHD_YES;
388-
}
389-
390-
bool webserver_impl::setup_new_upload_file_info(http::file_info& file,
391-
const char* filename, const char* content_type,
392-
const char* transfer_encoding) const {
393-
// First chunk for this (key, filename) pair: choose the on-disk
394-
// destination path (random if generate_random_filename_on_upload,
395-
// otherwise sanitize the client-supplied filename) and prime the
396-
// file_info with content_type / transfer_encoding when MHD gave
397-
// them to us.
398-
if (parent->config.generate_random_filename_on_upload) {
399-
file.set_file_system_file_name(
400-
http_utils::generate_random_upload_filename(parent->config.file_upload_dir));
401-
} else {
402-
std::string safe_name = http_utils::sanitize_upload_filename(filename);
403-
if (safe_name.empty()) return false;
404-
file.set_file_system_file_name(parent->config.file_upload_dir + "/" + safe_name);
405-
}
406-
// Avoid appending to a leftover file from a previous request.
407-
unlink(file.get_file_system_file_name().c_str());
408-
if (content_type != nullptr) file.set_content_type(content_type);
409-
if (transfer_encoding != nullptr) file.set_transfer_encoding(transfer_encoding);
410-
return true;
411-
}
412-
413-
void webserver_impl::manage_upload_stream(detail::modded_request* mr,
414-
const char* filename, const char* key, http::file_info& file) {
415-
// If MHD switches us to a different (filename, key) pair, close the
416-
// previous output stream. The four-way OR covers fresh state (both
417-
// tracking strings empty) and either coordinate changing.
418-
if (mr->upload_filename.empty()
419-
|| mr->upload_key.empty()
420-
|| strcmp(filename, mr->upload_filename.c_str()) != 0
421-
|| strcmp(key, mr->upload_key.c_str()) != 0) {
422-
if (mr->upload_ostrm != nullptr) mr->upload_ostrm->close();
423-
}
424-
// Open a stream when we don't already have one (first chunk, or
425-
// just-closed above).
426-
if (mr->upload_ostrm == nullptr || !mr->upload_ostrm->is_open()) {
427-
mr->upload_key = key;
428-
mr->upload_filename = filename;
429-
mr->upload_ostrm = std::make_unique<std::ofstream>();
430-
mr->upload_ostrm->open(file.get_file_system_file_name(),
431-
std::ios::binary | std::ios::app);
432-
}
433-
}
434-
435-
MHD_Result webserver_impl::process_file_upload(detail::modded_request* mr,
436-
const char* key, const char* filename, const char* content_type,
437-
const char* transfer_encoding, const char* data, size_t size) const {
438-
http::file_info& file = mr->request->get_or_create_file_info(key, filename);
439-
if (file.get_file_system_file_name().empty()) {
440-
if (!setup_new_upload_file_info(file, filename, content_type, transfer_encoding)) {
441-
return MHD_NO;
442-
}
443-
}
444-
manage_upload_stream(mr, filename, key, file);
445-
if (size > 0) {
446-
mr->upload_ostrm->write(data, size);
447-
if (!mr->upload_ostrm->good()) return MHD_NO;
448-
}
449-
file.grow_file_size(size);
450-
return MHD_YES;
451-
}
452-
366+
// MHD post-iterator trampoline. Registered with MHD_create_post_processor
367+
// (webserver_body_pipeline.cpp); its address is taken, so it stays a static
368+
// webserver_impl member. The upload logic moved to the upload_pipeline
369+
// behavior service (DR-014 §4.11). The no-file form-arg branch uses the
370+
// static upload_pipeline::handle_post_form_arg so it is reachable without an
371+
// owning webserver (post_iterator_null_key_test feeds a null-ws
372+
// modded_request); the file branch routes through the owning webserver's
373+
// upload_ instance.
453374
MHD_Result webserver_impl::post_iterator(void *cls, enum MHD_ValueKind kind,
454375
const char *key, const char *filename, const char *content_type,
455376
const char *transfer_encoding, const char *data, uint64_t off, size_t size) {
@@ -458,23 +379,10 @@ MHD_Result webserver_impl::post_iterator(void *cls, enum MHD_ValueKind kind,
458379
auto* mr = static_cast<detail::modded_request*>(cls);
459380

460381
if (!filename) {
461-
return handle_post_form_arg(mr, key, data, size, off);
462-
}
463-
464-
try {
465-
if (mr->ws->config.file_upload_target != FILE_UPLOAD_DISK_ONLY) {
466-
mr->request->set_arg_flat(key,
467-
std::string(mr->request->get_arg(key)) + std::string(data, size));
468-
}
469-
if (*filename != '\0' && mr->ws->config.file_upload_target != FILE_UPLOAD_MEMORY_ONLY) {
470-
MHD_Result r = mr->ws->impl_->process_file_upload(
471-
mr, key, filename, content_type, transfer_encoding, data, size);
472-
if (r != MHD_YES) return r;
473-
}
474-
return MHD_YES;
475-
} catch (const http::generateFilenameException&) {
476-
return MHD_NO;
382+
return upload_pipeline::handle_post_form_arg(mr, key, data, size, off);
477383
}
384+
return mr->ws->impl_->upload_.iterate_file(
385+
mr, key, filename, content_type, transfer_encoding, data, size);
478386
}
479387

480388
} // namespace detail

0 commit comments

Comments
 (0)