Skip to content

work#261

Merged
vinniefalco merged 20 commits intocppalliance:developfrom
vinniefalco:develop
Feb 7, 2026
Merged

work#261
vinniefalco merged 20 commits intocppalliance:developfrom
vinniefalco:develop

Conversation

@vinniefalco
Copy link
Member

@vinniefalco vinniefalco commented Feb 7, 2026

Summary by CodeRabbit

  • New Features

    • JSON body middleware for automatic JSON parsing
    • Content negotiation utilities (type, encoding, charset, language)
    • Compile-time schema/ORM helpers and directory-listing middleware
    • New HTTP worker for request/response sessions
  • API Changes

    • Router redesigned to a templated, fluent/middleware interface with richer dispatch and transforms
    • Serializer API reworked to streaming-first semantics
    • Route parameter and handler interfaces expanded
  • Removals

    • Legacy router variants and legacy bcrypt header-based surfaces removed
  • Bug Fixes

    • Parser now reports EOF when body is already complete

Replace the old start(m, buffers) buffer-sequence API and never-implemented
source mechanism with a clean two-mode sink: WriteSink (caller-owned buffers
via write/write_eof) and BufferSink (zero-copy via prepare/commit/commit_eof).
Add set_message() for decoupled message association, start_writes() and
start_buffers() entry points with lazy start from the sink, and remove
cbs_gen, max_type_erase, and all message-taking overloads.
Eliminate the two-step router+flat_router workflow by merging the
flattened dispatch directly into any_router. Routes are now inlined
into contiguous arrays at registration time, so dispatch is always
cache-friendly without a separate flattening step.
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Reworks routing into a templated router and public route_params, consolidates bcrypt headers, adds json_body/accepts/serve_index/http_worker, refactors serializer/parser streaming, introduces a compile-time DB schema, and removes legacy flat/basic router files and several bcrypt headers.

Changes

Cohort / File(s) Summary
Routing core removed
include/boost/http/server/basic_router.hpp, include/boost/http/server/flat_router.hpp, src/server/flat_router.cpp, src/server/detail/router_base.cpp
Deleted legacy basic/flat router headers and implementations; large public API removals and impl deletions.
New router template & impl
include/boost/http/server/router.hpp, src/server/any_router.cpp, src/server/detail/any_router.hpp, include/boost/http/server/detail/router_base.hpp
Added templated router<P,HT>, router_options, fluent_route, and new router_base dispatch/allow-header/finalization logic.
Route handler / params
include/boost/http/server/route_handler.hpp, include/boost/http/server/detail/dynamic_invoke.hpp, src/server/detail/route_match.*, src/server/router_types.cpp
Promoted route_params to public API, changed matcher/operator() signatures to use route_params, and added dynamic_invoke for polystore-based arg resolution.
Middleware & server utilities
include/boost/http/json/json_body.hpp, src/json/json_body.cpp, include/boost/http/server/accepts.hpp, src/server/accepts.cpp, include/boost/http/server/serve_index.hpp, src/server/serve_index.cpp, include/boost/http/server/http_worker.hpp, src/server/http_worker.cpp
Added JSON body middleware, content negotiation (accepts), directory index middleware, and an http_worker with coroutine-based session loop.
Serializer / parser refactor
include/boost/http/serializer.hpp, include/boost/http/impl/serializer.hpp, src/serializer.cpp, include/boost/http/impl/serializer.hpp, include/boost/http/parser.hpp
Removed cbs_gen/buffers path, changed serializer lifecycle (start/stream/write APIs), introduced lazy streaming modes and write/write_eof overloads; parser returns EOF when body already complete.
Bcrypt consolidation
include/boost/http/bcrypt.hpp, removed include/boost/http/bcrypt/*, src/bcrypt/*
Consolidated multiple bcrypt headers into a single public header; removed separate bcrypt headers and updated sources to include the consolidated header.
JSON sink tweaks
include/boost/http/json/json_sink.hpp
Introduced write_impl helper, added write_some() and write_eof(buffers) overloads; refactored write() to delegate.
DB schema system
include/boost/http/db/schema.hpp
Added a compile-time ORM-like schema: field descriptors, embed/has_one/has_many, tag_invoke-based table_name/fields, and for_each_field/field_count utilities.
Config/API removals
include/boost/http/config.hpp, src/config.cpp
Removed max_type_erase from parser/serializer configs and eliminated its space accounting in config construction.
Includes & cosmetics
many include/*, src/*, .github/workflows/ci.yml
Replaced several router.hpp includes with route_handler.hpp, standardized copyright email formatting, removed GCC 12 CI job. Many small comment-only edits.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Worker as http_worker
    participant Parser as request_parser
    participant Router as router
    participant Middleware as json_body
    participant Handler as Handler
    participant Serializer as serializer

    Client->>Worker: TCP request bytes
    Worker->>Parser: read_header() / parse request
    Parser-->>Worker: parsed request, url, headers
    Worker->>Router: dispatch(method, url, route_params)
    Router->>Middleware: invoke(route_params) (if matched)
    Middleware->>Parser: push body into json_sink
    Middleware-->>Router: store parsed json in route_data / next
    Router->>Handler: call resolved handler (dynamic_invoke may resolve args)
    Handler->>route_params: status(...) / send(body)
    route_params->>Serializer: write/write_eof(body)
    Serializer->>Client: write response bytes
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through routes and headers bright and new,
I parsed the body and nudged the router through,
I bundled bcrypt and tidied streams in line,
A worker hums and indexes list by design,
The rabbit stamped this patch — a tiny patch of dew.

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The pull request title 'work' is vague and generic, providing no meaningful information about the changeset's actual scope or intent. Replace with a descriptive title that summarizes the main changes, such as 'Refactor router API and consolidate bcrypt headers' or 'Restructure HTTP routing and async bcrypt operations'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cppalliance-bot
Copy link

cppalliance-bot commented Feb 7, 2026

An automated preview of the documentation is available at https://261.http.prtest3.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-02-07 22:03:20 UTC

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/config.cpp (1)

35-42: ⚠️ Potential issue | 🟡 Minor

Stale layout diagram — T is no longer allocated.

The ASCII layout still shows a T region (| fb | cb0 | cb1 | T | f |), but max_type_erase has been removed from the space calculation and T has no legend entry. This will mislead future readers about what the buffer actually contains.

Suggested fix
-    /*
-        | fb |     cb0     |     cb1     | T | f |
-
-        fb  flat_dynamic_buffer         headers.max_size
-        cb0 circular_buffer     min_buffer
-        cb1 circular_buffer     min_buffer
-        f   table               max_table_space
-    */
+    /*
+        | fb |     cb0     |     cb1     | f |
+
+        fb  flat_dynamic_buffer         headers.max_size
+        cb0 circular_buffer     min_buffer
+        cb1 circular_buffer     min_buffer
+        f   table               max_table_space
+    */

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works… Warn if… the overview appears stale relative to the code."

include/boost/http/parser.hpp (1)

467-476: ⚠️ Potential issue | 🟡 Minor

Docstring for pull doesn't mention capy::error::eof on body completion.

Line 610 now returns capy::error::eof when the body is complete, but the docstring at lines 471 only mentions "returns an empty span" without documenting the error code. The read method's docstring (line 347) correctly documents this behavior — pull should be consistent.

Suggested docstring update
     /** Pull buffer data from the body.

         On first invocation, reads headers if not yet parsed.
         Returns buffer descriptors pointing to internal parser
-        memory. When the body is complete, returns an empty span.
+        memory. When the body is complete, returns
+        `capy::error::eof` with an empty span.

         `@param` dest Span of const_buffer to fill.

         `@return` An awaitable yielding `(error_code,std::span<const_buffer>)`.
     */

As per coding guidelines, "Docstrings should include a brief description of purpose, document all parameters, return values, and any preconditions, postconditions, or exceptions."

src/server/mime_types.cpp (1)

28-81: ⚠️ Potential issue | 🔴 Critical

"7z" is out of sort order — binary search will never find it.

The array is documented as "sorted by extension for binary search," but "7z" is placed last. Since '7' (0x37) precedes 'a' (0x61) in ASCII (and compare_icase doesn't change digit ordering), "7z" must be the first entry. As-is, lookup("file.7z") will silently return an empty string.

🐛 Proposed fix
 constexpr ext_entry ext_db[] = {
+    { "7z", "application/x-7z-compressed" },
     { "aac", "audio/aac" },
     { "avif", "image/avif" },
     ...
     { "xml", "application/xml" },
     { "zip", "application/zip" },
-    { "7z", "application/x-7z-compressed" },
 };
src/server/detail/any_router.hpp (1)

57-68: ⚠️ Potential issue | 🔴 Critical

noexcept on a constructor that allocates a std::string — will std::terminate on allocation failure.

The third entry constructor is marked noexcept, but verb_str = verb_str_ (line 67) copies a std::string_view into a std::string, which allocates memory and can throw std::bad_alloc. If that happens, std::terminate will be called.

🐛 Proposed fix: remove noexcept
-    // string verb match
     entry(
         std::string_view verb_str_,
-        handler_ptr h_) noexcept
+        handler_ptr h_)
         : h(std::move(h_))
         , verb(http::string_to_method(verb_str_))
         , all(false)
src/serializer.cpp (1)

273-280: ⚠️ Potential issue | 🟡 Minor

Remove the unused two-argument impl constructor at lines 273–280.

The constructor impl(cfg, message_base const& msg) is never called. The codebase instantiates impl only with a single argument (line 765), and the message is set separately via set_message() (line 782). This dead code should be removed.

include/boost/http/server/route_handler.hpp (1)

270-282: ⚠️ Potential issue | 🟠 Major

Remove noexcept from route_error or change the contract to precondition.

Line 279 declares route_error(system::error_code ec) noexcept, but its docstring at line 277 claims @throw std::invalid_argument if !ec. The private constructor implementation (in src/server/router_types.cpp lines 85–92) confirms it does throw std::invalid_argument when the error code does not represent a failure. A noexcept function that throws will call std::terminate() instead of propagating the exception, violating the semantic contract of noexcept. Either remove the noexcept specifier or change the documentation to specify a precondition (e.g., "requires ec to be a failing code").

🤖 Fix all issues with AI agents
In `@include/boost/http/db/schema.hpp`:
- Around line 413-437: for_each_field and field_count call tag_invoke(fields,
T{}) which requires T be default-constructible, but HasMapping doesn't enforce
that; constrain these utilities to only accept default-initializable types by
adding std::default_initializable<T> (or equivalent) to the template requirement
list for template<HasMapping T, ...> in for_each_field and template<HasMapping
T> in field_count so the compiler will reject non-default-constructible T
instead of producing a hard-to-diagnose error when constructing T{}.

In `@include/boost/http/json/json_body.hpp`:
- Around line 32-39: The Content-Type check in the json_body middleware is too
permissive because it uses starts_with("application/json") and will match media
types like "application/json-patch+json"; update the check in the json_body
handler (the routine that verifies request method and content type before
creating a json_sink and storing json::value in the datastore) to accept
"application/json" only when it is followed by end-of-string, a semicolon, or
whitespace (i.e., parse the header token and allow optional parameters),
otherwise yield to the next handler; keep the rest of the flow (reading into
json_sink and storing under the "json::value" key) unchanged.

In `@include/boost/http/server/detail/router_base.hpp`:
- Line 28: Remove the stale forward declaration "template<class> class router;"
and replace nothing; instead rely on the existing correct two-parameter template
declaration of router (template<class P = route_params, class HT = identity>
class router) that the friend declaration already references; locate and delete
the single-parameter forward declaration in router_base.hpp so only the correct
router template (as defined in the router.hpp header) is visible and there are
no conflicting/unused forward declarations.

In `@include/boost/http/server/http_worker.hpp`:
- Around line 43-71: The example in the class docstring is stale: it calls
http_worker(router, parser_cfg, serializer_cfg) but the actual constructor is
http_worker(Stream& stream_, router, parser_cfg, serializer_cfg) and it already
wires rp.req_body, rp.res_body and stream; update the example's my_worker
constructor to call http_worker(sock, router, parser_cfg, serializer_cfg) (or
otherwise pass the stream object as the first argument) and remove the manual
assignments to rp.req_body, rp.res_body, and stream so the example compiles and
reflects the real http_worker(Stream& ...) constructor and behavior.
- Around line 10-11: The include guard macro is misnamed; change the opening
macro from BOOST_HTTP_SERVER_WORKER_HPP to BOOST_HTTP_SERVER_HTTP_WORKER_HPP and
update the corresponding closing `#endif` comment (if present) to match; locate
the macros in http_worker.hpp (the existing BOOST_HTTP_SERVER_WORKER_HPP) and
replace both the `#ifndef/`#define and the trailing `#endif` comment to use
BOOST_HTTP_SERVER_HTTP_WORKER_HPP so the guard matches the filename and Boost
naming conventions.
- Around line 89-110: The constructor docblock and template constraint need
updating: add a `@param` entry describing the first parameter stream_ (the stream
used for both input and output) and change the template constraint on
http_worker from template<capy::WriteStream Stream> to require both read and
write capabilities (e.g., use a requires-clause or combined concept so Stream
satisfies both capy::ReadStream and capy::WriteStream); this ensures
parser.source_for(stream_) and serializer.sink_for(stream_) are valid.
Reference: the http_worker constructor, the template parameter Stream, and the
calls parser.source_for(stream_) and serializer.sink_for(stream_).

In `@include/boost/http/server/route_handler.hpp`:
- Around line 478-483: The code creates a "soft slash" view by computing
p.priv_.decoded_path_.data() + p.priv_.decoded_path_.size() - 1 which is
undefined when decoded_path_ is empty; update the logic in the route handler
that sets p.path to first check p.priv_.decoded_path_.empty() (or assert
!empty()) and handle the empty case by assigning the literal "/" view (or
otherwise avoid subtracting from size()), otherwise perform the current pointer
arithmetic; reference p.path and p.priv_.decoded_path_ (and the block currently
asserting BOOST_ASSERT(p.path == "/")) when making the change.

In `@include/boost/http/server/router.hpp`:
- Around line 619-625: The converting constructor template for router
incorrectly requires std::derived_from<OtherP, P>, allowing constructing
router<P> from router<Derived> which leads to unsafe static_casts in handler
invoke(); change the constraint on the template<class OtherP> constructor to
require std::derived_from<P, OtherP> (i.e. P derives from OtherP) so only
routers whose params type is a base of P can be moved into router<P>, and verify
the constructor initializer still delegates to
detail::router_base(std::move(other)) and keeps noexcept semantics.

In `@src/serializer.cpp`:
- Around line 794-810: The two methods serializer::start_stream and
serializer::start_writes are identical but intended as distinct entry points;
either implement start_writes to call the correct backend (e.g.,
impl_->start_writes(*impl_->msg_) or another appropriate method on impl_) or, if
this synonymy is intentional during a transition, add a clear comment above
serializer::start_writes stating it intentionally delegates to
impl_->start_stream and why; update the call site accordingly and ensure the
unique symbols serializer::start_writes and impl_->start_writes (or the correct
impl method) are used to make the intent explicit.

In `@src/server/any_router.cpp`:
- Around line 36-37: The build_allow_header function currently returns a
hardcoded string when methods == ~0ULL which omits verbs in the known[] array
and any custom verbs; change it to build the Allow header dynamically: when
methods == ~0ULL treat it as "include all known methods" by iterating the
known[] array (and the custom verbs collection used elsewhere) to append every
known method name plus any registered custom verbs, join them with commas, and
return that string so CONNECT, TRACE, PROPFIND and custom verbs are included;
ensure you use the same known[] and custom storage identifiers as in the file so
behavior matches non-wildcard bitmask construction.
- Around line 614-623: The OPTIONS "*" early-return is invoking
impl_->options_handler_->invoke with p before p (route_params) is initialized;
move the route_params initialization (the block that sets p.kind_, p.verb_,
p.decoded_path_, p.params, etc.) to occur before the OPTIONS '*' check so p is
fully populated when passed to impl_->options_handler_->invoke, and apply the
same reordering in the dispatch overload that takes std::string_view verb (the
other dispatch block that calls impl_->options_handler_->invoke) so both code
paths initialize route_params prior to calling the options handler.

In `@src/server/http_worker.cpp`:
- Around line 66-73: The code calls rv.value() unconditionally after
urls::parse_uri_reference(rp.req.target()), which will throw if rv.has_error();
update the error path in the parsing block so that after setting
rp.status(http::status::bad_request) you skip using rv (e.g., send the response
and continue or return from the surrounding loop/coroutine) and only call rp.url
= rv.value() when rv.has_value() (or !rv.has_error()); locate the parsing block
around urls::parse_uri_reference, the local variable rv, and the
rp.status/rp.url assignments and ensure control flow exits the error case before
accessing rv.value().

In `@src/server/serve_index.cpp`:
- Around line 399-410: The canonicalization block for root_canonical and
dir_canonical silently ignores errors via a reused ec2, which can leave
non-canonical paths and make the show_up decision incorrect; update the code in
serve_index.cpp to use separate std::error_code variables for each
std::filesystem::canonical call (e.g., ec_root and ec_dir), check each error
after its call, and if either canonicalization fails set show_up = false (or
otherwise short-circuit the parent-link logic) before comparing dir_canonical
and root_canonical; ensure you still respect impl_->opts.show_parent when
deciding the final value of show_up.
- Around line 366-395: The code constructs a std::filesystem::directory_iterator
named it but then creates a second iterator in the for loop, causing the
directory to be opened twice and the error code from the second open to be
ignored; replace the second iterator with the already-created it (and use it in
the for loop) so you only open the directory once and check fec before
iterating, keeping the existing behavior around skipping hidden files,
populating dir_entry (e.name, e.is_dir, e.size, e.mtime) and pushing into
entries.
- Around line 230-244: The JSON string escaping in the loop that builds body
(see the loop iterating over e.name and calls like body.append and
body.push_back) only handles '"', '\\', '\n', '\r', '\t' and leaves other
control characters U+0000–U+001F unescaped; update that loop to escape all
control characters per RFC 8259 by mapping '\b' and '\f' to "\\b" and "\\f" and
encoding any other control codepoints (<= 0x1F) as "\\u00XX" (hex-escaped)
before appending to body so the generated JSON is always valid.
🧹 Nitpick comments (32)
src/server/fresh.cpp (1)

10-16: Add a high-level overview comment after the includes.

This file contains non-trivial implementation logic for HTTP cache freshness validation (ETag matching with weak/strong validator handling, Last-Modified date comparison). A block comment after the includes should explain the overall strategy, the simplifications made (e.g., lexicographic date comparison, simple ETag parsing), and reference to the HTTP caching specification being implemented (RFC 7232 freshness validation).

As per coding guidelines: "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works. This should highlight anything tricky or non-obvious that a maintainer might need to know in order to reason effectively about the code."

📝 Example overview comment
 `#include` <boost/http/server/fresh.hpp>
 `#include` <boost/http/field.hpp>
 
+/*
+    Implementation of HTTP cache freshness validation per RFC 7232.
+    
+    The is_fresh() function checks if a cached response can be reused based on
+    conditional request headers (If-None-Match, If-Modified-Since) and response
+    validators (ETag, Last-Modified).
+    
+    Strategy:
+    - ETag matching is checked first as the stronger validator
+    - Falls back to Last-Modified date comparison if ETags not present
+    - Handles weak/strong ETag distinction with W/ prefix stripping
+    
+    Simplifications:
+    - ETag matching uses simple substring search, not full list parsing
+    - Date comparison is lexicographic (works for RFC 7231 format dates)
+    - Does not fully parse comma-separated ETag lists
+*/
 namespace boost {
 namespace http {
include/boost/http/server/detail/dynamic_invoke.hpp (2)

13-18: Missing high-level overview block comment after includes.

This file implements non-trivial template metaprogramming (type-uniqueness checking, polystore-driven argument resolution, coroutine handler wrapping). A /* */ block comment after the includes explaining the overall design—how callables are resolved from a polystore, the role of find_key_t, and why duplicate types are disallowed—would help maintainers reason about this code.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."


37-40: Nit: consider std::remove_cvref_t (C++20).

Given the project already uses C++20 features (coroutines, fold expressions, std::bool_constant, etc.), std::remove_cvref_t<T> is a direct drop-in replacement for std::remove_cv_t<std::remove_reference_t<T>>.

♻️ Suggested simplification
 template<class T>
 using find_key_t =
-    std::remove_cv_t<
-        std::remove_reference_t<T>>;
+    std::remove_cvref_t<T>;
src/server/mime_types.cpp (1)

10-14: Consider adding a brief overview block comment after the includes.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."

include/boost/http/json/json_sink.hpp (1)

115-129: write_some and write are functionally identical — document the intended semantic difference.

Both write_some and write delegate to write_impl(buffers, false). Since JSON parsing is synchronous and always consumes all provided data, they behave identically. If the distinction exists purely to satisfy the WriteSink concept's surface area, a brief inline comment on write_some (e.g., "// satisfies WriteSink; equivalent to write() for synchronous sinks") would help future maintainers understand why both exist.

include/boost/http/db/schema.hpp (1)

1-18: Missing high-level overview comment and repository URL.

Per coding guidelines, files with non-trivial implementation logic should include a /* */ block comment after the includes providing a high-level overview. This file introduces a complete compile-time schema system with several interacting abstractions (fields, embeds, relationships, CPOs, concepts). A brief architectural overview would help maintainers orient themselves.

Also, unlike other headers in the project, the copyright block omits the // Official repository: ... line.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."

src/server/router_types.cpp (1)

9-17: Missing implementation overview block comment.

This file contains non-trivial logic (error category definition, route_result, route_params::is_method, route_params::send). Consider adding a /* */ block comment after the includes summarizing the purpose and key behaviors. As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview".

src/json/json_body.cpp (1)

9-16: Missing implementation overview block comment.

This file implements a coroutine-based JSON body parser middleware. A brief /* */ block comment after the includes would help future maintainers understand the flow (POST+JSON content-type guard → stream to json_sink → store in route_data). As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview".

src/server/http_worker.cpp (3)

52-53: std::cerr in library code is not ideal for production.

Library code typically should not write to stderr directly — callers have no control over this output. Consider routing through a configurable logging mechanism or the error code propagation path.


22-46: Guard destructor duplicates the per-iteration reset at the top of the loop.

The guard's destructor (lines 31-36) performs parser.reset(); parser.start(); rp.session_data.clear();, which is identical to lines 44-46 at the start of each loop iteration. The guard provides safety for mid-coroutine teardown, which is good, but the duplication is worth a brief inline comment explaining why both exist (the guard covers abnormal exit / cancellation).


9-14: Missing implementation overview block comment.

This coroutine drives the entire HTTP session lifecycle (read-header → dispatch → response loop). A /* */ overview after the includes would help a maintainer quickly understand the flow, especially the guard's role and the URL-parse → dispatch sequence. As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview".

include/boost/http/server/serve_index.hpp (1)

39-93: Consider documenting thread-safety guarantees.

Other classes in this PR (e.g., http_worker) document thread-safety ("Distinct objects: Safe. Shared objects: Unsafe."). Since serve_index holds mutable state behind a pointer and is const-callable via operator(), users would benefit from knowing whether it's safe to share across threads. As per coding guidelines, "Document thread-safety guarantees and exception specifications where applicable, particularly for classes intended for concurrent use".

include/boost/http/server/http_worker.hpp (1)

82-87: Public data members lack documentation.

The five public data members (fr, rp, stream, parser, serializer) are undocumented. Since this is a public header and users interact with these directly (as shown in the example), brief /// doc comments would help clarify ownership semantics and expected usage. As per coding guidelines, "Docstrings are required for all classes and functions in public headers in non-detail namespaces".

Proposed addition
 public:
+    /// The router for dispatching requests to handlers.
     http::router<route_params> fr;
+    /// Per-request state: request, response, and route data.
     http::route_params rp;
+    /// The underlying read stream for the connection.
     capy::any_read_stream stream;
+    /// The HTTP request parser.
     http::request_parser parser;
+    /// The HTTP response serializer.
     http::serializer serializer;
include/boost/http/server/accepts.hpp (1)

22-41: Well-documented public API.

The class-level and per-method documentation is thorough — parameters, return values, and behavior for edge cases (empty Accept header, no match) are all covered. One minor omission per the coding guidelines: the class stores a const& to fields_base, so a brief thread-safety and lifetime note would be helpful (e.g., "the caller must ensure fields outlives this object; const member functions may be called concurrently").

As per coding guidelines, "Document thread-safety guarantees and exception specifications where applicable, particularly for classes intended for concurrent use or functions that may throw."

src/server/serve_index.cpp (2)

26-55: path_cat is duplicated from src/server/serve_static.cpp.

The relevant code snippets show an identical path_cat function in src/server/serve_static.cpp (lines 23-51). Consider extracting it into a shared internal header to avoid maintaining two copies.


10-19: Missing implementation overview block comment.

This file contains non-trivial implementation logic (path assembly, directory reading, content negotiation, multiple rendering formats) but lacks a /* */ block comment after the includes providing a high-level overview for maintainers.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."

src/serializer.cpp (1)

622-632: is_done() and is_start() have identical bodies.

Both return state_ == state::start. While they serve different semantic purposes at the API level (checking "has serialization completed" vs. "has serialization not yet begun"), having identical implementations means they cannot be distinguished in practice. If the state machine genuinely resets to start after completion, consider adding a brief inline comment explaining why both map to the same state.

src/server/detail/any_router.hpp (1)

109-114: const_cast for lazy finalization is functional but fragile.

The const_cast<impl*>(this)->finalize_pending() pattern inside std::call_once works because the impl object is never truly const (it's held by shared_ptr<impl>), but it bypasses const-correctness guarantees. Since finalized_ is already mutable, consider making finalize_pending() itself callable on a const impl (or making ensure_finalized() non-const and adjusting call sites via the shared_ptr).

src/server/accepts.cpp (1)

10-14: Missing implementation overview block comment.

This file implements a full HTTP content negotiation engine with multiple parsing strategies (media types, simple tokens, language prefixes) and a generic negotiation template. A brief /* */ block comment after the includes summarizing the approach (parse Accept-* headers into scored entries, then negotiate against offered values using specificity + q-value + order) would help maintainers orient quickly.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."

src/server/any_router.cpp (3)

296-302: Redundant const_castp is already a non-const reference.

dispatch_loop takes route_params& p, so const_cast<route_params&>(p) at lines 298 and 317 is a no-op. This appears to be a leftover from when the parameter may have been const.

♻️ Proposed fix
         if(m.end_ && !e.match_method(
-            const_cast<route_params&>(p)))
+            p))
         {
             rv = co_await e.h->invoke(
-                const_cast<route_params&>(p));
+                p);

200-374: Core dispatch loop is well-structured but would benefit from an overview comment.

The dispatch loop implements a complex hierarchical matching algorithm with ancestor validation, depth tracking, OPTIONS aggregation, exception capture, and multiple continuation modes (next, next_route, done, close, error). A block comment at the top of the function summarizing this flow would greatly help maintainability.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."


625-648: Duplicate initialization logic across both dispatch overloads.

The two dispatch methods share ~20 lines of identical route_params initialization and path setup (decoding, trailing slash handling, base_path/path setup). Consider extracting this into a private helper to reduce duplication and prevent the two overloads from diverging.

Also applies to: 675-701

include/boost/http/server/route_handler.hpp (2)

440-446: reset(), status(), and send() lack sufficient documentation.

reset() should document what state it clears and its postconditions. status(http::status) should clarify that it sets the response status code and returns *this for chaining. send() has a one-liner but should mention that it writes the response through res_body and what happens if no body is provided (empty body vs. no body). These are public API methods on a central type.

As per coding guidelines, "Docstrings are required for all classes and functions in public headers in non-detail namespaces. Docstrings should include a brief description of purpose, document all parameters, return values, and any preconditions, postconditions, or exceptions."


458-508: match_result is a public nested struct accessing private state — consider restricting visibility.

match_result accesses priv_ members directly (lines 480–481, 490–493), yet it is declared public at line 383. Users could construct and call adjust_path/restore_path on arbitrary route_params objects, corrupting internal state. If this type is only used internally by the router, it should be in a private or detail scope and befriended as needed.

include/boost/http/serializer.hpp (3)

217-238: start_stream() coexists with start_writes() — clarify or deprecate.

The docstring for start_stream() says it is a "Low-level entry point equivalent to @ref start_writes." If they are truly equivalent, having both creates confusion about which one to call. Consider deprecating start_stream() in favor of start_writes() (e.g., with [[deprecated]]), or document why both exist (backward compatibility?).


647-680: Drain-loop logic is duplicated ~6 times across sink methods.

The pattern while(!is_done()) { prepare → check error/need_data → check empty → write_some → consume } is repeated in commit, commit_eof, write (twice: header drain + body drain), write_eof(buffers), and write_eof(). The only variations are whether need_data triggers break vs continue. Extracting a private helper coroutine (e.g., drain(bool until_done)) would significantly reduce duplication and the risk of inconsistent fixes.

Also applies to: 696-730, 752-774, 800-824, 856-878, 902-927


610-635: prepare() is synchronous but has no error reporting path.

If stream_prepare() throws (e.g., because the serializer is not in streaming mode), the exception propagates uncaught. The docstring doesn't mention exceptions. Since this is synchronous (not a coroutine), consider documenting which exceptions may propagate, or wrapping with a system::result.

As per coding guidelines, "Docstrings are required for all classes and functions in public headers in non-detail namespaces. Docstrings should include a brief description of purpose, document all parameters, return values, and any preconditions, postconditions, or exceptions."

include/boost/http/server/router.hpp (5)

61-130: Bit-packing in router_options is non-obvious — add a comment documenting the encoding.

The bit layout across merge_params (bit 0), case_sensitive (bits 1–2 for tri-state: 2 = set-true, 4 = set-false), and strict (bits 3–4 for tri-state: 8 = set-true, 16 = set-false) uses magic numbers. A brief comment next to unsigned int v_ = 0 explaining the bit layout would help maintainers.

As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment ... This should highlight anything tricky or non-obvious that a maintainer might need to know."

Proposed comment
 private:
     template<class, class> friend class router;
+    // Bit layout:
+    //   bit 0     : merge_params (0=false, 1=true)
+    //   bits 1-2  : case_sensitive tri-state (0=inherit, 2=true, 4=false)
+    //   bits 3-4  : strict tri-state (0=inherit, 8=true, 16=false)
     unsigned int v_ = 0;
 };

506-545: handlers_impl stores a dangling reference to the transform after construction.

T const& ht at line 509 is a reference to the transform. It is only used during the constructor's assign() calls (lines 522–539). After construction, ht is dangling if the original ht_ is a temporary or moves. Since the handler_ptr objects are fully populated during construction and moved out by add_middleware, this likely works in practice. However, the dangling reference is fragile. Consider storing the reference only within the constructor scope, or using a pointer set to nullptr after construction.


456-478: handler_impl::invoke relies on dead-code std::terminate() branch.

The else branch at line 474–477 calls std::terminate(), which should be unreachable given that handler_kind statically classifies handlers at registration. This is a reasonable safety net. Consider annotating with BOOST_UNREACHABLE() or a compiler hint for the optimiser, if available in the Boost ecosystem.


891-902: except() passes ht_ to make_handlers but transforms are never applied to exception handlers.

The class docstring states "Error handlers and exception handlers are never transformed." Inside handlers_impl::assign, exception handlers match the first if constexpr branch (lines 525–531) which does not invoke the transform, so this is functionally correct. However, passing ht_ is misleading to readers. Consider passing a no-op or identity{} explicitly, or adding a comment clarifying that the transform is unused for these handler kinds.


1190-1202: fluent_route stores a reference to router — document lifetime requirements.

The router& member at line 1201 means the fluent_route is only valid while the owning router is alive. Since route() at line 1058 returns by value and is typically used in chained calls (e.g., r.route("/x").get(...)), this is safe in normal usage. The docstring for fluent_route (line 559) could briefly note that the route must not outlive the router.

Comment on lines +413 to +437
template <HasMapping T, typename F>
constexpr void for_each_field(F&& f)
{
constexpr auto fs = tag_invoke(fields, T{});
detail::for_each_impl(
fs,
static_cast<F&&>(f),
std::make_index_sequence<
std::tuple_size_v<decltype(fs)>>{});
}

/** Return the number of fields in a mapped type.

@tparam T A type satisfying @ref HasMapping.

@return The compile-time field count.

@see for_each_field
*/
template <HasMapping T>
constexpr std::size_t field_count()
{
constexpr auto fs = tag_invoke(fields, T{});
return std::tuple_size_v<decltype(fs)>;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

for_each_field and field_count require default-constructible T, but HasMapping does not enforce this.

Both for_each_field<T> and field_count<T> invoke tag_invoke(fields, T{}), which requires T to be default-constructible. However, the HasMapping concept only checks that tag_invoke(fields, v) is valid for T const& v — it does not require default-constructibility. A type that satisfies HasMapping but has a deleted/private default constructor would pass the concept check but fail to compile when used with these utilities.

Consider either:

  1. Adding a std::default_initializable<T> constraint to these functions, or
  2. Using std::declval<T const&>() in a decltype context instead of T{} (though this complicates constexpr usage).
🤖 Prompt for AI Agents
In `@include/boost/http/db/schema.hpp` around lines 413 - 437, for_each_field and
field_count call tag_invoke(fields, T{}) which requires T be
default-constructible, but HasMapping doesn't enforce that; constrain these
utilities to only accept default-initializable types by adding
std::default_initializable<T> (or equivalent) to the template requirement list
for template<HasMapping T, ...> in for_each_field and template<HasMapping T> in
field_count so the compiler will reject non-default-constructible T instead of
producing a hard-to-diagnose error when constructing T{}.

Comment on lines +32 to +39
/** Route handler middleware that parses a JSON request body.

This middleware checks that the request is a POST with
Content-Type `application/json`, reads the entire request
body into a @ref json_sink, and stores the parsed
`json::value` in the route's @ref datastore under the
key `json::value`. If the request does not match, the
middleware yields to the next handler.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Content-Type prefix match may be too broad.

The implementation (in src/json/json_body.cpp) uses starts_with("application/json") to check the Content-Type. This would also match types like application/json-patch+json or application/jsonlines, which are distinct media types that shouldn't be parsed as plain JSON bodies.

Consider checking for an exact match or verifying that the character following "application/json" is either end-of-string, ;, or whitespace.

Proposed fix in src/json/json_body.cpp
-    if( ! p.is_method(method::post) ||
-        ! p.req.value_or(
-            field::content_type, "")
-                .starts_with("application/json"))
+    auto ct = p.req.value_or(
+        field::content_type, "");
+    bool is_json =
+        ct.starts_with("application/json") &&
+        (ct.size() == 16 ||
+         ct[16] == ';' ||
+         ct[16] == ' ');
+    if( ! p.is_method(method::post) || ! is_json)
🤖 Prompt for AI Agents
In `@include/boost/http/json/json_body.hpp` around lines 32 - 39, The Content-Type
check in the json_body middleware is too permissive because it uses
starts_with("application/json") and will match media types like
"application/json-patch+json"; update the check in the json_body handler (the
routine that verifies request method and content type before creating a
json_sink and storing json::value in the datastore) to accept "application/json"
only when it is followed by end-of-string, a semicolon, or whitespace (i.e.,
parse the header token and allow optional parameters), otherwise yield to the
next handler; keep the rest of the flow (reading into json_sink and storing
under the "json::value" key) unchanged.

template<class>
class basic_router;
class flat_router;
template<class> class router;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for all usages of detail::router
rg -n 'detail::router' --type=cpp --type=hpp

Repository: cppalliance/http

Length of output: 87


🏁 Script executed:

# Also search for just "router" in the detail namespace to see if it's referenced
rg -n '::router<' include/boost/http/server/detail/ --type=cpp --type=hpp

Repository: cppalliance/http

Length of output: 87


🏁 Script executed:

# Check the actual content of the file around lines 28 and 55
sed -n '25,60p' include/boost/http/server/detail/router_base.hpp

Repository: cppalliance/http

Length of output: 972


🏁 Script executed:

# Correct rg syntax - search for detail::router or router< pattern
rg 'detail::router' --type-list | grep -i hpp

Repository: cppalliance/http

Length of output: 449


🏁 Script executed:

# Try simpler search without file type specification
rg 'detail::router' -A 2 -B 2

Repository: cppalliance/http

Length of output: 4667


🏁 Script executed:

# Search for router references in detail namespace
rg 'router<' include/boost/http/server/detail/ -A 2 -B 2

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check the entire router_base.hpp file for any reference to the forward declaration
cat include/boost/http/server/detail/router_base.hpp | grep -n 'router'

Repository: cppalliance/http

Length of output: 1073


🏁 Script executed:

# Verify: search explicitly for any single-parameter router usage in detail namespace
rg 'router<[^,>]+>' include/boost/http/server/detail/

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check if there's any other definition of detail::router with one parameter
rg 'detail::router<' -A 3

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Verify the actual router template definition
sed -n '1,50p' include/boost/http/server/router.hpp

Repository: cppalliance/http

Length of output: 1543


Remove stale forward declaration with incorrect template arity.

Line 28 forward-declares template<class> class router; with a single template parameter, but the actual router template defined in include/boost/http/server/router.hpp takes two parameters: template<class P = route_params, class HT = identity> class router. The friend declaration at line 55 correctly references the two-parameter version. The stale single-parameter forward declaration is not referenced anywhere in the codebase and should be removed.

🤖 Prompt for AI Agents
In `@include/boost/http/server/detail/router_base.hpp` at line 28, Remove the
stale forward declaration "template<class> class router;" and replace nothing;
instead rely on the existing correct two-parameter template declaration of
router (template<class P = route_params, class HT = identity> class router) that
the friend declaration already references; locate and delete the
single-parameter forward declaration in router_base.hpp so only the correct
router template (as defined in the router.hpp header) is visible and there are
no conflicting/unused forward declarations.

Comment on lines +10 to +11
#ifndef BOOST_HTTP_SERVER_WORKER_HPP
#define BOOST_HTTP_SERVER_WORKER_HPP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include guard doesn't match the filename.

The file is http_worker.hpp but the guard is BOOST_HTTP_SERVER_WORKER_HPP. For consistency with Boost conventions (and to avoid collisions if a worker.hpp ever exists), this should be BOOST_HTTP_SERVER_HTTP_WORKER_HPP.

Proposed fix
-#ifndef BOOST_HTTP_SERVER_WORKER_HPP
-#define BOOST_HTTP_SERVER_WORKER_HPP
+#ifndef BOOST_HTTP_SERVER_HTTP_WORKER_HPP
+#define BOOST_HTTP_SERVER_HTTP_WORKER_HPP

And at the end of the file:

-#endif
+#endif // BOOST_HTTP_SERVER_HTTP_WORKER_HPP
🤖 Prompt for AI Agents
In `@include/boost/http/server/http_worker.hpp` around lines 10 - 11, The include
guard macro is misnamed; change the opening macro from
BOOST_HTTP_SERVER_WORKER_HPP to BOOST_HTTP_SERVER_HTTP_WORKER_HPP and update the
corresponding closing `#endif` comment (if present) to match; locate the macros in
http_worker.hpp (the existing BOOST_HTTP_SERVER_WORKER_HPP) and replace both the
`#ifndef/`#define and the trailing `#endif` comment to use
BOOST_HTTP_SERVER_HTTP_WORKER_HPP so the guard matches the filename and Boost
naming conventions.

Comment on lines +43 to +71
@par Example
@code
struct my_worker
: tcp_server::worker_base
, http_worker
{
corosio::tcp_socket sock;

my_worker(
corosio::io_context& ctx,
http::router<route_params> const& router,
http::shared_parser_config parser_cfg,
http::shared_serializer_config serializer_cfg)
: http_worker(router, parser_cfg, serializer_cfg)
, sock(ctx)
{
sock.open();
rp.req_body = capy::any_buffer_source(parser.source_for(sock));
rp.res_body = capy::any_buffer_sink(serializer.sink_for(sock));
stream = capy::any_read_stream(&sock);
}

corosio::tcp_socket& socket() override { return sock; }

void run(launcher launch) override
{
launch(sock.get_executor(), do_http_session());
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Example in class docstring won't compile — constructor signature mismatch.

The example constructs http_worker(router, parser_cfg, serializer_cfg) with 3 arguments, but the actual constructor (line 97) takes 4: http_worker(Stream& stream_, router, parser_cfg, serializer_cfg). The example also manually wires rp.req_body, rp.res_body, and stream, but the constructor already does this internally — the example appears to be stale.

🤖 Prompt for AI Agents
In `@include/boost/http/server/http_worker.hpp` around lines 43 - 71, The example
in the class docstring is stale: it calls http_worker(router, parser_cfg,
serializer_cfg) but the actual constructor is http_worker(Stream& stream_,
router, parser_cfg, serializer_cfg) and it already wires rp.req_body,
rp.res_body and stream; update the example's my_worker constructor to call
http_worker(sock, router, parser_cfg, serializer_cfg) (or otherwise pass the
stream object as the first argument) and remove the manual assignments to
rp.req_body, rp.res_body, and stream so the example compiles and reflects the
real http_worker(Stream& ...) constructor and behavior.

Comment on lines +614 to +623
// Handle OPTIONS * before normal dispatch
if(verb == http::method::options &&
url.encoded_path() == "*")
{
if(impl_->options_handler_)
{
return impl_->options_handler_->invoke(
p, impl_->global_allow_header_);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

route_params used before initialization in OPTIONS * early-return path.

When verb == method::options and the path is "*", the handler is invoked at line 620-621 with p — but p hasn't been initialized yet (the initialization block starts at line 626). This means route_params fields like kind_, verb_, decoded_path_, params, etc. contain whatever state the caller passed in, which could be stale or default values. The same issue exists in the second dispatch overload at lines 665-673.

🐛 Proposed fix: move initialization before the OPTIONS * check
     impl_->ensure_finalized();

-    // Handle OPTIONS * before normal dispatch
-    if(verb == http::method::options &&
-       url.encoded_path() == "*")
-    {
-        if(impl_->options_handler_)
-        {
-            return impl_->options_handler_->invoke(
-                p, impl_->global_allow_header_);
-        }
-    }
-
     // Initialize params
     auto& pv = *route_params_access{p};
     pv.kind_ = is_plain;
     pv.verb_ = verb;
     pv.verb_str_.clear();
     pv.ec_.clear();
     pv.ep_ = nullptr;
     p.params.clear();
+
+    // Handle OPTIONS * before normal dispatch
+    if(verb == http::method::options &&
+       url.encoded_path() == "*")
+    {
+        if(impl_->options_handler_)
+        {
+            return impl_->options_handler_->invoke(
+                p, impl_->global_allow_header_);
+        }
+    }
+
     pv.decoded_path_ = pct_decode_path(url.encoded_path());

Apply the same reordering in the std::string_view verb overload (lines 665-673).

🤖 Prompt for AI Agents
In `@src/server/any_router.cpp` around lines 614 - 623, The OPTIONS "*"
early-return is invoking impl_->options_handler_->invoke with p before p
(route_params) is initialized; move the route_params initialization (the block
that sets p.kind_, p.verb_, p.decoded_path_, p.params, etc.) to occur before the
OPTIONS '*' check so p is fully populated when passed to
impl_->options_handler_->invoke, and apply the same reordering in the dispatch
overload that takes std::string_view verb (the other dispatch block that calls
impl_->options_handler_->invoke) so both code paths initialize route_params
prior to calling the options handler.

Comment on lines +66 to +73
{
auto rv = urls::parse_uri_reference(rp.req.target());
if(rv.has_error())
{
rp.status(http::status::bad_request);
}
rp.url = rv.value();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Accessing rv.value() after an error will throw system_error.

When urls::parse_uri_reference fails, the code sets the response status to bad_request but falls through to rv.value() on line 72. Since rv holds an error, .value() will throw boost::system::system_error, likely terminating the coroutine with an unhandled exception.

The error path needs to skip the .value() call — either continue to the next iteration (after sending an error response) or break.

🐛 Proposed fix
         // Parse the URL
         {
             auto rv = urls::parse_uri_reference(rp.req.target());
             if(rv.has_error())
             {
                 rp.status(http::status::bad_request);
+                // TODO: send the 400 response before continuing
+                break;
             }
-            rp.url = rv.value();
+            else
+                rp.url = rv.value();
         }
🤖 Prompt for AI Agents
In `@src/server/http_worker.cpp` around lines 66 - 73, The code calls rv.value()
unconditionally after urls::parse_uri_reference(rp.req.target()), which will
throw if rv.has_error(); update the error path in the parsing block so that
after setting rp.status(http::status::bad_request) you skip using rv (e.g., send
the response and continue or return from the surrounding loop/coroutine) and
only call rp.url = rv.value() when rv.has_value() (or !rv.has_error()); locate
the parsing block around urls::parse_uri_reference, the local variable rv, and
the rp.status/rp.url assignments and ensure control flow exits the error case
before accessing rv.value().

Comment on lines +230 to +244
body.append("{\"name\":\"");

// Escape JSON string
for(auto c : e.name)
{
switch(c)
{
case '"': body.append("\\\""); break;
case '\\': body.append("\\\\"); break;
case '\n': body.append("\\n"); break;
case '\r': body.append("\\r"); break;
case '\t': body.append("\\t"); break;
default: body.push_back(c); break;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incomplete JSON string escaping — control characters emitted raw.

RFC 8259 §7 requires all control characters (U+0000–U+001F) to be escaped. This code only handles ", \\, \n, \r, \t. Characters like \b (\x08), \f (\x0C), and other control chars will be emitted raw, producing invalid JSON. While unlikely in filenames on most OSes, it's a correctness gap.

🐛 Proposed fix: escape remaining control characters
         for(auto c : e.name)
         {
             switch(c)
             {
             case '"':  body.append("\\\""); break;
             case '\\': body.append("\\\\"); break;
+            case '\b': body.append("\\b");  break;
+            case '\f': body.append("\\f");  break;
             case '\n': body.append("\\n");  break;
             case '\r': body.append("\\r");  break;
             case '\t': body.append("\\t");  break;
-            default:   body.push_back(c);   break;
+            default:
+                if(static_cast<unsigned char>(c) < 0x20)
+                {
+                    char buf[7];
+                    std::snprintf(buf, sizeof(buf),
+                        "\\u%04x",
+                        static_cast<unsigned>(
+                            static_cast<unsigned char>(c)));
+                    body.append(buf);
+                }
+                else
+                {
+                    body.push_back(c);
+                }
+                break;
             }
         }
🤖 Prompt for AI Agents
In `@src/server/serve_index.cpp` around lines 230 - 244, The JSON string escaping
in the loop that builds body (see the loop iterating over e.name and calls like
body.append and body.push_back) only handles '"', '\\', '\n', '\r', '\t' and
leaves other control characters U+0000–U+001F unescaped; update that loop to
escape all control characters per RFC 8259 by mapping '\b' and '\f' to "\\b" and
"\\f" and encoding any other control codepoints (<= 0x1F) as "\\u00XX"
(hex-escaped) before appending to body so the generated JSON is always valid.

Comment on lines +366 to +395
std::vector<dir_entry> entries;
{
std::filesystem::directory_iterator it(path, fec);
if(fec)
co_return route_next;

for(auto const& de :
std::filesystem::directory_iterator(path, fec))
{
auto name = de.path().filename().string();

// Skip hidden files unless configured
if(! impl_->opts.hidden &&
! name.empty() && name[0] == '.')
continue;

dir_entry e;
e.name = std::move(name);

std::error_code sec;
e.is_dir = de.is_directory(sec);
if(! e.is_dir)
e.size = de.file_size(sec);
auto lwt = de.last_write_time(sec);
if(! sec)
e.mtime = to_epoch(lwt);

entries.push_back(std::move(e));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: unused directory_iterator — directory is iterated twice.

it is constructed at line 368 but never used. A second directory_iterator is created at line 373 for the actual loop. This means the directory is opened twice (once to check for errors, once to iterate), and the error code from the second open is not checked before entering the loop.

🐛 Proposed fix: use a single iterator
     // Read directory entries
     std::vector<dir_entry> entries;
     {
         std::filesystem::directory_iterator it(path, fec);
         if(fec)
             co_return route_next;
 
-        for(auto const& de :
-            std::filesystem::directory_iterator(path, fec))
+        for(auto const& de : it)
         {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::vector<dir_entry> entries;
{
std::filesystem::directory_iterator it(path, fec);
if(fec)
co_return route_next;
for(auto const& de :
std::filesystem::directory_iterator(path, fec))
{
auto name = de.path().filename().string();
// Skip hidden files unless configured
if(! impl_->opts.hidden &&
! name.empty() && name[0] == '.')
continue;
dir_entry e;
e.name = std::move(name);
std::error_code sec;
e.is_dir = de.is_directory(sec);
if(! e.is_dir)
e.size = de.file_size(sec);
auto lwt = de.last_write_time(sec);
if(! sec)
e.mtime = to_epoch(lwt);
entries.push_back(std::move(e));
}
}
std::vector<dir_entry> entries;
{
std::filesystem::directory_iterator it(path, fec);
if(fec)
co_return route_next;
for(auto const& de : it)
{
auto name = de.path().filename().string();
// Skip hidden files unless configured
if(! impl_->opts.hidden &&
! name.empty() && name[0] == '.')
continue;
dir_entry e;
e.name = std::move(name);
std::error_code sec;
e.is_dir = de.is_directory(sec);
if(! e.is_dir)
e.size = de.file_size(sec);
auto lwt = de.last_write_time(sec);
if(! sec)
e.mtime = to_epoch(lwt);
entries.push_back(std::move(e));
}
}
🤖 Prompt for AI Agents
In `@src/server/serve_index.cpp` around lines 366 - 395, The code constructs a
std::filesystem::directory_iterator named it but then creates a second iterator
in the for loop, causing the directory to be opened twice and the error code
from the second open to be ignored; replace the second iterator with the
already-created it (and use it in the for loop) so you only open the directory
once and check fec before iterating, keeping the existing behavior around
skipping hidden files, populating dir_entry (e.name, e.is_dir, e.size, e.mtime)
and pushing into entries.

Comment on lines +399 to +410
// Determine ".." display
std::filesystem::path root_canonical(impl_->root);
std::filesystem::path dir_canonical(path);
{
std::error_code ec2;
root_canonical =
std::filesystem::canonical(root_canonical, ec2);
dir_canonical =
std::filesystem::canonical(dir_canonical, ec2);
}
bool show_up = impl_->opts.show_parent &&
dir_canonical != root_canonical;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Canonicalization silently ignores errors on both paths.

If std::filesystem::canonical fails for root_canonical (e.g., the root was deleted between the directory check and here), ec2 is set but ignored, and the subsequent dir_canonical call reuses the same ec2 variable. The comparison dir_canonical != root_canonical at line 410 then operates on potentially non-canonical paths, which could incorrectly show or hide the parent link.

Consider checking ec2 and falling back to show_parent = false (or true) on failure.

🤖 Prompt for AI Agents
In `@src/server/serve_index.cpp` around lines 399 - 410, The canonicalization
block for root_canonical and dir_canonical silently ignores errors via a reused
ec2, which can leave non-canonical paths and make the show_up decision
incorrect; update the code in serve_index.cpp to use separate std::error_code
variables for each std::filesystem::canonical call (e.g., ec_root and ec_dir),
check each error after its call, and if either canonicalization fails set
show_up = false (or otherwise short-circuit the parent-link logic) before
comparing dir_canonical and root_canonical; ensure you still respect
impl_->opts.show_parent when deciding the final value of show_up.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@include/boost/http/server/detail/dynamic_invoke.hpp`:
- Around line 130-140: The single-arg invoke_impl (template<class First>)
wrongly assumes f(p) yields route_task; update it to mirror the multi-arg
overload's return-type dispatch: compute the callable's return type for
invocation with route_params& (use std::invoke_result_t or equivalent) and use
if constexpr to detect whether it is route_task or route_result, returning f(p)
directly for route_task and returning wrap_result(f(p)) when the callable
returns route_result; keep the existing static_assert on First accepting
route_params& and keep calling f(p) otherwise.

In `@include/boost/http/server/router.hpp`:
- Around line 629-634: The converting and default constructors for router are
currently marked unconditional noexcept but may call HT move/default
constructors (ht_ or ht_(std::move(other.ht_))) which can throw; change those
noexcept specifiers to conditional ones: for the converting constructor make it
noexcept when HT is nothrow-constructible from OtherHT and detail::router_base
is nothrow-move-constructible, and for the default constructor make it noexcept
when HT is nothrow-default-constructible and detail::router_base is
nothrow-move-constructible (use the std::is_nothrow_* traits with HT, OtherHT
and detail::router_base to implement these conditions).
- Around line 523-547: The template assign<I, H1, HN...> can SFINAE-fail when HT
(T) is not invocable with H1 because the decltype in the second constexpr is
instantiated unconditionally; fix by guarding that branch with an invocability
check (e.g. std::is_invocable_v or std::invocable) so the decltype is only
considered if T const& is callable with H1, and add an explicit fallback branch
for direct plain handlers (the case where detail::returns_route_task<H1, P&,
...> is true but HT is not invocable) that assigns v[I] =
make_handler(std::forward<H1>(h1)); ensure you update the conditions around
detail::returns_route_task, the decltype check, and the fallback so assign,
make_handler, ht, and the detail::returns_route_task checks behave correctly
without causing hard errors.
🧹 Nitpick comments (1)
include/boost/http/server/detail/dynamic_invoke.hpp (1)

13-18: Consider adding a high-level overview comment after the includes.

Per coding guidelines, files with non-trivial implementation logic should include a /* */ block comment after the includes providing a high-level overview. A brief note explaining the dependency-injection-style dispatch pattern (resolve callable args from a polystore, return route_next on miss) would help future maintainers. As per coding guidelines, "Files containing non-trivial implementation logic should include a /* */ block comment after the includes that provides a high-level overview of how the implementation works."

Comment on lines +523 to +547
private:
template<std::size_t I, class H1, class... HN>
void assign(H1&& h1, HN&&... hn)
{
if constexpr (
detail::returns_route_task<
H1, P&, system::error_code> ||
detail::returns_route_task<
H1, P&, std::exception_ptr>)
{
v[I] = make_handler(std::forward<H1>(h1));
}
else if constexpr (detail::returns_route_task<
decltype(std::declval<T const&>()(std::declval<H1>())), P&>)
{
v[I] = make_handler(ht(std::forward<H1>(h1)));
}
assign<I+1>(std::forward<HN>(hn)...);
}

template<std::size_t>
void assign(int = 0)
{
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing branch for direct plain handlers causes hard compile error with custom transforms.

When HT is a custom transform (not identity), a handler that already satisfies route_task(P&) directly will skip the error/exception branch (line 527) and reach line 535. There, decltype(std::declval<T const&>()(std::declval<H1>())) is a hard error if HT is not invocable with H1if constexpr does not make the decltype SFINAE-friendly.

This contradicts handler_kind (line 414), which correctly classifies direct plain handlers before checking the transform, so handler_check static asserts pass but instantiation of assign fails.

Proposed fix — guard invocability and add a direct-plain fallback
         template<std::size_t I, class H1, class... HN>
         void assign(H1&& h1, HN&&... hn)
         {
             if constexpr (
                 detail::returns_route_task<
                     H1, P&, system::error_code> ||
                 detail::returns_route_task<
                     H1, P&, std::exception_ptr>)
             {
                 v[I] = make_handler(std::forward<H1>(h1));
             }
-            else if constexpr (detail::returns_route_task<
-                decltype(std::declval<T const&>()(std::declval<H1>())), P&>)
+            else if constexpr (
+                std::is_invocable_v<T const&, H1> &&
+                detail::returns_route_task<
+                    std::invoke_result_t<T const&, H1>, P&>)
             {
                 v[I] = make_handler(ht(std::forward<H1>(h1)));
             }
+            else if constexpr (detail::returns_route_task<H1, P&>)
+            {
+                v[I] = make_handler(std::forward<H1>(h1));
+            }
+            else
+            {
+                static_assert(sizeof(H1) == 0,
+                    "handler has no valid dispatch path");
+            }
             assign<I+1>(std::forward<HN>(hn)...);
         }
🤖 Prompt for AI Agents
In `@include/boost/http/server/router.hpp` around lines 523 - 547, The template
assign<I, H1, HN...> can SFINAE-fail when HT (T) is not invocable with H1
because the decltype in the second constexpr is instantiated unconditionally;
fix by guarding that branch with an invocability check (e.g. std::is_invocable_v
or std::invocable) so the decltype is only considered if T const& is callable
with H1, and add an explicit fallback branch for direct plain handlers (the case
where detail::returns_route_task<H1, P&, ...> is true but HT is not invocable)
that assigns v[I] = make_handler(std::forward<H1>(h1)); ensure you update the
conditions around detail::returns_route_task, the decltype check, and the
fallback so assign, make_handler, ht, and the detail::returns_route_task checks
behave correctly without causing hard errors.

Comment on lines +629 to +634
router(
router<OtherP, OtherHT>&& other) noexcept
: detail::router_base(std::move(other))
, ht_(std::move(other.ht_))
{
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unconditional noexcept may call std::terminate if HT construction throws.

Line 630 and 642: both converting constructors are marked noexcept, but ht_(std::move(other.ht_)) (move-constructing HT from OtherHT) and default-initializing HT are not guaranteed noexcept. If either throws, the program terminates.

Consider using a conditional noexcept:

noexcept(std::is_nothrow_constructible_v<HT, OtherHT>
      && std::is_nothrow_move_constructible_v<detail::router_base>)
🤖 Prompt for AI Agents
In `@include/boost/http/server/router.hpp` around lines 629 - 634, The converting
and default constructors for router are currently marked unconditional noexcept
but may call HT move/default constructors (ht_ or ht_(std::move(other.ht_)))
which can throw; change those noexcept specifiers to conditional ones: for the
converting constructor make it noexcept when HT is nothrow-constructible from
OtherHT and detail::router_base is nothrow-move-constructible, and for the
default constructor make it noexcept when HT is nothrow-default-constructible
and detail::router_base is nothrow-move-constructible (use the std::is_nothrow_*
traits with HT, OtherHT and detail::router_base to implement these conditions).

@vinniefalco vinniefalco merged commit 7dc998f into cppalliance:develop Feb 7, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants