Skip to content

Conversation

@vinniefalco
Copy link
Member

@vinniefalco vinniefalco commented Jan 19, 2026

Summary by CodeRabbit

  • New Features

    • Comprehensive static file serving with caching, ETag/Last-Modified handling, range support, directory/index handling, and configurable dotfile/redirect behavior.
    • Utilities added: URL encoding, HTML escaping, ETag generation, HTTP-date formatting, Range parsing, MIME lookup APIs, and status-code predicate helpers.
    • Coroutine-based route handling primitives for streaming responses.
  • Documentation

    • Added a documentation style/config entry for rule guidance.
  • Tests

    • Added unit tests for static serving and status predicates; test helpers for route handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@sdarwin
Copy link
Contributor

sdarwin commented Jan 19, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa86e6 and ff61671.

📒 Files selected for processing (28)
  • .cursor/rules/doc.mdc
  • include/boost/http/server/cors.hpp
  • include/boost/http/server/encode_url.hpp
  • include/boost/http/server/escape_html.hpp
  • include/boost/http/server/etag.hpp
  • include/boost/http/server/fresh.hpp
  • include/boost/http/server/mime_db.hpp
  • include/boost/http/server/mime_types.hpp
  • include/boost/http/server/range_parser.hpp
  • include/boost/http/server/route_handler.hpp
  • include/boost/http/server/send_file.hpp
  • include/boost/http/server/serve_static.hpp
  • include/boost/http/server/statuses.hpp
  • src/server/cors.cpp
  • src/server/encode_url.cpp
  • src/server/escape_html.cpp
  • src/server/etag.cpp
  • src/server/fresh.cpp
  • src/server/mime_db.cpp
  • src/server/mime_types.cpp
  • src/server/range_parser.cpp
  • src/server/send_file.cpp
  • src/server/serve_static.cpp
  • src/server/statuses.cpp
  • test/unit/server/route_handler.cpp
  • test/unit/server/serve_static.cpp
  • test/unit/server/statuses.cpp
  • test/unit/server/test_route_handler.hpp
 ____________________________________________________
< Fluent in over six million forms of bug detection. >
 ----------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

📝 Walkthrough

Walkthrough

Adds many Boost.HTTP server features: async route handler API, CORS middleware updated to coroutines, URL/HTML encoding, ETag generation, freshness checks, MIME database and lookup, Range header parsing, file sending and static-serving middleware, status predicates, and accompanying unit tests and documentation rule.

Changes

Cohort / File(s) Summary
Async route handler & tests
include/boost/http/server/route_handler.hpp, test/unit/server/test_route_handler.hpp, test/unit/server/route_handler.cpp
Introduces route_task alias; adds async virtuals send(), write(), end() to route_params; adds test double test_route_params and adjusts tests to use it.
CORS middleware
include/boost/http/server/cors.hpp, src/server/cors.cpp
Converts CORS operator to coroutine-based route_task and renames parameter to rp; moves options_ to public and updates docs/comments.
Encoding / escaping
include/boost/http/server/encode_url.hpp, src/server/encode_url.cpp, include/boost/http/server/escape_html.hpp, src/server/escape_html.cpp
Adds encode_url(core::string_view) and escape_html(core::string_view) public APIs and implementations for percent-encoding and HTML escaping.
ETag & freshness
include/boost/http/server/etag.hpp, src/server/etag.cpp, include/boost/http/server/fresh.hpp, src/server/fresh.cpp
Adds etag_options, two etag() overloads, and is_fresh(request const&, response const&) with internal helpers for matching and date checks.
MIME DB & types
include/boost/http/server/mime_db.hpp, src/server/mime_db.cpp, include/boost/http/server/mime_types.hpp, src/server/mime_types.cpp
Adds mime_type_entry, mime_db::lookup(), and mime_types utilities: lookup(), extension(), charset(), and content_type() with case-insensitive searches.
Range parsing
include/boost/http/server/range_parser.hpp, src/server/range_parser.cpp
Adds byte_range, range_result, range_result_type, and parse_range(size, header) to parse/validate HTTP Range headers.
File sending & static serving
include/boost/http/server/send_file.hpp, src/server/send_file.cpp, include/boost/http/server/serve_static.hpp, src/server/serve_static.cpp, test/unit/server/serve_static.cpp
New send_file_options/send_file_info and send_file_init() plus format_http_date(); adds serve_static middleware with options (dotfiles, index, caching, ranges, redirects) and full coroutine-based streaming implementation; includes basic unit test for header inclusion.
Status predicates
include/boost/http/server/statuses.hpp, src/server/statuses.cpp, test/unit/server/statuses.cpp
Adds boost::http::statuses::is_empty(), is_redirect(), and is_retry() with tests.
Tooling / Docs
.cursor/rules/doc.mdc
Adds documentation style rule file with YAML front matter and rule requiring route_params param to be named rp.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant serve_static as serve_static
    participant FS as File System
    participant Mime as MIME DB
    participant Range as Range Parser
    participant RP as route_params

    Client->>serve_static: HTTP GET /path
    serve_static->>FS: stat(path)
    FS-->>serve_static: (exists,size,mtime)
    serve_static->>Mime: lookup(path)
    Mime-->>serve_static: content_type
    serve_static->>serve_static: generate ETag / set headers
    Client->>serve_static: If-None-Match / Range headers
    serve_static->>serve_static: is_fresh? (ETag / Last-Modified)
    alt fresh (304)
        serve_static->>RP: set 304 and end
    else if Range present
        serve_static->>Range: parse_range(size, header)
        Range-->>serve_static: ranges/result
        serve_static->>FS: open file (with range)
        loop stream chunks
            serve_static->>RP: write(chunk) (co_await)
        end
        serve_static->>RP: end()
    else full content
        serve_static->>FS: open file
        loop stream chunks
            serve_static->>RP: write(chunk) (co_await)
        end
        serve_static->>RP: end()
    end
    RP-->>Client: HTTP response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hop through patches, swift and spry,
New coroutines that reach the sky,
Files and ranges, ETags bright,
MIME whispers, headers right—
A rabbit's nod to HTTP delight.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'more' is vague and generic, using a non-descriptive term that does not convey meaningful information about the changeset. Replace the title with a descriptive summary of the primary changes, such as 'Add HTTP server utilities (CORS, file serving, MIME types, ETag, range parsing)' or similar that reflects the substantial new functionality.
✅ 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

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

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: 14

🤖 Fix all issues with AI agents
In @.cursor/rules/doc.mdc:
- Around line 1-5: The rule currently has an empty globs array and alwaysApply:
false so it never runs; update the rule by either populating the globs key with
the appropriate file patterns (e.g., doc/Javadoc/API examples patterns) or set
alwaysApply: true if you intend the rule to be global, ensuring the 'globs' and
'alwaysApply' fields in the rule header are adjusted accordingly so the rule
will actually execute (update the 'globs' array or toggle 'alwaysApply' in the
doc.mdc rule definition).

In `@include/boost/http/server/encode_url.hpp`:
- Around line 20-39: The doc for encode_url is ambiguous about component vs
full-URL encoding; update the API so callers aren't misled: either change the
documentation to state clearly that encode_url preserves RFC-reserved characters
(e.g., '/', '?', '#') and is meant for encoding full URLs/locations only, or add
an explicit overload/flag (e.g., encode_url_component(const std::string& url) or
encode_url(const std::string& url, bool encode_reserved)) that percent-encodes
reserved characters for URL components; update the comment block to mention
which characters are preserved and reference the chosen symbol (encode_url or
new encode_url_component/encode_url(..., encode_reserved)) so callers know which
to use.

In `@include/boost/http/server/etag.hpp`:
- Around line 35-40: The documentation example for etag() is inconsistent with
the implementation in etag.cpp: replace the base64/slash-style example with one
that matches the actual output format produced by etag (size in hex, a hyphen,
then the 16-hex-character FNV-1a hash), e.g. something like
"\"<size_hex>-<16_hex_chars>\"" or, alternatively, add a note that the example
is illustrative only; update the docblock around the etag function to reflect
this change.

In `@include/boost/http/server/send_file.hpp`:
- Around line 102-116: The doc example uses rp.status(status::not_modified)
which is inconsistent with the implementation; update the example in the
send_file.hpp snippet so that when send_file_info::result ==
send_file_result::not_modified it calls rp.res.set_status(status::not_modified)
instead of rp.status(...), preserving the co_return co_await rp.send("") flow
and keeping references to send_file_info, send_file_init, and send_file_result
to match the real implementation pattern.

In `@src/server/etag.cpp`:
- Around line 59-66: The append calls that pass C-strings are hitting the
append(const char*) overload and triggering -Wrestrict; update them to use the
length-aware overload instead: when writing the size prefix built into buf (in
the ETag formatting code that uses std::snprintf and buf) call
result.append(buf, std::strlen(buf)) (or capture snprintf's return and append
that many bytes) rather than result.append(buf), and make the same replacement
for the other occurrence in the file (the hex append already uses
result.append(hex, 16) but ensure any other result.append(...) calls that take a
raw char* use the length-aware overload); reference symbols: result.append, buf,
hex, to_hex, body.size(), hash.

In `@src/server/fresh.cpp`:
- Around line 21-58: The etag_matches function currently uses
if_none_match.find(etag_val) which can return false positives; instead parse the
comma-separated if_none_match into distinct tokens, trim whitespace around each
token, remove optional surrounding quotes and the W/ weak prefix via the
existing strip_weak logic, and then compare each token for exact equality
against etag_val (and also compare the stripped entire if_none_match as a
fallback using inm_stripped). Update the logic in etag_matches to iterate tokens
(splitting on ',') and perform exact token comparisons rather than calling find,
ensuring correct handling of weak prefixes via strip_weak and preserving the "*"
special-case handling.
- Around line 61-75: The current not_modified_since(if_modified_since,
last_modified) does a lexicographic compare which fails across RFC7231's three
allowed date formats; replace the string comparison with proper HTTP-date
parsing: implement a helper like parse_http_date(core::string_view) that tries
IMF-fixdate, RFC850 and asctime formats (returning std::optional<time_t> or
chrono::time_point), use it to parse both if_modified_since and last_modified,
and then compare the parsed time values; if either parse fails, return false
(no-match). Ensure the new parse_http_date is used only inside
not_modified_since so the semantics and noexcept handling remain consistent.

In `@src/server/mime_types.cpp`:
- Around line 28-81: The ext_db constexpr array is intended to be sorted by
extension for binary search but "7z" is incorrectly last, breaking ordering;
update ext_db (constexpr ext_entry ext_db[]) by moving the {"7z",
"application/x-7z-compressed"} entry into its proper lexicographic position
among the extension keys (i.e., before the alphabetic entries so digits come
first) so the array remains strictly sorted and binary search will work
correctly.

In `@src/server/range_parser.cpp`:
- Around line 47-187: parse_range_spec currently returns false for both syntax
errors and valid-but-unsatisfiable specs, causing parse_range to misclassify
malformed headers as unsatisfiable; change parse_range_spec to return a
tri-state outcome (e.g., enum ParseOutcome { Malformed, Unsatisfiable, Ok } or
an out-parameter) instead of bool, and update parse_range to inspect that
outcome: if any Malformed -> set result.type = range_result_type::malformed;
otherwise collect ranges for Ok outcomes and treat Unsatisfiable outcomes as
non-satisfiable entries but not as Malformed; ensure functions referenced are
parse_range_spec, parse_range, range_result, and range_result_type so malformed
inputs like "bytes=abc-def" produce malformed (400) while true-but-unsatisfiable
specs produce unsatisfiable (416).

In `@src/server/send_file.cpp`:
- Around line 139-173: The code currently accepts the first range from
parse_range even when multiple ranges are requested; change the logic inside the
Range handling block (the branch using parse_range, range_result_type::ok, and
range_result.ranges) to only honor the Range when range_result.ranges.size() ==
1; if there are >1 ranges, treat the header as absent (do not set info.is_range,
do not set rp.res.set_status(status::partial_content), do not set Content-Range,
and let the normal full-content path run), since multipart/byteranges is not
implemented; keep existing behavior for the single-range case (setting
info.range_start/end and send_file_result::ok).

In `@src/server/serve_static.cpp`:
- Around line 55-67: The is_dotfile function only checks the last path segment
and thus misses dotfiles in earlier segments (e.g., "/.git/config"); update
is_dotfile to scan all path segments of the input core::string_view path and
return true if any segment begins with a '.' (except explicitly allowlisting
".well-known" if that is desired); modify the logic in is_dotfile to iterate
over segments split by '/' (or walk the string looking for '/' boundaries) and
check the first character after each boundary for '.', keeping the function
noexcept and using core::string_view operations so callers like serve_static can
rely on the updated behavior.
- Around line 152-155: The concatenation using path_cat(path, impl_->root,
req_path) lacks normalization and allows directory traversal; after building
path, resolve both impl_->root and the concatenated path to canonical forms
(e.g., std::filesystem::weakly_canonical or equivalent) and verify the resolved
file path is contained within the resolved root before serving; if the resolved
path lies outside the root (or std::filesystem::relative(resolvedPath,
resolvedRoot) begins with ".."), reject the request and return an error. Ensure
this check is performed immediately after path_cat and before any file access in
the serve_static logic.
- Around line 262-273: The loop reading from f.read and writing via rp.write
must detect partial reads or errors and abort the response instead of
unconditionally calling rp.end; after the loop check if ec is set or remaining
!= 0 (or if any rp.write failed/returned an error) and, in that case, propagate
the error by logging and closing/aborting the response/connection (e.g., call
rp.abort() or set an error status) rather than calling rp.end(); update the
logic around the read/write block (references: f.read, remaining, ec, rp.write,
rp.end) to ensure you only finalize the response when remaining == 0 and no
errors occurred.

In `@test/unit/server/statuses.cpp`:
- Around line 34-49: The test suite is missing coverage for HTTP status 305;
update the test_is_redirect() unit test to assert the current behavior of
statuses::is_redirect for 305 by adding a BOOST_TEST( statuses::is_redirect( 305
) ); (place it with the other "Redirect status codes" assertions inside
test_is_redirect) so the test verifies the implementation's handling of the Use
Proxy code.
🧹 Nitpick comments (3)
include/boost/http/server/encode_url.hpp (1)

13-15: Use the public Boost.Core string_view header in a public API.

Including <boost/core/detail/string_view.hpp> in a public header relies on an internal header. Prefer the public header to avoid stability issues across Boost releases.

♻️ Proposed change
-#include <boost/core/detail/string_view.hpp>
+#include <boost/core/string_view.hpp>
include/boost/http/server/serve_static.hpp (1)

13-41: Add explicit includes for used types.

This public header relies on transitive includes for std::uint32_t and core::string_view. Adding direct includes improves self-containment and reduces fragility.

♻️ Suggested include cleanup
 `#include` <boost/http/detail/config.hpp>
 `#include` <boost/http/server/route_handler.hpp>
+#include <boost/core/string_view.hpp>
+#include <cstdint>
src/server/mime_db.cpp (1)

20-72: Guard the MIME db sort invariant used by binary search.

lookup assumes the array is sorted case-insensitively. Consider a small static/assertion test (or unit test) to prevent accidental unsorted inserts from silently breaking lookups.

Comment on lines +1 to +5
---
description: Documentation style conventions for code snippets, javadocs, and API documentation examples
globs:
alwaysApply: false
---
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

Define globs or enable alwaysApply so the rule actually runs.

With alwaysApply: false and an empty globs, this rule likely won’t apply anywhere. Please add explicit doc globs or switch to alwaysApply: true if it should be global.

🤖 Prompt for AI Agents
In @.cursor/rules/doc.mdc around lines 1 - 5, The rule currently has an empty
globs array and alwaysApply: false so it never runs; update the rule by either
populating the globs key with the appropriate file patterns (e.g.,
doc/Javadoc/API examples patterns) or set alwaysApply: true if you intend the
rule to be global, ensuring the 'globs' and 'alwaysApply' fields in the rule
header are adjusted accordingly so the rule will actually execute (update the
'globs' array or toggle 'alwaysApply' in the doc.mdc rule definition).

Comment on lines +35 to +40
@par Example
@code
std::string content = "Hello, World!";
std::string tag = etag( content );
// tag == "\"d-3/1gIbsr1bCvZ2KQgJ7DpTGR3YH\""
@endcode
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

Documentation example may not match actual output format.

The example shows "\"d-3/1gIbsr1bCvZ2KQgJ7DpTGR3YH\"" but the implementation in etag.cpp produces a format like "<size_hex>-<16_hex_chars>" using FNV-1a hash converted to hex. The slash character and base64-like encoding shown here don't match the hex-based implementation.

Consider updating the example to reflect the actual output format, or mark it as illustrative only.

🤖 Prompt for AI Agents
In `@include/boost/http/server/etag.hpp` around lines 35 - 40, The documentation
example for etag() is inconsistent with the implementation in etag.cpp: replace
the base64/slash-style example with one that matches the actual output format
produced by etag (size in hex, a hyphen, then the 16-hex-character FNV-1a hash),
e.g. something like "\"<size_hex>-<16_hex_chars>\"" or, alternatively, add a
note that the example is illustrative only; update the docblock around the etag
function to reflect this change.

Comment on lines +102 to +116
@par Example
@code
route_task serve( route_params& rp, std::string path )
{
send_file_info info;
send_file_init( info, rp, path );
if( info.result == send_file_result::not_modified )
{
rp.status( status::not_modified );
co_return co_await rp.send( "" );
}
if( info.result != send_file_result::ok )
co_return route::next;
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:

#!/bin/bash
# Locate route_params and check for any status helper
route_params_files=$(rg -l 'struct route_params|class route_params' --type cpp --type hpp)
echo "Files containing route_params definition:"
echo "$route_params_files"
echo ""
echo "Searching for status-related methods:"
rg -n 'status\s*\(' $route_params_files | head -30

Repository: cppalliance/http

Length of output: 2499


🏁 Script executed:

#!/bin/bash
# Search for route_params definition in header files
rg 'route_params' include/ -t rust -A 10 | head -100

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Find route_params definition
find include -name "*.h" -o -name "*.hpp" | xargs grep -l "route_params"
echo ""
echo "Looking for route_params struct/class definition:"
rg 'struct route_params|class route_params' include/
echo ""
echo "Looking for status method in route_params context:"
rg -B 5 -A 15 'struct route_params|class route_params' include/

Repository: cppalliance/http

Length of output: 6588


🏁 Script executed:

#!/bin/bash
# Get the full route_params_base class definition
cat include/boost/http/server/router_types.hpp | grep -n "class route_params_base" -A 200 | head -250

Repository: cppalliance/http

Length of output: 2797


🏁 Script executed:

#!/bin/bash
# Check route_handler.hpp for status method
sed -n '140,170p' include/boost/http/server/route_handler.hpp

Repository: cppalliance/http

Length of output: 1003


🏁 Script executed:

#!/bin/bash
# Find route_params class definition (not _base)
rg -n 'class route_params[^_]|struct route_params[^_]' include/ -A 15

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for route_params definition/typedef
rg -n 'using route_params|typedef.*route_params|route_params =' include/
echo ""
echo "Check what route_params is passed to the status() method:"
rg -B 5 -A 10 'status\(http::status' include/boost/http/server/route_handler.hpp

Repository: cppalliance/http

Length of output: 1024


🏁 Script executed:

#!/bin/bash
# Check the full send_file.hpp file structure
head -50 include/boost/http/server/send_file.hpp
echo "---"
rg -n 'template.*route_params|route_params.*template' include/boost/http/server/ -A 5

Repository: cppalliance/http

Length of output: 1298


🏁 Script executed:

#!/bin/bash
# Check the send_file.cpp implementation
grep -n "status" src/server/send_file.cpp | head -20
echo ""
echo "Show context around set_status usage:"
sed -n '150,195p' src/server/send_file.cpp

Repository: cppalliance/http

Length of output: 1938


🏁 Script executed:

#!/bin/bash
# Check if rp.status() is actually used in implementations
echo "Search for rp.status() usage in source files:"
rg 'rp\.status\s*\(' src/ --type cpp
echo ""
echo "Compare with rp.res.set_status() usage:"
rg 'rp\.res\.set_status\s*\(' src/ --type cpp

Repository: cppalliance/http

Length of output: 1063


Update doc example to use actual implementation pattern

The example at lines 109–111 uses rp.status(status::not_modified), but the actual implementation consistently uses rp.res.set_status(status::...) throughout send_file.cpp and other implementations. Change the example to:

if( info.result == send_file_result::not_modified )
{
    rp.res.set_status( status::not_modified );
    co_return co_await rp.send( "" );
}
🤖 Prompt for AI Agents
In `@include/boost/http/server/send_file.hpp` around lines 102 - 116, The doc
example uses rp.status(status::not_modified) which is inconsistent with the
implementation; update the example in the send_file.hpp snippet so that when
send_file_info::result == send_file_result::not_modified it calls
rp.res.set_status(status::not_modified) instead of rp.status(...), preserving
the co_return co_await rp.send("") flow and keeping references to
send_file_info, send_file_init, and send_file_result to match the real
implementation pattern.

Comment on lines 59 to 66
// Format: size-hash
char buf[32];
std::snprintf( buf, sizeof( buf ), "%zx-", body.size() );
result.append( buf );

char hex[16];
to_hex( hash, hex );
result.append( hex, 16 );
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

Resolve CI -Wrestrict failure in ETag formatting.

CI reports a -Werror on std::char_traits::copy triggered by the current append path (Line 61, Line 86). Switching to length-aware append avoids the append(const char*) overload and should silence the overlap warning while keeping the formatting logic intact.

🛠️ Proposed fix (use length-aware append)
-    std::snprintf( buf, sizeof( buf ), "%zx-", body.size() );
-    result.append( buf );
+    int n = std::snprintf( buf, sizeof( buf ), "%zx-", body.size() );
+    if( n > 0 && n < static_cast<int>( sizeof( buf ) ) )
+        result.append( buf, static_cast<std::size_t>( n ) );
@@
-    std::snprintf( buf, sizeof( buf ),
-        "%llx-%llx",
-        static_cast<unsigned long long>( size ),
-        static_cast<unsigned long long>( mtime ) );
-    result.append( buf );
+    int n = std::snprintf( buf, sizeof( buf ),
+        "%llx-%llx",
+        static_cast<unsigned long long>( size ),
+        static_cast<unsigned long long>( mtime ) );
+    if( n > 0 && n < static_cast<int>( sizeof( buf ) ) )
+        result.append( buf, static_cast<std::size_t>( n ) );

Also applies to: 85-90

🤖 Prompt for AI Agents
In `@src/server/etag.cpp` around lines 59 - 66, The append calls that pass
C-strings are hitting the append(const char*) overload and triggering
-Wrestrict; update them to use the length-aware overload instead: when writing
the size prefix built into buf (in the ETag formatting code that uses
std::snprintf and buf) call result.append(buf, std::strlen(buf)) (or capture
snprintf's return and append that many bytes) rather than result.append(buf),
and make the same replacement for the other occurrence in the file (the hex
append already uses result.append(hex, 16) but ensure any other
result.append(...) calls that take a raw char* use the length-aware overload);
reference symbols: result.append, buf, hex, to_hex, body.size(), hash.

Comment on lines +139 to +173
// Handle Range header
auto range_header = rp.req.value_or(field::range, "");
if(! range_header.empty())
{
auto range_result = parse_range(
static_cast<std::int64_t>(info.size),
range_header);

if(range_result.type == range_result_type::ok &&
! range_result.ranges.empty())
{
// Use first range only (simplification)
auto const& range = range_result.ranges[0];
info.is_range = true;
info.range_start = range.start;
info.range_end = range.end;

// Set 206 Partial Content
rp.res.set_status(status::partial_content);

auto const content_length =
range.end - range.start + 1;
rp.res.set_payload_size(
static_cast<std::uint64_t>(content_length));

// Content-Range header
std::string cr = "bytes " +
std::to_string(range.start) + "-" +
std::to_string(range.end) + "/" +
std::to_string(info.size);
rp.res.set(field::content_range, cr);

info.result = send_file_result::ok;
return;
}
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

Avoid returning 206 for multi‑range requests unless multipart is implemented.

Right now the first range is served even when the request has multiple ranges. That can mislead clients expecting a multipart/byteranges response. If you don’t plan to implement multipart, ignore the Range header when more than one range is requested and fall back to full content.

🔧 Suggested adjustment
-        if(range_result.type == range_result_type::ok &&
-            ! range_result.ranges.empty())
+        if(range_result.type == range_result_type::ok &&
+            range_result.ranges.size() == 1)
         {
             // Use first range only (simplification)
             auto const& range = range_result.ranges[0];
             info.is_range = true;
             info.range_start = range.start;
             info.range_end = range.end;
@@
             info.result = send_file_result::ok;
             return;
         }
🤖 Prompt for AI Agents
In `@src/server/send_file.cpp` around lines 139 - 173, The code currently accepts
the first range from parse_range even when multiple ranges are requested; change
the logic inside the Range handling block (the branch using parse_range,
range_result_type::ok, and range_result.ranges) to only honor the Range when
range_result.ranges.size() == 1; if there are >1 ranges, treat the header as
absent (do not set info.is_range, do not set
rp.res.set_status(status::partial_content), do not set Content-Range, and let
the normal full-content path run), since multipart/byteranges is not
implemented; keep existing behavior for the single-range case (setting
info.range_start/end and send_file_result::ok).

Comment on lines +55 to +67
bool
is_dotfile(core::string_view path) noexcept
{
auto pos = path.rfind('/');
if(pos == core::string_view::npos)
pos = 0;
else
++pos;

if(pos < path.size() && path[pos] == '.')
return true;

return false;
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

Dotfile detection only checks the final path segment.

Requests like /.git/config will bypass the current check because only the last segment is inspected. Consider scanning all path segments (or explicitly exempting .well-known if that’s intended).

🔧 Suggested fix
 bool
 is_dotfile(core::string_view path) noexcept
 {
-    auto pos = path.rfind('/');
-    if(pos == core::string_view::npos)
-        pos = 0;
-    else
-        ++pos;
-
-    if(pos < path.size() && path[pos] == '.')
-        return true;
-
-    return false;
+    std::size_t start = 0;
+    while(start <= path.size())
+    {
+        auto end = path.find('/', start);
+        if(end == core::string_view::npos)
+            end = path.size();
+        if(end > start && path[start] == '.')
+            return true;
+        start = end + 1;
+    }
+    return false;
 }
📝 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
bool
is_dotfile(core::string_view path) noexcept
{
auto pos = path.rfind('/');
if(pos == core::string_view::npos)
pos = 0;
else
++pos;
if(pos < path.size() && path[pos] == '.')
return true;
return false;
bool
is_dotfile(core::string_view path) noexcept
{
std::size_t start = 0;
while(start <= path.size())
{
auto end = path.find('/', start);
if(end == core::string_view::npos)
end = path.size();
if(end > start && path[start] == '.')
return true;
start = end + 1;
}
return false;
}
🤖 Prompt for AI Agents
In `@src/server/serve_static.cpp` around lines 55 - 67, The is_dotfile function
only checks the last path segment and thus misses dotfiles in earlier segments
(e.g., "/.git/config"); update is_dotfile to scan all path segments of the input
core::string_view path and return true if any segment begins with a '.' (except
explicitly allowlisting ".well-known" if that is desired); modify the logic in
is_dotfile to iterate over segments split by '/' (or walk the string looking for
'/' boundaries) and check the first character after each boundary for '.',
keeping the function noexcept and using core::string_view operations so callers
like serve_static can rely on the updated behavior.

Comment on lines +152 to +155
// Build the file path
std::string path;
path_cat(path, impl_->root, req_path);

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

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the serve_static.cpp file
find . -name "serve_static.cpp" -type f

Repository: cppalliance/http

Length of output: 125


🏁 Script executed:

# Read the file to see lines 152-155 and surrounding context
wc -l src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 91


🏁 Script executed:

# Get the specific lines in question with surrounding context
sed -n '140,170p' src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 995


🏁 Script executed:

# Find the path_cat function definition
rg -A 10 "^\s*void\s+path_cat|^\s*std::string\s+path_cat|^path_cat" src/

Repository: cppalliance/http

Length of output: 593


🏁 Script executed:

# Search for any path normalization or canonicalization in the file
rg -i "canonical|normalize|normalizePath" src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Get more of the path_cat function
sed -n '1,50p' src/server/serve_static.cpp | tail -30

Repository: cppalliance/http

Length of output: 723


🏁 Script executed:

# Find where path_cat is defined - search more broadly
rg -B 2 -A 20 "path_cat\(" src/server/serve_static.cpp | head -60

Repository: cppalliance/http

Length of output: 1308


🏁 Script executed:

# Check if there are any validation checks after path_cat is called
sed -n '152,200p' src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 1396


🏁 Script executed:

# Look for any usage of std::filesystem::canonical or similar normalization
rg "std::filesystem::" src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 119


🏁 Script executed:

# Check the includes to see what filesystem functions are available
head -30 src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 809


🏁 Script executed:

# Look at the complete path_cat function to see the full implementation
rg -A 30 "void\s+path_cat" src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check the send_file_init function to see if it does any path validation
rg -B 2 -A 30 "send_file_init" src/server/serve_static.cpp | head -50

Repository: cppalliance/http

Length of output: 1436


🏁 Script executed:

# Search for send_file.hpp to see what validation send_file_init does
find . -name "send_file.hpp" -o -name "send_file.cpp" | head -5

Repository: cppalliance/http

Length of output: 128


🏁 Script executed:

# Check if send_file_init does any path validation or canonicalization
rg "send_file_init" src/server/send_file.cpp src/server/send_file.hpp 2>/dev/null | head -20

Repository: cppalliance/http

Length of output: 100


🏁 Script executed:

# Look for any access control or path validation in the serve_static implementation
rg -i "access|permission|traverse|escape|outside|root" src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 327


🏁 Script executed:

# Get the complete path_cat function
sed -n '24,60p' src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 841


🏁 Script executed:

# Look at send_file_init implementation
sed -n '1,100p' src/server/send_file.cpp

Repository: cppalliance/http

Length of output: 2416


🏁 Script executed:

# Check send_file.hpp for function signature and documentation
cat include/boost/http/server/send_file.hpp | head -80

Repository: cppalliance/http

Length of output: 1889


🏁 Script executed:

# Search for any canonicalization or path normalization in send_file
rg "canonical|normalize" src/server/send_file.cpp

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check where req_path comes from and if it's validated
rg "req_path" src/server/serve_static.cpp | head -20

Repository: cppalliance/http

Length of output: 269


🏁 Script executed:

# Look at the complete operator() function to see the full flow
sed -n '100,160p' src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 1531


🏁 Script executed:

# Check if there's any ".." handling or path traversal defense
rg "\\.\\./" src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check if req_path is URL decoded and how
rg -B 5 -A 5 "req_path" src/server/serve_static.cpp | head -40

Repository: cppalliance/http

Length of output: 1134


🏁 Script executed:

# Check the full operator() function to see if there are any other validations
sed -n '110,280p' src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 4666


🏁 Script executed:

# Check the get_file_stats function in send_file.cpp more carefully
sed -n '25,60p' src/server/send_file.cpp

Repository: cppalliance/http

Length of output: 971


🏁 Script executed:

# Verify: does std::filesystem::is_directory resolve symlinks?
# Let's check if there's any relative_to or other containment check
rg "relative_to|is_within|inside|contains" src/

Repository: cppalliance/http

Length of output: 257


🏁 Script executed:

# Check if the path variable is ever validated against root after concatenation
sed -n '150,180p' src/server/serve_static.cpp

Repository: cppalliance/http

Length of output: 906


Add path containment validation to prevent directory traversal attacks.

path_cat concatenates the root directory and request path without normalization or validation. Request paths containing .. segments or symlinks pointing outside the root can bypass the intended directory boundary. Implement explicit path containment checks before serving any file:

  1. Resolve the concatenated path to its canonical form
  2. Verify the resolved path is within the root directory
  3. Reject requests that attempt to escape the root

The suggested approach using weakly_canonical path comparison is valid, or alternatively use std::filesystem::relative() and validate the result does not start with ...

🤖 Prompt for AI Agents
In `@src/server/serve_static.cpp` around lines 152 - 155, The concatenation using
path_cat(path, impl_->root, req_path) lacks normalization and allows directory
traversal; after building path, resolve both impl_->root and the concatenated
path to canonical forms (e.g., std::filesystem::weakly_canonical or equivalent)
and verify the resolved file path is contained within the resolved root before
serving; if the resolved path lies outside the root (or
std::filesystem::relative(resolvedPath, resolvedRoot) begins with ".."), reject
the request and return an error. Ensure this check is performed immediately
after path_cat and before any file access in the serve_static logic.

Comment on lines +262 to +273
while(remaining > 0)
{
auto const to_read = static_cast<std::size_t>(
(std::min)(remaining, static_cast<std::int64_t>(buf_size)));

auto const n = f.read(buffer, to_read, ec);
if(ec || n == 0)
break;

co_await rp.write(capy::const_buffer(buffer, n));
remaining -= static_cast<std::int64_t>(n);
}
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

🧩 Analysis chain

🏁 Script executed:

# First, find the file and examine context around the problematic lines
find . -name "serve_static.cpp" -type f

Repository: cppalliance/http

Length of output: 125


🏁 Script executed:

# Get the full context around lines 262-273 to understand Content-Length handling
cat -n src/server/serve_static.cpp | sed -n '240,290p'

Repository: cppalliance/http

Length of output: 1421


🏁 Script executed:

# Search for route_params definition and error handling APIs
rg -nP 'struct\s+route_params\b|class\s+route_params\b' -g '*.hpp' -g '*.cpp' -g '*.ipp' | head -20

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Search for error/abort/fail methods
rg -nP '\b(route_params::)?(send|write|end|fail|abort|cancel)\s*\(' -g '*.hpp' -g '*.cpp' -g '*.ipp' | head -30

Repository: cppalliance/http

Length of output: 2145


🏁 Script executed:

# Search for Content-Length in the serve_static file
rg -n "Content-Length|content.length" src/server/serve_static.cpp -i

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Look at the function signature and beginning to see where Content-Length is set
cat -n src/server/serve_static.cpp | sed -n '100,260p'

Repository: cppalliance/http

Length of output: 5553


🏁 Script executed:

# Find route_params class definition more broadly
fd -e hpp -e h | xargs rg -l "route_params" | head -10

Repository: cppalliance/http

Length of output: 460


🏁 Script executed:

# Search for route_params definition in those files
fd -e hpp -e h | xargs rg -A 20 "struct\s+route_params|class\s+route_params" | head -100

Repository: cppalliance/http

Length of output: 5579


🏁 Script executed:

# Search for send_file_init to understand what headers it sets
rg -n "send_file_init" -A 30 src/server/send_file.cpp | head -100

Repository: cppalliance/http

Length of output: 841


🏁 Script executed:

# Find route_params struct definition (not base)
rg -n "^struct route_params|^class route_params" -A 30 include/boost/http/server/router_types.hpp | head -80

Repository: cppalliance/http

Length of output: 2279


🏁 Script executed:

# Search for rp.end() and rp.write() in include files to understand the API
rg -n "\.end\(\)|\.write\(" include/boost/http/server/router_types.hpp -B 2 -A 5 | head -100

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Search for route_params with write, end, send methods
rg -n "route_params" include/boost/http/server/route_handler.hpp -A 5 -B 2 | head -150

Repository: cppalliance/http

Length of output: 2818


🏁 Script executed:

# Find where Content-Length is actually set during send_file_init
rg -n "Content-Length|content-length|set_header" src/server/send_file.cpp -i -B 2 -A 2 | head -80

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Look at the full send_file_init function
cat -n src/server/send_file.cpp | sed -n '75,250p'

Repository: cppalliance/http

Length of output: 4229


🏁 Script executed:

# Find where rp.write() and rp.end() are implemented
rg -n "route_params::write|route_params::end" include/boost/http/server/ -B 2 -A 10 | head -100

Repository: cppalliance/http

Length of output: 983


🏁 Script executed:

# Search for write() method in route_handler.hpp
rg -n "write(" include/boost/http/server/route_handler.hpp -B 3 -A 15 | head -150

Repository: cppalliance/http

Length of output: 122


🏁 Script executed:

# Look at the implementation of write and end in the header
cat -n include/boost/http/server/route_handler.hpp | sed -n '200,400p'

Repository: cppalliance/http

Length of output: 3457


🏁 Script executed:

# Search for set_payload_size to confirm it sets Content-Length
rg -n "set_payload_size" include/boost/http/server/ -B 2 -A 5

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check if there's any error recovery or validation after the streaming loop
cat -n src/server/serve_static.cpp | sed -n '262,276p'

Repository: cppalliance/http

Length of output: 572


🏁 Script executed:

# Search for how write() and end() handle incomplete responses with Content-Length
rg -n "Content-Length|payload_size" src/server/ -B 3 -A 3 | grep -A 5 -B 5 "write\|end"

Repository: cppalliance/http

Length of output: 1145


🏁 Script executed:

# Check if route_params has any error/abort mechanism we missed
rg -n "route_result|abort|cancel|fail|error" include/boost/http/server/route_handler.hpp -B 2 -A 3 | head -80

Repository: cppalliance/http

Length of output: 1462


🏁 Script executed:

# Check what route_result enum contains
rg -n "enum.*route_result|enum class route_result" include/boost/http/server/ -A 10

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Search for how the response headers are committed/finalized
rg -n "headers.*sent|commit|finalize" src/server/ -i -B 2 -A 2

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Find route_result definition
rg -n "route_result" include/boost/http/server/router_types.hpp -B 2 -A 10 | head -100

Repository: cppalliance/http

Length of output: 1082


🏁 Script executed:

# Find route namespace and route::next
rg -n "namespace route|route::next" include/boost/http/server/ -B 2 -A 5 | head -80

Repository: cppalliance/http

Length of output: 5615


🏁 Script executed:

# Check what happens with remaining > 0 after the loop breaks
rg -n "remaining" src/server/serve_static.cpp -B 3 -A 3

Repository: cppalliance/http

Length of output: 766


🏁 Script executed:

# Check if write() can return errors that should be checked
rg -n "co_await rp.write" src/server/serve_static.cpp -B 2 -A 2

Repository: cppalliance/http

Length of output: 214


🏁 Script executed:

# Look at how end() behaves and what happens if write fails
cat -n include/boost/http/server/route_handler.hpp | sed -n '240,290p'

Repository: cppalliance/http

Length of output: 1779


🏁 Script executed:

# Check test file to see how errors are handled in streaming scenarios
rg -n "write|error|break" test/unit/server/serve_static.cpp -B 2 -A 2 | head -100

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check if rp.end() is documented to handle incomplete writes
rg -n "incomplete|truncated|mismatch" include/boost/http/server/ -i -B 2 -A 2

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Look for how other parts of the codebase handle streaming with Content-Length
rg -n "set_payload_size" src/server/ -B 5 -A 15 | head -120

Repository: cppalliance/http

Length of output: 2848


🏁 Script executed:

# Check if write() returns are checked anywhere else in codebase
rg -n "co_await.*write\(" src/ -B 1 -A 1 | grep -A 1 -B 1 "if\|error\|ec"

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check when headers are actually sent/committed
rg -n "write_impl|end()" src/server/ -B 5 -A 15 | grep -A 10 -B 10 "header\|commit\|send" | head -150

Repository: cppalliance/http

Length of output: 8067


Check for read errors and validate complete transmission before finalizing the response.

If f.read() sets ec or returns 0 before all bytes are written, the loop exits but the response headers with Content-Length are already committed. Calling rp.end() unconditionally then produces a truncated response that violates the HTTP protocol. Check that remaining == 0 after the loop or propagate the read error; alternatively, detect write failures and abort the response/connection.

🤖 Prompt for AI Agents
In `@src/server/serve_static.cpp` around lines 262 - 273, The loop reading from
f.read and writing via rp.write must detect partial reads or errors and abort
the response instead of unconditionally calling rp.end; after the loop check if
ec is set or remaining != 0 (or if any rp.write failed/returned an error) and,
in that case, propagate the error by logging and closing/aborting the
response/connection (e.g., call rp.abort() or set an error status) rather than
calling rp.end(); update the logic around the read/write block (references:
f.read, remaining, ec, rp.write, rp.end) to ensure you only finalize the
response when remaining == 0 and no errors occurred.

Comment on lines +34 to +49
void
test_is_redirect()
{
// Redirect status codes
BOOST_TEST( statuses::is_redirect( 300 ) );
BOOST_TEST( statuses::is_redirect( 301 ) );
BOOST_TEST( statuses::is_redirect( 302 ) );
BOOST_TEST( statuses::is_redirect( 303 ) );
BOOST_TEST( statuses::is_redirect( 307 ) );
BOOST_TEST( statuses::is_redirect( 308 ) );

// Not redirects
BOOST_TEST( ! statuses::is_redirect( 200 ) );
BOOST_TEST( ! statuses::is_redirect( 304 ) ); // Not Modified is not a redirect
BOOST_TEST( ! statuses::is_redirect( 404 ) );
}
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

Missing test coverage for status code 305 (Use Proxy).

The implementation in src/server/statuses.cpp returns true for is_redirect(305), but this test doesn't verify that behavior. Consider adding:

 BOOST_TEST( statuses::is_redirect( 303 ) );
+BOOST_TEST( statuses::is_redirect( 305 ) );
 BOOST_TEST( statuses::is_redirect( 307 ) );
📝 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
void
test_is_redirect()
{
// Redirect status codes
BOOST_TEST( statuses::is_redirect( 300 ) );
BOOST_TEST( statuses::is_redirect( 301 ) );
BOOST_TEST( statuses::is_redirect( 302 ) );
BOOST_TEST( statuses::is_redirect( 303 ) );
BOOST_TEST( statuses::is_redirect( 307 ) );
BOOST_TEST( statuses::is_redirect( 308 ) );
// Not redirects
BOOST_TEST( ! statuses::is_redirect( 200 ) );
BOOST_TEST( ! statuses::is_redirect( 304 ) ); // Not Modified is not a redirect
BOOST_TEST( ! statuses::is_redirect( 404 ) );
}
void
test_is_redirect()
{
// Redirect status codes
BOOST_TEST( statuses::is_redirect( 300 ) );
BOOST_TEST( statuses::is_redirect( 301 ) );
BOOST_TEST( statuses::is_redirect( 302 ) );
BOOST_TEST( statuses::is_redirect( 303 ) );
BOOST_TEST( statuses::is_redirect( 305 ) );
BOOST_TEST( statuses::is_redirect( 307 ) );
BOOST_TEST( statuses::is_redirect( 308 ) );
// Not redirects
BOOST_TEST( ! statuses::is_redirect( 200 ) );
BOOST_TEST( ! statuses::is_redirect( 304 ) ); // Not Modified is not a redirect
BOOST_TEST( ! statuses::is_redirect( 404 ) );
}
🤖 Prompt for AI Agents
In `@test/unit/server/statuses.cpp` around lines 34 - 49, The test suite is
missing coverage for HTTP status 305; update the test_is_redirect() unit test to
assert the current behavior of statuses::is_redirect for 305 by adding a
BOOST_TEST( statuses::is_redirect( 305 ) ); (place it with the other "Redirect
status codes" assertions inside test_is_redirect) so the test verifies the
implementation's handling of the Use Proxy code.

@cppalliance-bot
Copy link

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

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

2026-01-19 23:46:27 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: 3

Caution

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

⚠️ Outside diff range comments (1)
include/boost/http/server/cors.hpp (1)

13-16: Add <string> include for std::string types.

The cors_options struct uses std::string (lines 26, 29, 32, 35), but this header doesn't explicitly include <string>. While currently available through transitive includes (route_handler.hpprouter_types.hpp<string>), relying on transitive includes is fragile and violates the principle that headers should explicitly include all dependencies they use.

Proposed fix
 `#include` <boost/http/server/route_handler.hpp>
 `#include` <boost/http/status.hpp>
 `#include` <chrono>
+#include <string>
🤖 Fix all issues with AI agents
In `@src/server/send_file.cpp`:
- Around line 187-192: When info.size is 0 the code sets info.range_end to -1
which can break callers; update the block that sets
rp.res.set_payload_size(info.size), info.range_start, info.range_end and
info.result so it special-cases empty files: if info.size == 0 set
rp.res.set_payload_size(0), info.range_start = 0 and info.range_end = 0 (instead
of -1) and leave info.result = send_file_result::ok, otherwise keep the existing
logic; adjust the handling in the send_file_result::ok branch (use symbols
rp.res, info.range_start, info.range_end, info.size, send_file_result::ok) so
callers receive a non-negative range for empty files.
- Around line 48-54: Replace the manual "now-offset" conversion around sctp and
mtime: instead of computing sctp via ftime - file_time_type::clock::now() +
system_clock::now(), use the standard C++20 conversion to system_clock (e.g.,
std::chrono::clock_cast<std::chrono::system_clock>(ftime) or
std::filesystem::file_time_type::clock::to_sys/
std::filesystem::file_clock::to_sys(ftime) depending on your standard library)
and then compute mtime with std::chrono::system_clock::to_time_t(...) from that
converted time_point; update the code that sets sctp and mtime (references:
sctp, mtime, ftime, std::filesystem::file_time_type) to use the
clock_cast/to_sys approach for accurate epoch translation.

In `@src/server/serve_static.cpp`:
- Around line 232-242: The TOCTOU bug occurs because send_file_init performs
statting/headers but the file is actually opened later with capy::file::open in
this block; to fix, open the file before calling send_file_init (or merge open
into send_file_init) so the same file descriptor is used for stat/size and
header generation, and if open fails handle errors consistently (respect
impl_->opts.fallthrough and set rp.res/status before calling rp.send). Update
the flow around send_file_init and the capy::file f.open call (and related error
handling that returns route::next or uses rp.send) so headers are only set after
a successful open or so that open failures roll back any partial response
headers.
♻️ Duplicate comments (10)
include/boost/http/server/encode_url.hpp (1)

20-39: Clarify component-vs-full-URL encoding semantics.

The doc still says “URL or URL component,” but reserved characters are preserved, which is incorrect for generic components. Please either document this as full-URL encoding only or provide a component-safe variant.

🔧 Minimal doc clarification (if keeping current behavior)
-    `@param` url The URL or URL component to encode.
+    `@param` url The full URL to encode (reserved characters are preserved).
src/server/fresh.cpp (2)

46-56: Substring ETag matching can return false positives.

The find() call on line 49 can match partial tokens (e.g., "abc" inside "xabc") leading to incorrect 304 responses. This should parse the comma-separated list and perform exact token comparison.


64-76: Lexicographic date comparison does not handle RFC 7231's multiple date formats.

RFC 7231 allows three date formats (IMF-fixdate, RFC 850, and asctime). A simple string comparison will produce incorrect results when the client and server use different formats. Proper HTTP date parsing should be implemented.

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

35-40: Update ETag example to match hex output.

Line 39 shows a base64-like value, but the implementation formats "size_hex-<16_hex_chars>" (see src/server/etag.cpp). Please update the example or mark it illustrative.

🛠️ Suggested doc tweak
-    // tag == "\"d-3/1gIbsr1bCvZ2KQgJ7DpTGR3YH\""
+    // tag == "\"d-<16_hex_chars>\"" // format: "<size_hex>-<hash_hex>"
src/server/etag.cpp (1)

60-62: Avoid append(const char*) to silence -Wrestrict.

Lines 61–62 and 90 append buf via the C-string overload, which triggers -Wrestrict in CI. Use the length-aware overload with snprintf’s return.

🛠️ Proposed fix (length-aware append)
-    std::snprintf( buf, sizeof( buf ), "%zx-", body.size() );
-    result.append( buf );
+    int n = std::snprintf( buf, sizeof( buf ), "%zx-", body.size() );
+    if( n > 0 && n < static_cast<int>( sizeof( buf ) ) )
+        result.append( buf, static_cast<std::size_t>( n ) );
@@
-    std::snprintf( buf, sizeof( buf ),
-        "%llx-%llx",
-        static_cast<unsigned long long>( size ),
-        static_cast<unsigned long long>( mtime ) );
-    result.append( buf );
+    int n = std::snprintf( buf, sizeof( buf ),
+        "%llx-%llx",
+        static_cast<unsigned long long>( size ),
+        static_cast<unsigned long long>( mtime ) );
+    if( n > 0 && n < static_cast<int>( sizeof( buf ) ) )
+        result.append( buf, static_cast<std::size_t>( n ) );

Also applies to: 85-90

src/server/mime_types.cpp (1)

27-81: Fix ext_db ordering for binary search.

Line 80 puts "7z" after "zip", which breaks the sorted order and can make lookup_ext miss entries.

🛠️ Proposed fix (reorder entry)
 constexpr ext_entry ext_db[] = {
+    { "7z", "application/x-7z-compressed" },
     { "aac", "audio/aac" },
     { "avif", "image/avif" },
@@
-    { "zip", "application/zip" },
-    { "7z", "application/x-7z-compressed" },
+    { "zip", "application/zip" },
 };
src/server/send_file.cpp (1)

147-173: Avoid returning 206 for multi-range requests unless multipart is implemented.

The code serves only the first range even when multiple ranges are requested, which can mislead clients expecting a multipart/byteranges response. Consider restricting to single-range requests only.

🔧 Suggested adjustment
-        if(range_result.type == range_result_type::ok &&
-            ! range_result.ranges.empty())
+        if(range_result.type == range_result_type::ok &&
+            range_result.ranges.size() == 1)
         {
             // Use first range only (simplification)
src/server/serve_static.cpp (3)

55-68: Dotfile detection only checks the final path segment.

Requests like /.git/config will bypass the current check because only the last segment is inspected. All path segments should be scanned for dotfiles.


152-155: Add path containment validation to prevent directory traversal attacks.

path_cat concatenates the root directory and request path without normalization or validation. Request paths containing .. segments can escape the root directory. Resolve the concatenated path to canonical form and verify it remains within the root.


262-275: Check for read errors and validate complete transmission before finalizing the response.

If f.read() sets ec or returns 0 before all bytes are written, the loop exits but response headers with Content-Length are already committed. Calling rp.end() unconditionally produces a truncated response violating HTTP protocol.

🔧 Suggested fix
     while(remaining > 0)
     {
         auto const to_read = static_cast<std::size_t>(
             (std::min)(remaining, static_cast<std::int64_t>(buf_size)));

         auto const n = f.read(buffer, to_read, ec);
         if(ec || n == 0)
             break;

         co_await rp.write(capy::const_buffer(buffer, n));
         remaining -= static_cast<std::int64_t>(n);
     }

-    co_return co_await rp.end();
+    // Check if we sent all expected bytes
+    if(remaining > 0)
+    {
+        // Truncated response - connection should be closed
+        // Log error or handle appropriately
+        co_return route::error;  // or appropriate error handling
+    }
+
+    co_return co_await rp.end();
🧹 Nitpick comments (5)
include/boost/http/server/statuses.hpp (1)

87-103: LGTM!

The selection of 502, 503, and 504 as retryable codes is a sensible conservative choice focusing on gateway/server transient failures.

Optional consideration: Some applications also treat 408 (Request Timeout) and 429 (Too Many Requests) as retryable. If broader retry semantics are desired in the future, these could be considered for inclusion or offered as a separate predicate. The current focused set is reasonable for the stated purpose.

src/server/send_file.cpp (1)

66-70: Consider checking strftime return value.

std::strftime returns 0 on failure (buffer too small or encoding error). While the 64-byte buffer is sufficient for this format, defensive code would check the return value.

♻️ Suggested defensive check
     char buf[64];
-    std::strftime(buf, sizeof(buf),
+    auto len = std::strftime(buf, sizeof(buf),
         "%a, %d %b %Y %H:%M:%S GMT", &tm);
-    return std::string(buf);
+    if(len == 0)
+        return std::string();  // or a fallback
+    return std::string(buf, len);
include/boost/http/server/serve_static.hpp (1)

79-119: Consider explicitly deleting copy operations for clarity.

The class has a move constructor but no copy constructor or assignment operators. While the raw impl_ pointer makes copying unsafe, explicitly deleting copy operations would make the intent clearer and prevent accidental implicit copies.

♻️ Suggested addition
     /** Move constructor.
     */
     serve_static(serve_static&& other) noexcept;
+
+    /** Deleted copy constructor.
+    */
+    serve_static(serve_static const&) = delete;
+
+    /** Deleted copy assignment.
+    */
+    serve_static& operator=(serve_static const&) = delete;
+
+    /** Move assignment operator.
+    */
+    serve_static& operator=(serve_static&& other) noexcept;
src/server/serve_static.cpp (2)

106-111: Move constructor leaves source in potentially dangerous state.

While setting other.impl_ to nullptr is correct, the destructor will call delete nullptr which is safe. However, the class lacks a move assignment operator, which could lead to resource leaks if assigned post-construction.

♻️ Add move assignment operator
 serve_static::
 serve_static(serve_static&& other) noexcept
     : impl_(other.impl_)
 {
     other.impl_ = nullptr;
 }
+
+serve_static&
+serve_static::
+operator=(serve_static&& other) noexcept
+{
+    if(this != &other)
+    {
+        delete impl_;
+        impl_ = other.impl_;
+        other.impl_ = nullptr;
+    }
+    return *this;
+}

220-226: Cache-Control header may be set twice.

send_file_init already sets Cache-Control: public, max-age=N when opts.max_age > 0. This code then overwrites it with the immutable variant. While functionally correct, the duplicate set operation is inefficient and could be consolidated.

Consider passing the immutable flag to send_file_init or setting Cache-Control only once after send_file_init returns.

Comment on lines +48 to +54
// Convert to Unix timestamp
auto const sctp = std::chrono::time_point_cast<
std::chrono::system_clock::duration>(
ftime - std::filesystem::file_time_type::clock::now() +
std::chrono::system_clock::now());
mtime = static_cast<std::uint64_t>(
std::chrono::system_clock::to_time_t(sctp));
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

🧩 Analysis chain

🌐 Web query:

C++20 std::filesystem::file_time_type to time_t conversion best practice

💡 Result:

Best practice (C++20)

  • Convert file_time_type -> system_clock via file_clock utilities (prefer std::chrono::clock_cast when available), then to time_t:

    std::time_t to_time_t(std::filesystem::file_time_type ft)
    {
    using namespace std::chrono;
    auto st = clock_cast<system_clock>(ft); // or file_clock::to_sys(ft)
    return system_clock::to_time_t(st);
    }

This preserves correct epoch/translation implemented by the library (no manual epoch math). (docs.w3cub.com)

Fallbacks for pre-C++20

  • There is no fully portable, guaranteed conversion before C++20. Common pragmatic options are:
    • Convert the other way: convert your time_t to file_time_type (safer for comparisons) if your implementation supports it. (stackoverflow.com)
    • Use the “now-offset” trick: map ft to system_clock by aligning clocks via their now() values (approximate, not exact). (stackoverflow.com)
    • Use implementation-specific APIs (Windows FILETIME mapping, libc++/libstdc++ helpers) if you need exact conversion. (cppstories.com)

References

  1. cppreference: file_clock / file_time_type (conversion helpers and clock_cast). [online]. (docs.w3cub.com)
  2. StackOverflow discussion and practical pre-C++20 workarounds. (stackoverflow.com)
  3. Article and implementation notes about platform specifics (MSVC vs GCC/Clang). (cppstories.com)

Citations:


🏁 Script executed:

cat -n src/server/send_file.cpp | sed -n '40,65p'

Repository: cppalliance/http

Length of output: 905


File time conversion should use C++20 clock_cast for accuracy.

The current code uses the "now-offset" trick (ftime - file_time_type::clock::now() + system_clock::now()), which is an approximate workaround that loses precision by aligning clocks at a single point in time. C++20 provides the proper solution: use std::chrono::clock_cast<std::chrono::system_clock>(ftime) or std::filesystem::file_clock::to_sys(ftime) followed by system_clock::to_time_t(). This avoids clock synchronization assumptions and preserves correct epoch translation implemented by the library.

🤖 Prompt for AI Agents
In `@src/server/send_file.cpp` around lines 48 - 54, Replace the manual
"now-offset" conversion around sctp and mtime: instead of computing sctp via
ftime - file_time_type::clock::now() + system_clock::now(), use the standard
C++20 conversion to system_clock (e.g.,
std::chrono::clock_cast<std::chrono::system_clock>(ftime) or
std::filesystem::file_time_type::clock::to_sys/
std::filesystem::file_clock::to_sys(ftime) depending on your standard library)
and then compute mtime with std::chrono::system_clock::to_time_t(...) from that
converted time_point; update the code that sets sctp and mtime (references:
sctp, mtime, ftime, std::filesystem::file_time_type) to use the
clock_cast/to_sys approach for accurate epoch translation.

Comment on lines +187 to +192
// Full content response
rp.res.set_status(status::ok);
rp.res.set_payload_size(info.size);
info.range_start = 0;
info.range_end = static_cast<std::int64_t>(info.size) - 1;
info.result = send_file_result::ok;
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

Potential issue when info.size is 0.

If the file is empty (info.size == 0), then info.range_end becomes -1. This could cause issues in the caller's streaming loop. Consider handling zero-size files explicitly.

🔧 Suggested fix
     // Full content response
     rp.res.set_status(status::ok);
     rp.res.set_payload_size(info.size);
-    info.range_start = 0;
-    info.range_end = static_cast<std::int64_t>(info.size) - 1;
+    if(info.size > 0)
+    {
+        info.range_start = 0;
+        info.range_end = static_cast<std::int64_t>(info.size) - 1;
+    }
+    else
+    {
+        info.range_start = 0;
+        info.range_end = -1;  // Indicates empty file
+    }
     info.result = send_file_result::ok;
📝 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
// Full content response
rp.res.set_status(status::ok);
rp.res.set_payload_size(info.size);
info.range_start = 0;
info.range_end = static_cast<std::int64_t>(info.size) - 1;
info.result = send_file_result::ok;
// Full content response
rp.res.set_status(status::ok);
rp.res.set_payload_size(info.size);
if(info.size > 0)
{
info.range_start = 0;
info.range_end = static_cast<std::int64_t>(info.size) - 1;
}
else
{
info.range_start = 0;
info.range_end = -1; // Indicates empty file
}
info.result = send_file_result::ok;
🤖 Prompt for AI Agents
In `@src/server/send_file.cpp` around lines 187 - 192, When info.size is 0 the
code sets info.range_end to -1 which can break callers; update the block that
sets rp.res.set_payload_size(info.size), info.range_start, info.range_end and
info.result so it special-cases empty files: if info.size == 0 set
rp.res.set_payload_size(0), info.range_start = 0 and info.range_end = 0 (instead
of -1) and leave info.result = send_file_result::ok, otherwise keep the existing
logic; adjust the handling in the send_file_result::ok branch (use symbols
rp.res, info.range_start, info.range_end, info.size, send_file_result::ok) so
callers receive a non-negative range for empty files.

Comment on lines +232 to +242
// Open and stream the file
capy::file f;
system::error_code ec;
f.open(path.c_str(), capy::file_mode::scan, ec);
if(ec)
{
if(impl_->opts.fallthrough)
co_return route::next;
rp.res.set_status(status::internal_server_error);
co_return co_await rp.send("Internal Server Error");
}
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

TOCTOU race between send_file_init and f.open.

The file is stat'd in send_file_init, but opened later. The file could be deleted or modified between these operations, leading to inconsistent behavior (e.g., headers set for a file that no longer exists). Consider opening the file first or handling the open failure more gracefully after headers are partially set.

🤖 Prompt for AI Agents
In `@src/server/serve_static.cpp` around lines 232 - 242, The TOCTOU bug occurs
because send_file_init performs statting/headers but the file is actually opened
later with capy::file::open in this block; to fix, open the file before calling
send_file_init (or merge open into send_file_init) so the same file descriptor
is used for stat/size and header generation, and if open fails handle errors
consistently (respect impl_->opts.fallthrough and set rp.res/status before
calling rp.send). Update the flow around send_file_init and the capy::file
f.open call (and related error handling that returns route::next or uses
rp.send) so headers are only set after a successful open or so that open
failures roll back any partial response headers.

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.

3 participants