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

📝 Walkthrough

Walkthrough

Adds many new Boost.HTTP server APIs and implementations: async route handler support, coroutine-based CORS, URL/HTML encoding, ETag/freshness helpers, MIME DB and lookup, Range parsing, file send and static-serving middleware, status predicates, tests, and a 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
Adds route_task alias and coroutine-based route_params methods (send, write, end, write_impl); adds test_route_params test double and updates tests to use it.
CORS middleware
include/boost/http/server/cors.hpp, src/server/cors.cpp
Converts cors::operator() to return route_task (coroutine), renames parameter to rp, updates internal flow to co_await/co_return and adjusts Vary helper to use rp.
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) declarations 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 (body and size+mtime), and is_fresh(request const&, response const&) with matching/date helpers.
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
Introduces mime_type_entry, mime_db::lookup(), and mime_types utilities (lookup, extension, charset, content_type) with case-insensitive search.
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) for HTTP Range header parsing and validation.
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
Adds send_file_options, send_file_info, send_file_init(), format_http_date(), and a coroutine-based serve_static middleware (dotfiles, index, caching, ranges, redirects) with streaming file send implementation and a header-include unit test.
Status predicates & tests
include/boost/http/server/statuses.hpp, src/server/statuses.cpp, test/unit/server/statuses.cpp
Adds boost::http::statuses::{is_empty,is_redirect,is_retry} implementations and unit tests.
Range/serving helpers integrated
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
Multiple source files implement lookup, parsing, etag, freshness, and send-file logic used together by serve_static.
Docs / tooling rule
.cursor/rules/doc.mdc
Adds a documentation style rule file with YAML front matter requiring route_params parameter name rp.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Serve 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: HTTP GET /path
    Serve->>FS: stat(path)
    FS-->>Serve: (exists,size,mtime)
    Serve->>Mime: lookup(path)
    Mime-->>Serve: content_type
    Serve->>Serve: generate ETag / set headers
    Client->>Serve: If-None-Match / Range headers
    Serve->>Serve: is_fresh? (ETag / Last-Modified)
    alt fresh (304)
        Serve->>RP: set 304 and end
    else if Range present
        Serve->>Range: parse_range(size, header)
        Range-->>Serve: ranges/result
        Serve->>FS: open file (with range)
        loop stream chunks
            Serve->>RP: write(chunk)
        end
        Serve->>RP: end()
    else full content
        Serve->>FS: open file
        loop stream chunks
            Serve->>RP: write(chunk)
        end
        Serve->>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 title 'more' is vague and non-descriptive, providing no meaningful information about the substantial changeset that adds multiple HTTP server utilities and middleware components. Replace with a descriptive title summarizing the main additions, such as 'Add HTTP server utilities: ETag, freshness, MIME types, ranges, file serving, and static middleware' or similar.
✅ 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

cppalliance-bot commented Jan 19, 2026

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-20 00:06:41 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.

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

Caution

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

⚠️ Outside diff range comments (1)
src/server/cors.cpp (1)

121-128: Potential bug: Vary header appends request header value instead of header name.

Line 126 appends s (the value of Access-Control-Request-Headers from the request) to the Vary header. The Vary header should contain header names that affect response caching. This should likely append to_string(field::access_control_request_headers) instead.

🐛 Proposed fix
     if(! s.empty())
     {
         v.set(field::access_control_allow_headers, s);
-        v.append(field::vary, s);
+        v.append(field::vary, to_string(field::access_control_request_headers));
     }
🤖 Fix all issues with AI agents
In `@include/boost/http/server/cors.hpp`:
- Around line 68-88: The cors constructor is marked noexcept while taking
cors_options by value, which can trigger non-noexcept std::string moves; change
the signature of cors::cors to either remove the noexcept specifier or accept a
const cors_options& parameter to avoid potentially throwing moves (i.e., adjust
the constructor declaration in class cors to use cors_options const& or drop
noexcept), and update any corresponding implementation to match.

In `@include/boost/http/server/mime_types.hpp`:
- Around line 36-38: Update the documentation examples that show the
Content-Type charset casing to match the actual behavior of
mime_types::content_type which appends the charset verbatim from the database
(uppercase "UTF-8"); replace examples that show "charset=utf-8" with
"charset=UTF-8" (e.g., the comment example demonstrating
content_type("application/json") should read "application/json; charset=UTF-8"
and similarly update the other example comments near the other content_type
usages).

In `@include/boost/http/server/serve_static.hpp`:
- Around line 13-41: The header uses std::uint32_t in serve_static_options but
doesn't include <cstdint>, making the header non-self-contained; add the line to
include the C++ header <cstdint> at the top of the file (alongside the existing
includes) so std::uint32_t is guaranteed to be available for
serve_static_options and any other symbols relying on fixed-width integer types.

In `@src/server/etag.cpp`:
- Around line 57-66: std::snprintf's return value (n) must be validated before
using it to construct the std::string; if n < 0 or n >= sizeof(buf) you can end
up reading/allocating incorrectly. After calling std::snprintf (in the block
that formats the ETag using buf, body.size(), and hex), check if n < 0 and
return an empty string or propagate an error, and if static_cast<std::size_t>(n)
>= sizeof(buf) clamp n to sizeof(buf) - 1 (the buffer is truncated but
null-terminated) before calling std::string(buf, static_cast<std::size_t>(n));
apply this validation to both the weak and non-weak snprintf branches.
♻️ Duplicate comments (12)
src/server/fresh.cpp (2)

21-59: Substring ETag matching can return false positives.

The find() approach on line 49 matches partial tokens (e.g., "abc" inside "xabc"), leading to incorrect freshness determination. The comma-separated list should be properly tokenized with exact token comparison after stripping whitespace and weak prefixes.


64-76: Lexicographic date comparison doesn't handle RFC 7231's multiple date formats.

RFC 7231 allows three date formats (IMF-fixdate, RFC 850, and asctime). String comparison only works correctly when both dates use the same format. If a client sends If-Modified-Since in a different format than the server's Last-Modified, this will misclassify freshness.

src/server/range_parser.cpp (1)

47-113: Distinguish malformed range specs from unsatisfiable ones.

parse_range_spec returns false for both syntax errors (e.g., bytes=abc-def) and valid-but-unsatisfiable ranges (e.g., bytes=10000- for a 100-byte file). This causes parse_range to potentially return unsatisfiable (triggering 416) for malformed input that should be ignored or trigger 400.

Per RFC 7233, a syntactically invalid Range header should be ignored (treated as if absent), while a syntactically valid but unsatisfiable range warrants 416.

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

102-113: Align the doc example with the actual response API.
The snippet uses rp.status(...), but the implementation pattern uses rp.res.set_status(...). Please verify the public API and update the example for consistency.

📘 Suggested doc fix
-            rp.status( status::not_modified );
+            rp.res.set_status( status::not_modified );
src/server/mime_types.cpp (1)

27-80: Fix ext_db ordering: "7z" breaks binary search.
The table is declared “Sorted by extension,” but "7z" is last. Digits sort before letters, so the array is no longer ordered and binary search can miss "7z" (and even nearby 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 (3)

48-54: Prefer C++20 file_clock::to_sys / clock_cast for mtime conversion.

The now-offset conversion is approximate; use the standard C++20 conversion for accurate epoch translation.


147-173: Avoid serving only the first range when multiple ranges are requested.

If multipart isn’t implemented, ignore multi-range requests rather than returning 206 for the first range only.


187-192: Handle zero-size files without a negative range_end.

info.range_end = size - 1 yields -1 for empty files, which can confuse callers. Special-case size==0.

src/server/serve_static.cpp (4)

55-67: Dotfile detection only checks the last segment.

Requests like /.git/config still pass. Scan all path segments (optionally allowlist .well-known).


152-155: Add canonical path containment checks to prevent traversal.

path_cat concatenation without normalization allows .. or symlinks to escape the root. Resolve and verify the resolved path is within the root before serving.


186-242: TOCTOU between send_file_init and file open.

The file is stat’d and headers are set before opening the file, so it can change or disappear. Open first or handle open failure without committing headers.


262-275: Check for read/stream errors before finalizing the response.

If read fails or returns short, you still call rp.end() with a committed Content-Length, producing truncated responses. Validate remaining == 0 and no error before ending.

🧹 Nitpick comments (2)
include/boost/http/server/statuses.hpp (1)

63-85: Consider noting that 305 Use Proxy is deprecated.

The documentation lists 305 Use Proxy, which was deprecated in RFC 7231 Section 6.4.5 due to security concerns. While including it for completeness is reasonable, you may want to add a note in the documentation mentioning its deprecated status, or consider excluding it if the library targets modern HTTP usage.

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

35-58: Consider default-initializing type to malformed instead of ok.

The range_result struct defaults type to ok, but parse_range immediately sets it to malformed at the start of parsing. Defaulting to malformed would be a safer "fail-closed" default if a range_result is constructed outside of parse_range.

♻️ Suggested change
     /// The result type.
-    range_result_type type = range_result_type::ok;
+    range_result_type type = range_result_type::malformed;
 };

Comment on lines +68 to 88
class BOOST_HTTP_DECL cors
{
cors_options options_;

public:
BOOST_HTTP_DECL
explicit cors(
cors_options options = {}) noexcept;
/** Construct a CORS middleware.
BOOST_HTTP_DECL
route_result
operator()(route_params& p) const;
@param options Configuration options.
*/
explicit cors(cors_options options = {}) noexcept;

private:
cors_options options_;
/** Handle a request.
Sets CORS headers and handles preflight requests.
@param rp The route parameters.
@return A task that completes with the routing result.
*/
route_task operator()(route_params& rp) const;
};
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

🌐 Web query:

std::string move constructor noexcept C++11 C++14 C++17 standard guarantee

💡 Result:

Short answer: The standard never guaranteed that every std::string move constructor is noexcept across C++11/C++14; it was implementation-/form-dependent (allocator forms could throw). The language rules for implicitly‑declared/defaulted move constructors’ exception specification are in the core language (and evolved), but library wording and LWG discussion show basic_string’s move/noexcept was tricky and not an absolute, unconditional guarantee. See cppreference for the language rules and the LWG issues (2069, 2319) about basic_string’s noexcept wording. [1][2][3]

Sources

  • cppreference — move constructors and how their exception specification is determined. [1]
  • C++ LWG issue 2069 (inconsistent exception spec for basic_string move constructor). [2]
  • C++ LWG issue 2319 (discussion about basic_string move constructor noexcept). [3]

🏁 Script executed:

# Search for cors_options definition
rg -n "class cors_options|struct cors_options" include/

Repository: cppalliance/http

Length of output: 117


🏁 Script executed:

# Check for C++ standard configuration
find . -name "CMakeLists.txt" -o -name "cmake.txt" -o -name ".clang-format" -o -name "*.h" -o -name "*.hpp" | head -20 | xargs grep -l "std=c++" 2>/dev/null || echo "Searching for standard flags..."

Repository: cppalliance/http

Length of output: 91


🏁 Script executed:

# Look at the top of the header file for any C++ standard comments or includes
head -50 include/boost/http/server/cors.hpp

Repository: cppalliance/http

Length of output: 1369


Remove noexcept from the constructor or use const reference parameter.

The constructor is marked noexcept but takes cors_options by value, which moves its std::string members (origin, methods, allowedHeaders, exposedHeaders). The std::string move constructor is not unconditionally noexcept—its exception specification depends on the allocator, and this is not guaranteed by the standard in any C++ version. Either remove the noexcept specification, accept const cors_options& instead, or document that only noexcept allocators are supported.

🤖 Prompt for AI Agents
In `@include/boost/http/server/cors.hpp` around lines 68 - 88, The cors
constructor is marked noexcept while taking cors_options by value, which can
trigger non-noexcept std::string moves; change the signature of cors::cors to
either remove the noexcept specifier or accept a const cors_options& parameter
to avoid potentially throwing moves (i.e., adjust the constructor declaration in
class cors to use cors_options const& or drop noexcept), and update any
corresponding implementation to match.

Comment on lines +36 to +38
// Get full Content-Type header value
auto ct = mime_types::content_type( "application/json" );
// ct == "application/json; charset=utf-8"
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:

find . -name "mime_types.hpp" -type f

Repository: cppalliance/http

Length of output: 102


🏁 Script executed:

cat -n include/boost/http/server/mime_types.hpp

Repository: cppalliance/http

Length of output: 3931


🏁 Script executed:

# Also search for mime_db or charset definitions
rg "charset|UTF-8|utf-8" include/boost/http/server/mime_types.hpp -n

Repository: cppalliance/http

Length of output: 541


🏁 Script executed:

find . -name "*.cpp" -o -name "*.ipp" | xargs grep -l "mime_types\|content_type" | head -5

Repository: cppalliance/http

Length of output: 167


🏁 Script executed:

# Search for mime_db or charset definitions
rg "mime_db|UTF-8|charset" --type cpp -A 2 -B 2 | head -100

Repository: cppalliance/http

Length of output: 5000


🏁 Script executed:

# Look for the implementation of content_type and charset functions
find . -type f \( -name "*.cpp" -o -name "*.ipp" -o -name "*mime*" \) | head -20

Repository: cppalliance/http

Length of output: 566


Update documentation examples to match actual charset casing.
The MIME type database stores charset as "UTF-8" (uppercase), and content_type() appends it verbatim. Examples at lines 38, 93, and 96 incorrectly show lowercase "utf-8".

Updates required
- // ct == "application/json; charset=utf-8"
+ // ct == "application/json; charset=UTF-8"
- // ct == "text/html; charset=utf-8"
+ // ct == "text/html; charset=UTF-8"
- // ct2 == "application/json; charset=utf-8"
+ // ct2 == "application/json; charset=UTF-8"
📝 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
// Get full Content-Type header value
auto ct = mime_types::content_type( "application/json" );
// ct == "application/json; charset=utf-8"
// Get full Content-Type header value
auto ct = mime_types::content_type( "application/json" );
// ct == "application/json; charset=UTF-8"
🤖 Prompt for AI Agents
In `@include/boost/http/server/mime_types.hpp` around lines 36 - 38, Update the
documentation examples that show the Content-Type charset casing to match the
actual behavior of mime_types::content_type which appends the charset verbatim
from the database (uppercase "UTF-8"); replace examples that show
"charset=utf-8" with "charset=UTF-8" (e.g., the comment example demonstrating
content_type("application/json") should read "application/json; charset=UTF-8"
and similarly update the other example comments near the other content_type
usages).

Comment on lines +13 to +41
#include <boost/http/detail/config.hpp>
#include <boost/http/server/route_handler.hpp>

namespace boost {
namespace http {

/** Policy for handling dotfiles in static file serving.
*/
enum class dotfiles_policy
{
/// Allow access to dotfiles.
allow,

/// Deny access to dotfiles (403 Forbidden).
deny,

/// Ignore dotfiles (pass to next handler).
ignore
};

/** Options for the static file server.
*/
struct serve_static_options
{
/// How to handle dotfiles.
dotfiles_policy dotfiles = dotfiles_policy::ignore;

/// Maximum cache age in seconds.
std::uint32_t max_age = 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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n include/boost/http/server/serve_static.hpp

Repository: cppalliance/http

Length of output: 3842


🏁 Script executed:

# Check if <cstdint> is included in the current file
rg "#include.*cstdint" include/boost/http/server/serve_static.hpp

Repository: cppalliance/http

Length of output: 42


🏁 Script executed:

# Check what boost/http/detail/config.hpp and route_handler.hpp include
cat -n include/boost/http/detail/config.hpp | head -50

Repository: cppalliance/http

Length of output: 1805


🏁 Script executed:

cat -n include/boost/http/server/route_handler.hpp | head -50

Repository: cppalliance/http

Length of output: 2073


Add <cstdint> to make the header self-contained.

serve_static_options uses std::uint32_t, which requires the C++ header <cstdint>. While the code currently relies on transitive inclusion of <stdint.h> through boost/http/detail/config.hpp, this does not guarantee std::uint32_t is available in the std namespace. Explicit inclusion of <cstdint> is necessary for proper C++ compliance and header independence.

Proposed fix
 `#include` <boost/http/detail/config.hpp>
 `#include` <boost/http/server/route_handler.hpp>
+#include <cstdint>
📝 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
#include <boost/http/detail/config.hpp>
#include <boost/http/server/route_handler.hpp>
namespace boost {
namespace http {
/** Policy for handling dotfiles in static file serving.
*/
enum class dotfiles_policy
{
/// Allow access to dotfiles.
allow,
/// Deny access to dotfiles (403 Forbidden).
deny,
/// Ignore dotfiles (pass to next handler).
ignore
};
/** Options for the static file server.
*/
struct serve_static_options
{
/// How to handle dotfiles.
dotfiles_policy dotfiles = dotfiles_policy::ignore;
/// Maximum cache age in seconds.
std::uint32_t max_age = 0;
`#include` <boost/http/detail/config.hpp>
`#include` <boost/http/server/route_handler.hpp>
`#include` <cstdint>
namespace boost {
namespace http {
/** Policy for handling dotfiles in static file serving.
*/
enum class dotfiles_policy
{
/// Allow access to dotfiles.
allow,
/// Deny access to dotfiles (403 Forbidden).
deny,
/// Ignore dotfiles (pass to next handler).
ignore
};
/** Options for the static file server.
*/
struct serve_static_options
{
/// How to handle dotfiles.
dotfiles_policy dotfiles = dotfiles_policy::ignore;
/// Maximum cache age in seconds.
std::uint32_t max_age = 0;
🤖 Prompt for AI Agents
In `@include/boost/http/server/serve_static.hpp` around lines 13 - 41, The header
uses std::uint32_t in serve_static_options but doesn't include <cstdint>, making
the header non-self-contained; add the line to include the C++ header <cstdint>
at the top of the file (alongside the existing includes) so std::uint32_t is
guaranteed to be available for serve_static_options and any other symbols
relying on fixed-width integer types.

Comment on lines +57 to +66
char buf[64];
int n;
if( opts.weak )
n = std::snprintf( buf, sizeof(buf),
"W/\"%zx-%s\"", body.size(), hex );
else
n = std::snprintf( buf, sizeof(buf),
"\"%zx-%s\"", body.size(), hex );

return std::string( buf, static_cast<std::size_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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

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

Repository: cppalliance/http

Length of output: 81


🏁 Script executed:

# Read the file to see the exact code at the mentioned lines
head -100 src/server/etag.cpp | tail -50

Repository: cppalliance/http

Length of output: 1162


🏁 Script executed:

# Get more context around the specific lines mentioned
sed -n '40,100p' src/server/etag.cpp

Repository: cppalliance/http

Length of output: 1224


Guard snprintf return before using it as length.

If std::snprintf returns a negative value (encoding error) or a value >= buffer size, std::string(buf, n) can read out of bounds or allocate a huge length. Clamp or bail out before constructing the string.

🛠️ Suggested fix (apply to both overloads)
-    return std::string( buf, static_cast<std::size_t>(n) );
+    if(n < 0)
+        return {};
+    if(n >= static_cast<int>(sizeof(buf)))
+        n = static_cast<int>(sizeof(buf)) - 1;
+    return std::string( buf, static_cast<std::size_t>(n) );
🤖 Prompt for AI Agents
In `@src/server/etag.cpp` around lines 57 - 66, std::snprintf's return value (n)
must be validated before using it to construct the std::string; if n < 0 or n >=
sizeof(buf) you can end up reading/allocating incorrectly. After calling
std::snprintf (in the block that formats the ETag using buf, body.size(), and
hex), check if n < 0 and return an empty string or propagate an error, and if
static_cast<std::size_t>(n) >= sizeof(buf) clamp n to sizeof(buf) - 1 (the
buffer is truncated but null-terminated) before calling std::string(buf,
static_cast<std::size_t>(n)); apply this validation to both the weak and
non-weak snprintf branches.

@cppalliance-bot
Copy link

GCOVR code coverage report https://245.http.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://245.http.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff https://245.http.prtest3.cppalliance.org/gcovr/coverage_diff.txt

Build time: 2026-01-20 00:05:34 UTC

@cppalliance-bot
Copy link

GCOVR code coverage report https://245.http.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://245.http.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff https://245.http.prtest3.cppalliance.org/gcovr/coverage_diff.txt

Build time: 2026-01-20 00:20:22 UTC

@vinniefalco vinniefalco merged commit ff61671 into cppalliance:develop Jan 20, 2026
14 of 15 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.

3 participants