diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..ba6af77
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,39 @@
+# Copyright 2019 - 2021 Alexander Grund
+# Distributed under the Boost Software License, Version 1.0.
+# (See accompanying file LICENSE_1_0.txt or copy at http://boost.org/LICENSE_1_0.txt)
+
+codecov:
+ max_report_age: off
+ require_ci_to_pass: yes
+ notify:
+ # Increase this if you have multiple coverage collection jobs
+ after_n_builds: 1
+ wait_for_ci: yes
+
+# Fix paths from CI build to match repository structure.
+# The lcov report records absolute paths such as
+# /home/runner/.../boost-root/libs/burl/include/...
+fixes:
+ - "boost-root/libs/burl/::"
+
+# Make coverage checks informational (report but never fail CI)
+coverage:
+ status:
+ project:
+ default:
+ informational: true
+ patch:
+ default:
+ informational: true
+
+# Change how pull request comments look
+comment:
+ layout: "reach,diff,flags,files,footer"
+
+# Ignore specific files or folders. Glob patterns are supported.
+# See https://docs.codecov.com/docs/ignoring-paths
+ignore:
+ - example/*
+ - example/**/*
+ - test/*
+ - test/**/*
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c657414..4e46955 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -138,7 +138,7 @@ jobs:
b2-toolset: "gcc"
is-latest: true
name: "GCC 15: C++20"
- shared: true
+ shared: false # TODO
build-type: "Release"
build-cmake: true
@@ -187,7 +187,7 @@ jobs:
b2-toolset: "clang"
is-latest: true
name: "Clang 20: C++20-23"
- shared: true
+ shared: false # TODO
build-type: "Release"
build-cmake: true
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
new file mode 100644
index 0000000..df1df4d
--- /dev/null
+++ b/.github/workflows/code-coverage.yml
@@ -0,0 +1,311 @@
+#
+# Copyright (c) 2026 Sam Darwin
+# Copyright (c) 2026 Alexander Grund
+#
+# Distributed under the Boost Software License, Version 1.0. (See accompanying
+# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+#
+# Official repository: https://github.com/cppalliance/burl/
+#
+
+# Instructions
+#
+# After running this workflow successfully, go to https://github.com/cppalliance/burl/settings/pages
+# and enable github pages on the code-coverage branch.
+# The coverage will be hosted at https://cppalliance.github.io/burl
+#
+
+name: Code Coverage
+
+on:
+ push:
+ branches:
+ - master
+ - develop
+ paths:
+ - 'src/**'
+ - 'include/**'
+ - 'test/**'
+ - '.github/workflows/code-coverage.yml'
+ workflow_dispatch:
+
+concurrency:
+ group: code-coverage-pages
+ cancel-in-progress: false
+
+env:
+ GIT_FETCH_JOBS: 8
+ NET_RETRY_COUNT: 5
+ GCOVR_COMMIT_MSG: "Update coverage data"
+
+jobs:
+ build-linux:
+ defaults:
+ run:
+ shell: bash
+ name: Coverage (Linux)
+ runs-on: ubuntu-24.04
+ timeout-minutes: 120
+
+ steps:
+ - name: Clone Boost.Burl
+ uses: actions/checkout@v6
+ with:
+ path: burl-root
+
+ - name: Setup C++
+ uses: alandefreitas/cpp-actions/setup-cpp@v1.9.4
+ id: setup-cpp
+ with:
+ compiler: gcc
+ version: 13
+ check-latest: true
+
+ - name: Install packages
+ uses: alandefreitas/cpp-actions/package-install@v1.9.4
+ with:
+ apt-get: >-
+ build-essential libssl-dev zlib1g-dev libbrotli-dev
+ lcov
+
+ - name: Clone Boost
+ uses: alandefreitas/cpp-actions/boost-clone@v1.9.4
+ id: boost-clone
+ with:
+ branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }}
+ boost-dir: boost-source
+ scan-modules-dir: burl-root
+ patches: >
+ https://github.com/cppalliance/capy
+ https://github.com/cppalliance/corosio
+ https://github.com/cppalliance/http
+
+ - name: ASLR Fix
+ run: sysctl vm.mmap_rnd_bits=28
+
+ - name: Patch Boost
+ id: patch
+ run: |
+ set -xe
+
+ # Identify boost module being tested
+ module=${GITHUB_REPOSITORY#*/}
+ echo "module=$module" >> $GITHUB_OUTPUT
+
+ # Identify GitHub workspace root
+ workspace_root=$(echo "$GITHUB_WORKSPACE" | sed 's/\\/\//g')
+ echo -E "workspace_root=$workspace_root" >> $GITHUB_OUTPUT
+
+ # Remove module from boost-source
+ rm -r "boost-source/libs/$module" || true
+
+ # Copy cached boost-source to an isolated boost-root
+ cp -r boost-source boost-root
+
+ # Set boost-root output
+ cd boost-root
+ boost_root="$(pwd)"
+ boost_root=$(echo "$boost_root" | sed 's/\\/\//g')
+ echo -E "boost_root=$boost_root" >> $GITHUB_OUTPUT
+ cd ..
+
+ # Patch boost-root with workspace module
+ cp -r "$workspace_root"/burl-root "boost-root/libs/$module"
+
+ - name: Build with coverage
+ uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.4
+ with:
+ source-dir: boost-root
+ build-dir: __build_cmake_test__
+ build-type: Debug
+ build-target: tests
+ run-tests: true
+ install-prefix: .local
+ cxxstd: '20'
+ cc: ${{ steps.setup-cpp.outputs.cc || 'gcc-13' }}
+ cxx: ${{ steps.setup-cpp.outputs.cxx || 'g++-13' }}
+ cxxflags: '--coverage -fprofile-arcs -ftest-coverage'
+ ccflags: '--coverage -fprofile-arcs -ftest-coverage'
+ shared: false
+ cmake-version: '>=3.20'
+ extra-args: |
+ -D Boost_VERBOSE=ON
+ -D BOOST_INCLUDE_LIBRARIES="${{ steps.patch.outputs.module }}"
+ -D CMAKE_EXPORT_COMPILE_COMMANDS=ON
+ package: false
+ package-artifact: false
+ ref-source-dir: boost-root/libs/burl
+
+ - name: Install Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.13'
+
+ - name: Install Python packages
+ run: pip install gcovr
+
+ - name: Checkout ci-automation
+ uses: actions/checkout@v6
+ with:
+ repository: cppalliance/ci-automation
+ path: ci-automation
+
+ - name: Generate gcovr report
+ run: |
+ set -xe
+ module=$(basename ${GITHUB_REPOSITORY})
+ gcov_tool="gcov-${{ steps.setup-cpp.outputs.version-major }}"
+ command -v "$gcov_tool" >/dev/null 2>&1 || gcov_tool="gcov"
+ mkdir -p gcovr
+
+ # First pass: collect raw coverage data into JSON
+ gcovr \
+ --root boost-root \
+ --gcov-executable "$gcov_tool" \
+ --merge-mode-functions separate \
+ --sort uncovered-percent \
+ --html-title "${module}" \
+ --merge-lines \
+ --exclude-unreachable-branches \
+ --exclude-throw-branches \
+ --exclude '.*/example/.*' \
+ --exclude '.*/examples/.*' \
+ --filter ".*/${module}/.*" \
+ --html --output gcovr/index.html \
+ --json-summary-pretty --json-summary gcovr/summary.json \
+ --json gcovr/coverage-raw.json \
+ boost-root/__build_cmake_test__
+
+ # Fix paths for repo-relative display
+ python3 ci-automation/scripts/fix_paths.py \
+ gcovr/coverage-raw.json \
+ gcovr/coverage-fixed.json \
+ --repo "${module}"
+
+ # Create symlinks so gcovr can find source files at repo-relative paths
+ ln -sfn "boost-root/libs/${module}/include" include 2>/dev/null || true
+ ln -sfn "boost-root/libs/${module}/src" src 2>/dev/null || true
+
+ # Second pass: generate nested HTML from fixed JSON with custom templates
+ gcovr \
+ -a gcovr/coverage-fixed.json \
+ --merge-mode-functions separate \
+ --sort uncovered-percent \
+ --html-nested \
+ --html-template-dir=ci-automation/gcovr-templates/html \
+ --html-title "${module}" \
+ --merge-lines \
+ --exclude-unreachable-branches \
+ --exclude-throw-branches \
+ --exclude '(^|.*/)test/.*' \
+ --exclude '.*/example/.*' \
+ --exclude '.*/examples/.*' \
+ --html --output gcovr/index.html \
+ --json-summary-pretty --json-summary gcovr/summary.json
+
+ - name: Generate sidebar navigation
+ run: python3 ci-automation/scripts/gcovr_build_tree.py gcovr
+
+ - name: Generate badges
+ run: python3 ci-automation/scripts/generate_badges.py gcovr --json gcovr/summary.json
+
+ - name: Upload coverage artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-linux
+ path: gcovr/
+
+ deploy:
+ needs: [build-linux]
+ if: ${{ !cancelled() }}
+ defaults:
+ run:
+ shell: bash
+ name: Deploy Coverage
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Check for code-coverage Branch
+ run: |
+ set -xe
+ git config --global user.name cppalliance-bot
+ git config --global user.email cppalliance-bot@example.com
+ git fetch origin
+ if git branch -r | grep origin/code-coverage; then
+ echo "The code-coverage branch exists. Continuing."
+ else
+ echo "The code-coverage branch does not exist. Creating it."
+ git switch --orphan code-coverage
+ git commit --allow-empty -m "$GCOVR_COMMIT_MSG"
+ git push origin code-coverage
+ git checkout $GITHUB_REF_NAME
+ fi
+
+ - name: Checkout GitHub pages branch
+ uses: actions/checkout@v6
+ with:
+ ref: code-coverage
+ path: gh_pages_dir
+
+ - name: Download coverage artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: coverage-*
+ path: coverage-artifacts/
+
+ - name: Copy coverage results
+ run: |
+ set -xe
+ touch gh_pages_dir/.nojekyll
+
+ mkdir -p "gh_pages_dir/develop"
+ mkdir -p "gh_pages_dir/master"
+
+ # Remove old single-directory layout (migration from gcovr/ to gcovr-linux/)
+ rm -rf "gh_pages_dir/${GITHUB_REF_NAME}/gcovr"
+
+ # Copy each platform's results (only if artifact exists)
+ for platform in linux; do
+ if [ -d "coverage-artifacts/coverage-${platform}" ]; then
+ rm -rf "gh_pages_dir/${GITHUB_REF_NAME}/gcovr-${platform}"
+ cp -rp "coverage-artifacts/coverage-${platform}" \
+ "gh_pages_dir/${GITHUB_REF_NAME}/gcovr-${platform}"
+ fi
+ done
+
+ # Generate branch index pages
+ for branch in develop master; do
+ cat > "gh_pages_dir/${branch}/index.html" << 'HTMLEOF'
+
+
Code Coverage
+
+ Code Coverage Reports
+
+
+
+ HTMLEOF
+ done
+
+ # Root index
+ cat > gh_pages_dir/index.html << 'HTMLEOF'
+
+
+
+ develop
+ master
+
+
+ HTMLEOF
+
+ cd gh_pages_dir
+ git config --global user.name cppalliance-bot
+ git config --global user.email cppalliance-bot@example.com
+ git add .
+ git commit --amend -m "$GCOVR_COMMIT_MSG"
+ git push -f origin code-coverage
diff --git a/example/usage.cpp b/example/usage.cpp
index 3af343f..84621fd 100644
--- a/example/usage.cpp
+++ b/example/usage.cpp
@@ -354,11 +354,11 @@ set_timeouts(corosio::tls_context tls_ctx)
burl::client::config cfg;
// Connect timeout, including TLS handshake and proxy connect
- cfg.pool.connect_timeout = std::chrono::seconds(30);
+ cfg.connect_timeout = std::chrono::seconds(30);
// Per read/write timeout, for detecting unresponsive servers regardless
// of the request/response size
- cfg.pool.io_timeout = std::chrono::seconds(5);
+ cfg.io_timeout = std::chrono::seconds(5);
// Timeout for the whole operation, including retrieving the response
cfg.timeout = std::chrono::seconds(60);
@@ -434,8 +434,8 @@ capy::task<>
connection_pool(corosio::tls_context tls_ctx)
{
burl::client::config cfg;
- cfg.pool.idle_timeout = std::chrono::seconds(60);
- cfg.pool.max_idle_per_host = 10;
+ cfg.pool_idle_timeout = std::chrono::seconds(60);
+ cfg.pool_max_idle_per_host = 10;
burl::client client(co_await capy::this_coro::executor, tls_ctx, cfg);
@@ -456,7 +456,7 @@ use_proxy(corosio::tls_context tls_ctx)
{
burl::client::config cfg;
// SOCKS5 and HTTP proxies are supported
- cfg.pool.proxy = urls::url("socks5h://user:pass@localhost:8080");
+ cfg.proxy = urls::url("socks5h://user:pass@localhost:8080");
burl::client client(co_await capy::this_coro::executor, tls_ctx, cfg);
diff --git a/include/boost/burl.hpp b/include/boost/burl.hpp
index dcbcb8b..19959b8 100644
--- a/include/boost/burl.hpp
+++ b/include/boost/burl.hpp
@@ -13,7 +13,6 @@
#include
#include
#include
-#include
#include
#include
#include
diff --git a/include/boost/burl/client.hpp b/include/boost/burl/client.hpp
index 4cba22a..7deef4e 100644
--- a/include/boost/burl/client.hpp
+++ b/include/boost/burl/client.hpp
@@ -10,23 +10,28 @@
#ifndef BOOST_BURL_CLIENT_HPP
#define BOOST_BURL_CLIENT_HPP
-#include
#include
#include
+#include
#include
#include
#include
+#include
#include
+#include
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
+#include
#include
#include
@@ -56,8 +61,7 @@ class request_builder;
@see
@ref request_builder,
- @ref response,
- @ref connection_pool.
+ @ref response.
*/
class client
{
@@ -206,18 +210,95 @@ class client
*/
std::optional timeout;
- /** Connection pool settings.
+ /** Timeout for establishing a connection.
- Controls connection establishment,
- including timeouts, socket options, and
- the proxy, along with connection reuse.
+ Covers name resolution, the TCP
+ connection, proxy negotiation, and the
+ TLS handshake.
*/
- connection_pool::config pool;
+ clock::duration connect_timeout = std::chrono::seconds(60);
+
+ /** Timeout for individual I/O operations.
+
+ When set, applies to every read and write
+ performed on a connection, bounding the
+ time the peer may remain unresponsive
+ regardless of the message size.
+ */
+ std::optional io_timeout = std::nullopt;
+
+ /** Time an idle pooled connection remains usable.
+
+ Pooled connections which have been idle for
+ longer than this duration are discarded
+ instead of being reused.
+ */
+ clock::duration pool_idle_timeout = std::chrono::seconds(90);
+
+ /** Maximum number of idle pooled connections per origin.
+
+ When the limit is reached, additional
+ connections are closed instead of being
+ returned to the pool.
+ */
+ std::size_t pool_max_idle_per_host = 10;
+
+ /** Set the `TCP_NODELAY` option on sockets.
+
+ Disables Nagle's algorithm on newly
+ established connections.
+ */
+ bool tcp_nodelay = true;
+
+ /** The local endpoint to bind sockets to.
+ */
+ corosio::endpoint local_address;
+
+ /** The proxy used for establishing connections.
+
+ Supported proxy schemes are `http`,
+ `socks5`, and `socks5h`. Credentials in the
+ userinfo component of the URL are used for
+ proxy authentication.
+
+ @par Example
+ @code
+ cfg.proxy = urls::url("socks5h://user:pass@localhost:8080");
+ @endcode
+ */
+ std::optional proxy;
+
+ /** Override connection establishment.
+
+ When set, this function is invoked
+ instead of the built-in name resolution,
+ TCP connection, proxy negotiation, and
+ TLS handshake whenever the pool needs a
+ new connection.
+
+ Intended for testing and for advanced uses
+ such as connecting over a pre-established
+ tunnel or a Unix domain socket.
+
+ @par Example
+ @code
+ cfg.connect_handler =
+ [](urls::url_view) -> capy::io_task
+ {
+ auto [a, b] = capy::test::make_stream_pair();
+ // drive b from the test; hand a to the client
+ co_return { {}, capy::any_stream(std::move(a)) };
+ };
+ @endcode
+ */
+ std::function<
+ capy::io_task(urls::url_view url)>
+ connect_handler;
};
private:
config config_;
- connection_pool pool_;
+ std::shared_ptr pool_;
http::fields headers_;
burl::cookie_jar cookie_jar_;
@@ -255,6 +336,30 @@ class client
BOOST_BURL_DECL
client(capy::executor_ref exec, corosio::tls_context tls_ctx, config cfg);
+ /** Copy constructor (deleted).
+ */
+ client(client const&) = delete;
+
+ /** Copy assignment (deleted).
+ */
+ client&
+ operator=(client const&) = delete;
+
+ /** Move constructor.
+
+ @param other The client to move from.
+ */
+ client(client&& other) = default;
+
+ /** Move assignment.
+
+ @param other The client to move from.
+
+ @return A reference to this client.
+ */
+ client&
+ operator=(client&& other) = default;
+
/** Return the default headers.
These headers are sent with every request.
diff --git a/include/boost/burl/connection_pool.hpp b/include/boost/burl/connection_pool.hpp
deleted file mode 100644
index ccff57b..0000000
--- a/include/boost/burl/connection_pool.hpp
+++ /dev/null
@@ -1,256 +0,0 @@
-//
-// Copyright (c) 2026 Mohammad Nejati
-//
-// Distributed under the Boost Software License, Version 1.0. (See accompanying
-// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
-//
-// Official repository: https://github.com/cppalliance/burl
-//
-
-#ifndef BOOST_BURL_CONNECTION_POOL_HPP
-#define BOOST_BURL_CONNECTION_POOL_HPP
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace boost
-{
-namespace burl
-{
-
-class client;
-class response;
-
-/** A pool of reusable HTTP connections.
-
- This class establishes connections to origin
- servers, directly or through a proxy, and
- retains completed keep-alive connections for
- reuse. Connections are keyed by origin, that is,
- the scheme, host, and port of the URL. Idle
- connections are discarded after
- @ref config::idle_timeout, and at most
- @ref config::max_idle_per_host are retained per
- origin.
-
- Each @ref client owns a connection pool,
- configured through @ref client::config::pool.
-
- This class is a shared handle to the pool state.
- Copies share the same underlying pool.
-
- @see @ref client.
-*/
-class connection_pool
-{
- friend class client;
- friend class response;
-
-public:
- /** Configuration settings for a connection pool.
- */
- struct config
- {
- using clock = std::chrono::steady_clock;
-
- /** Timeout for establishing a connection.
-
- Covers name resolution, the TCP
- connection, proxy negotiation, and the
- TLS handshake.
- */
- clock::duration connect_timeout = std::chrono::seconds(60);
-
- /** Timeout for individual I/O operations.
-
- When set, applies to every read and
- write performed on a connection,
- bounding the time the peer may remain
- unresponsive regardless of the message
- size.
- */
- std::optional io_timeout = std::nullopt;
-
- /** Time an idle connection remains usable.
-
- Pooled connections which have been idle
- for longer than this duration are
- discarded instead of being reused.
- */
- clock::duration idle_timeout = std::chrono::seconds(90);
-
- /** Maximum number of idle connections per origin.
-
- When the limit is reached, additional
- connections are closed instead of being
- returned to the pool.
- */
- std::size_t max_idle_per_host = 10;
-
- /** Set the `TCP_NODELAY` option on sockets.
-
- Disables Nagle's algorithm on newly
- established connections.
- */
- bool tcp_nodelay = true;
-
- /** The local endpoint to bind sockets to.
- */
- corosio::endpoint local_address;
-
- /** The proxy used for establishing connections.
-
- Supported proxy schemes are `http`,
- `socks5`, and `socks5h`. Credentials in
- the userinfo component of the URL are
- used for proxy authentication.
-
- @par Example
- @code
- cfg.pool.proxy = urls::url("socks5h://user:pass@localhost:8080");
- @endcode
- */
- std::optional proxy;
- };
-
- /** Constructor.
-
- Constructs a connection pool with a default
- configuration.
-
- @param exec The executor used to perform
- asynchronous operations.
-
- @param tls_ctx The TLS context used for
- `https` connections.
- */
- connection_pool(capy::executor_ref exec, corosio::tls_context tls_ctx)
- : connection_pool(exec, std::move(tls_ctx), config{})
- {
- }
-
- /** Constructor.
-
- Constructs a connection pool with the
- provided configuration.
-
- @param exec The executor used to perform
- asynchronous operations.
-
- @param tls_ctx The TLS context used for
- `https` connections.
-
- @param cfg The configuration settings.
- */
- BOOST_BURL_DECL
- connection_pool(
- capy::executor_ref exec,
- corosio::tls_context tls_ctx,
- config cfg);
-
-private:
- class impl;
-
- class connection
- {
- public:
- virtual capy::io_task
- read_some(std::span buffers) = 0;
-
- virtual capy::io_task
- write_some(std::span buffers) = 0;
-
- virtual capy::io_task<>
- shutdown() = 0;
-
- virtual bool
- is_open() = 0;
-
- virtual ~connection() = default;
- };
-
- class pooled_connection
- {
- friend class impl;
- friend class response;
-
- std::unique_ptr conn_;
- std::weak_ptr pool_;
- std::string key_;
- std::optional io_timeout_;
- capy::detail::buffer_array<8, false> rba_; // TODO
- capy::detail::buffer_array<8, true> wba_; // TODO
-
- pooled_connection() = default;
-
- pooled_connection(
- std::unique_ptr conn,
- std::weak_ptr pool,
- std::string key,
- std::optional io_timeout = std::nullopt)
- : conn_(std::move(conn))
- , pool_(std::move(pool))
- , key_(std::move(key))
- , io_timeout_(io_timeout)
- {
- }
-
- public:
- template
- capy::io_task
- read_some(MB buffers)
- {
- rba_ = buffers;
-
- if(io_timeout_)
- return capy::timeout(conn_->read_some(rba_), *io_timeout_);
- return conn_->read_some(rba_);
- }
-
- template
- capy::io_task
- write_some(CB buffers)
- {
- wba_ = buffers;
-
- if(io_timeout_)
- return capy::timeout(conn_->write_some(wba_), *io_timeout_);
- return conn_->write_some(wba_);
- }
-
- explicit
- operator bool() const noexcept
- {
- return conn_ != nullptr;
- }
-
- BOOST_BURL_DECL
- void
- return_to_pool();
- };
-
- capy::io_task
- acquire(urls::url_view url);
-
- std::shared_ptr impl_;
-};
-
-} // namespace burl
-} // namespace boost
-
-#endif
diff --git a/include/boost/burl/detail/connection_pool.hpp b/include/boost/burl/detail/connection_pool.hpp
new file mode 100644
index 0000000..d704507
--- /dev/null
+++ b/include/boost/burl/detail/connection_pool.hpp
@@ -0,0 +1,134 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#ifndef BOOST_BURL_DETAIL_CONNECTION_POOL_HPP
+#define BOOST_BURL_DETAIL_CONNECTION_POOL_HPP
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+class connection_pool;
+
+class connection
+{
+ capy::detail::buffer_array<8, false> rba_; // TODO
+ capy::detail::buffer_array<8, true> wba_; // TODO
+
+public:
+ template
+ capy::io_task
+ read_some(MB buffers)
+ {
+ rba_ = buffers;
+ return do_read_some(rba_);
+ }
+
+ template
+ capy::io_task
+ write_some(CB buffers)
+ {
+ wba_ = buffers;
+ return do_write_some(wba_);
+ }
+
+ virtual bool
+ is_open() const noexcept = 0;
+
+ virtual capy::io_task<>
+ shutdown() = 0;
+
+ virtual ~connection() = default;
+
+private:
+ virtual capy::io_task
+ do_read_some(std::span buffers) = 0;
+
+ virtual capy::io_task
+ do_write_some(std::span buffers) = 0;
+};
+
+class pooled_connection
+{
+ friend class connection_pool;
+
+ using duration = std::chrono::steady_clock::duration;
+
+ std::unique_ptr conn_;
+ std::weak_ptr pool_;
+ std::string key_;
+ std::optional io_timeout_;
+
+public:
+ pooled_connection() = default;
+
+ template
+ capy::io_task
+ read_some(MB buffers)
+ {
+ if(io_timeout_)
+ return capy::timeout(conn_->read_some(buffers), *io_timeout_);
+ return conn_->read_some(buffers);
+ }
+
+ template
+ capy::io_task
+ write_some(CB buffers)
+ {
+ if(io_timeout_)
+ return capy::timeout(conn_->write_some(buffers), *io_timeout_);
+ return conn_->write_some(buffers);
+ }
+
+ explicit
+ operator bool() const noexcept
+ {
+ return conn_ != nullptr;
+ }
+
+ BOOST_BURL_DECL
+ void
+ return_to_pool();
+
+private:
+ pooled_connection(
+ std::unique_ptr conn,
+ std::weak_ptr pool,
+ std::string key,
+ std::optional io_timeout = std::nullopt)
+ : conn_(std::move(conn))
+ , pool_(std::move(pool))
+ , key_(std::move(key))
+ , io_timeout_(io_timeout)
+ {
+ }
+};
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
+
+#endif
diff --git a/include/boost/burl/response.hpp b/include/boost/burl/response.hpp
index 90dbe2a..258d22e 100644
--- a/include/boost/burl/response.hpp
+++ b/include/boost/burl/response.hpp
@@ -10,9 +10,9 @@
#ifndef BOOST_BURL_RESPONSE_HPP
#define BOOST_BURL_RESPONSE_HPP
-#include
#include
#include
+#include
#include
#include
#include
@@ -77,13 +77,13 @@ class response
using clock = std::chrono::steady_clock;
urls::url url_;
- connection_pool::pooled_connection conn_;
+ detail::pooled_connection conn_;
http::response_parser parser_;
std::optional deadline_;
response(
urls::url url,
- connection_pool::pooled_connection conn,
+ detail::pooled_connection conn,
http::response_parser parser,
std::optional deadline);
@@ -96,7 +96,16 @@ class response
*/
response() = default;
- /** Constructor.
+ /** Copy constructor (deleted).
+ */
+ response(response const&) = delete;
+
+ /** Copy assignment (deleted).
+ */
+ response&
+ operator=(response const&) = delete;
+
+ /** Move constructor.
Constructs a response by taking ownership of
the contents of another response, including
@@ -108,7 +117,7 @@ class response
BOOST_BURL_DECL
response(response&& other) noexcept;
- /** Assignment.
+ /** Move assignment.
Takes ownership of the contents of another
response, including the underlying
diff --git a/src/client.cpp b/src/client.cpp
index 255c88d..40bde72 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -11,12 +11,15 @@
#include
#include "detail/base64.hpp"
-#include "detail/drain.hpp"
-#include "detail/reuse.hpp"
+#include "detail/can_reuse_conn.hpp"
+#include "detail/connection_pool.hpp"
+#include "detail/drain_body.hpp"
+#include "detail/redirect.hpp"
#include
#include
#include
+#include
#include
#include
#include
@@ -41,51 +44,6 @@ namespace burl
namespace
{
-struct is_redirect_result
-{
- bool is_redirect = false;
- bool need_method_change = false;
-};
-
-is_redirect_result
-is_redirect(http::status status, const client::config& cfg) noexcept
-{
- // The specifications do not intend for 301 and 302
- // redirects to change the HTTP method, but most
- // user agents do change the method in practice.
- switch(status)
- {
- case http::status::moved_permanently:
- return { true, !cfg.post301 };
- case http::status::found:
- return { true, !cfg.post302 };
- case http::status::see_other:
- return { true, !cfg.post303 };
- case http::status::temporary_redirect:
- case http::status::permanent_redirect:
- return { true, false };
- default:
- return { false, false };
- }
-}
-
-urls::url
-redirect_url(http::response_base const& response, const urls::url_view& referer)
-{
- auto it = response.find(http::field::location);
- if(it != response.end())
- {
- auto rs = urls::parse_uri_reference(it->value);
- if(rs.has_value())
- {
- urls::url url;
- urls::resolve(referer, rs.value(), url);
- return url;
- }
- }
- return {};
-}
-
void
set_accept_encoding(
http::parser_config& parser_cfg,
@@ -144,7 +102,9 @@ client::client(
corosio::tls_context tls_ctx,
config cfg)
: config_(cfg)
- , pool_(exec, std::move(tls_ctx), cfg.pool)
+ , pool_(
+ std::make_shared(
+ exec, std::move(tls_ctx), cfg))
{
// Disable codings whose decoder service is unavailable.
auto const& ctx = capy::get_system_context();
@@ -301,7 +261,7 @@ client::execute_impl(
headers.set(field::cookie, cookies);
}
- auto [cec, conn] = co_await pool_.acquire(url);
+ auto [cec, conn] = co_await pool_->acquire(url);
if(cec)
co_return { cec, {} };
@@ -347,7 +307,7 @@ client::execute_impl(
}
auto [is_redirect, need_method_change] =
- burl::is_redirect(parser.get().status(), config_);
+ detail::is_redirect(parser.get().status(), config_);
if(!is_redirect || !followlocation)
{
@@ -364,10 +324,9 @@ client::execute_impl(
// Read and discard small bodies so the connection can be reused
auto [dec] = co_await capy::timeout(
- detail::drain_body(parser, conn, 1024 * 1024),
+ detail::drain_body(parser, capy::any_stream(&conn), 1024 * 1024),
std::chrono::seconds(2));
-
- if(detail::can_reuse_conn(parser))
+ if(!dec && detail::can_reuse_conn(parser))
conn.return_to_pool();
if(maxredirs-- == 0)
@@ -383,7 +342,7 @@ client::execute_impl(
}
// Prepare the next request to follow the redirect
- url = redirect_url(parser.get(), url);
+ url = detail::resolve_location(parser.get(), url);
if(url.empty())
co_return { error::bad_redirect_response, {} };
diff --git a/src/connection_pool.cpp b/src/connection_pool.cpp
deleted file mode 100644
index 088fbcc..0000000
--- a/src/connection_pool.cpp
+++ /dev/null
@@ -1,514 +0,0 @@
-//
-// Copyright (c) 2026 Mohammad Nejati
-//
-// Distributed under the Boost Software License, Version 1.0. (See accompanying
-// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
-//
-// Official repository: https://github.com/cppalliance/burl
-//
-
-#include
-#include
-
-#include "detail/base64.hpp"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-
-namespace boost
-{
-namespace burl
-{
-
-namespace
-{
-
-std::string_view
-effective_port(const urls::url_view& url)
-{
- if(url.has_port())
- return url.port();
-
- if(url.scheme() == "https")
- return "443";
-
- if(url.scheme() == "http")
- return "80";
-
- if(url.scheme() == "socks5" || url.scheme() == "socks5h")
- return "1080";
-
- return {};
-}
-
-std::string
-origin(urls::url_view url)
-{
- std::string key{ url.scheme() };
- key += "://";
- key += url.encoded_host_and_port();
- return key;
-}
-
-capy::io_task<>
-connect_tcp(
- corosio::tcp_socket& socket,
- capy::executor_ref exec,
- const connection_pool::config& cfg,
- std::string_view host,
- std::string_view port)
-{
- corosio::resolver resolver(exec);
- auto [rec, eps] = co_await resolver.resolve(host, port);
- if(rec)
- co_return rec;
-
- if(auto [cec, ep] = co_await corosio::connect(socket, eps); cec)
- co_return cec;
-
- if(cfg.tcp_nodelay)
- socket.set_option(corosio::socket_option::no_delay(true));
-
- co_return {};
-}
-
-capy::io_task<>
-connect_http_proxy(
- corosio::tcp_socket& socket,
- std::string_view target_host,
- std::string_view target_port,
- urls::url_view proxy)
-{
- std::string host_port(target_host);
- host_port += ':';
- host_port += target_port;
-
- http::request req(http::method::connect, host_port);
- req.set(http::field::host, host_port);
- req.set(http::field::proxy_connection, "keep-alive");
-
- if(proxy.has_userinfo())
- {
- std::string value = "Basic ";
- detail::base64_encode(value, proxy.encoded_userinfo().decode());
- req.set(http::field::proxy_authorization, value);
- }
-
- if(auto [ec, n] =
- co_await capy::write(socket, capy::make_buffer(req.buffer()));
- ec)
- co_return ec;
-
- auto parser_cfg = http::make_parser_config(http::parser_config{ false });
- http::response_parser parser(parser_cfg);
- parser.reset();
- parser.start();
- if(auto [ec] = co_await parser.read_header(socket); ec)
- co_return { error::proxy_connect_failed };
-
- auto status = parser.get().status();
- if(status == http::status::proxy_authentication_required)
- co_return { error::proxy_auth_failed };
- if(to_status_class(status) != http::status_class::successful)
- co_return { error::proxy_connect_failed };
-
- co_return {};
-}
-
-capy::io_task<>
-connect_socks5_proxy(
- corosio::tcp_socket& socket,
- std::string_view target_host,
- std::string_view target_port,
- urls::url_view proxy)
-{
- // Greeting: offer username/password auth only when credentials are present.
- if(proxy.has_userinfo())
- {
- std::uint8_t greeting[4] = { 0x05, 0x02, 0x00, 0x02 };
- auto [ec, n] =
- co_await capy::write(socket, capy::make_buffer(greeting));
- if(ec)
- co_return ec;
- }
- else
- {
- std::uint8_t greeting[3] = { 0x05, 0x01, 0x00 };
- auto [ec, n] =
- co_await capy::write(socket, capy::make_buffer(greeting));
- if(ec)
- co_return ec;
- }
-
- std::uint8_t greeting_resp[2];
- if(auto [ec, n] =
- co_await capy::read(socket, capy::make_buffer(greeting_resp));
- ec)
- co_return ec;
-
- if(greeting_resp[0] != 0x05)
- co_return { error::proxy_unsupported_version };
-
- switch(greeting_resp[1])
- {
- case 0x00: // no authentication required
- break;
- case 0x02: // username/password (RFC 1929)
- {
- std::string auth_req;
- auth_req.push_back(0x01); // sub-negotiation version
-
- auto user = proxy.encoded_user();
- auth_req.push_back(static_cast(user.decoded_size()));
- user.decode({}, urls::string_token::append_to(auth_req));
-
- auto pass = proxy.encoded_password();
- auth_req.push_back(static_cast(pass.decoded_size()));
- pass.decode({}, urls::string_token::append_to(auth_req));
-
- if(auto [ec, n] =
- co_await capy::write(socket, capy::make_buffer(auth_req));
- ec)
- co_return ec;
-
- std::uint8_t auth_resp[2];
- if(auto [ec, n] =
- co_await capy::read(socket, capy::make_buffer(auth_resp));
- ec)
- co_return ec;
-
- if(auth_resp[1] != 0x00)
- co_return { error::proxy_auth_failed };
- break;
- }
- default: // no acceptable method (0xFF) or anything unexpected
- co_return { error::proxy_auth_failed };
- }
-
- // connection request
- std::string conn_req = { 0x05, 0x01, 0x00, 0x03 };
-
- conn_req.push_back(static_cast(target_host.size()));
- conn_req.append(target_host);
-
- auto port =
- static_cast(std::stoul(std::string(target_port)));
- conn_req.push_back(static_cast((port >> 8) & 0xFF));
- conn_req.push_back(static_cast(port & 0xFF));
-
- if(auto [ec, n] = co_await capy::write(socket, capy::make_buffer(conn_req));
- ec)
- co_return ec;
-
- // connection response
- std::uint8_t reply_head[5];
- if(auto [ec, n] =
- co_await capy::read(socket, capy::make_buffer(reply_head));
- ec)
- co_return ec;
-
- if(reply_head[1] != 0x00)
- co_return { error::proxy_connect_failed };
-
- std::size_t tail = 0;
- switch(reply_head[3])
- {
- case 0x01:
- tail = 4 + 2 - 1; // ipv4 + port
- break;
- case 0x03:
- tail = reply_head[4] + 2u; // domain name + port
- break;
- case 0x04:
- tail = 16 + 2 - 1; // ipv6 + port
- break;
- default:
- co_return { error::proxy_connect_failed };
- }
-
- std::string reply_tail;
- reply_tail.resize(tail);
- if(auto [ec, n] =
- co_await capy::read(socket, capy::make_buffer(reply_tail));
- ec)
- co_return ec;
-
- co_return {};
-}
-
-} // namespace
-
-class connection_pool::impl
- : public std::enable_shared_from_this
-{
- struct idle_connection
- {
- std::unique_ptr conn;
- config::clock::time_point idle_since;
- };
-
- capy::executor_ref exec_;
- corosio::tls_context tls_ctx_;
- std::unordered_multimap idle_;
- config config_;
-
-public:
- impl(
- capy::executor_ref exec,
- corosio::tls_context tls_ctx,
- config cfg)
- : exec_(exec)
- , tls_ctx_(std::move(tls_ctx))
- , config_(std::move(cfg))
- {
- }
-
- capy::io_task
- acquire(urls::url_view url)
- {
- auto key = origin(url);
- auto [it, last] = idle_.equal_range(key);
- while(it != last)
- {
- auto entry = std::move(it->second);
- it = idle_.erase(it);
-
- if(config::clock::now() - entry.idle_since >= config_.idle_timeout)
- continue;
-
- if(!entry.conn->is_open())
- continue;
-
- co_return {
- {},
- { std::move(entry.conn),
- weak_from_this(),
- std::move(key),
- config_.io_timeout }
- };
- }
-
- auto [ec, conn] =
- co_await capy::timeout(connect(url), config_.connect_timeout);
- if(ec)
- co_return { ec, {} };
-
- co_return {
- {},
- { std::move(conn),
- weak_from_this(),
- std::move(key),
- config_.io_timeout }
- };
- }
-
- void
- release(pooled_connection pc)
- {
- if(!pc.conn_ || !pc.conn_->is_open())
- return;
-
- if(idle_.count(pc.key_) >= config_.max_idle_per_host)
- return;
-
- idle_.emplace(
- std::move(pc.key_),
- idle_connection{ std::move(pc.conn_), config::clock::now() });
- }
-
-private:
- class tcp_connection final : public connection
- {
- corosio::tcp_socket socket_;
-
- public:
- explicit tcp_connection(corosio::tcp_socket socket)
- : socket_(std::move(socket))
- {
- }
-
- virtual capy::io_task
- read_some(std::span buffers) override
- {
- co_return co_await socket_.read_some(buffers);
- }
-
- virtual capy::io_task
- write_some(std::span buffers) override
- {
- co_return co_await socket_.write_some(buffers);
- }
-
- capy::io_task<>
- shutdown() override
- {
- socket_.shutdown(corosio::shutdown_both);
- co_return {};
- }
-
- bool
- is_open() override
- {
- return socket_.is_open();
- }
- };
-
- class tls_connection final : public connection
- {
- corosio::tcp_socket socket_;
- corosio::openssl_stream stream_;
-
- public:
- tls_connection(
- corosio::tcp_socket socket,
- const corosio::tls_context& ctx)
- : socket_(std::move(socket))
- , stream_(&socket_, ctx)
- {
- }
-
- virtual capy::io_task
- read_some(std::span buffers) override
- {
- return stream_.read_some(buffers);
- }
-
- virtual capy::io_task
- write_some(std::span buffers) override
- {
- return stream_.write_some(buffers);
- }
-
- capy::io_task<>
- handshake()
- {
- return stream_.handshake(corosio::openssl_stream::client);
- }
-
- capy::io_task<>
- shutdown() override
- {
- return stream_.shutdown();
- }
-
- bool
- is_open() override
- {
- return socket_.is_open();
- }
- };
-
- capy::io_task>
- connect(urls::url_view url) const
- {
- auto target_port = effective_port(url);
- if(target_port.empty())
- co_return { error::unsupported_url_scheme, {} };
-
- corosio::tcp_socket socket(exec_);
-
- if(config_.proxy)
- {
- auto const& proxy = *config_.proxy;
- auto proxy_port = effective_port(proxy);
- if(proxy_port.empty())
- co_return { error::unsupported_proxy_scheme, {} };
-
- auto [ec] = co_await connect_tcp(
- socket, exec_, config_, proxy.encoded_host(), proxy_port);
- if(ec)
- co_return { ec, {} };
-
- if(proxy.scheme() == "http")
- {
- auto [ec] = co_await connect_http_proxy(
- socket, url.encoded_host(), target_port, proxy);
- if(ec)
- co_return { ec, {} };
- }
- else if(proxy.scheme() == "socks5" || proxy.scheme() == "socks5h")
- {
- auto [ec] = co_await connect_socks5_proxy(
- socket, url.encoded_host(), target_port, proxy);
- if(ec)
- co_return { ec, {} };
- }
- else
- {
- co_return { error::unsupported_proxy_scheme, {} };
- }
- }
- else
- {
- auto [ec] = co_await connect_tcp(
- socket, exec_, config_, url.encoded_host(), target_port);
- if(ec)
- co_return { ec, {} };
- }
-
- if(url.scheme_id() == urls::scheme::https)
- {
- auto tls_ctx = tls_ctx_;
- tls_ctx.set_hostname(url.encoded_host());
-
- auto conn =
- std::make_unique(std::move(socket), tls_ctx);
- auto [hec] = co_await conn->handshake();
- if(hec)
- co_return { hec, {} };
-
- co_return { {}, std::move(conn) };
- }
-
- co_return { {}, std::make_unique(std::move(socket)) };
- }
-};
-
-connection_pool::connection_pool(
- capy::executor_ref exec,
- corosio::tls_context tls_ctx,
- config cfg)
- : impl_(
- std::make_shared(
- exec, std::move(tls_ctx), std::move(cfg)))
-{
-}
-
-capy::io_task
-connection_pool::acquire(urls::url_view url)
-{
- return impl_->acquire(url);
-}
-
-void
-connection_pool::pooled_connection::return_to_pool()
-{
- if(auto pool = pool_.lock())
- pool->release(std::move(*this));
-}
-
-} // namespace burl
-} // namespace boost
diff --git a/src/detail/reuse.cpp b/src/detail/can_reuse_conn.cpp
similarity index 84%
rename from src/detail/reuse.cpp
rename to src/detail/can_reuse_conn.cpp
index d3ded92..2d953aa 100644
--- a/src/detail/reuse.cpp
+++ b/src/detail/can_reuse_conn.cpp
@@ -7,7 +7,7 @@
// Official repository: https://github.com/cppalliance/burl
//
-#include "reuse.hpp"
+#include "can_reuse_conn.hpp"
namespace boost
{
@@ -40,7 +40,13 @@ can_reuse_conn(http::response_parser& parser) noexcept
}
}
- return parser.is_complete();
+ if(!parser.is_complete())
+ return false;
+
+ if(parser.has_buffered_data())
+ return false;
+
+ return true;
}
} // namespace detail
diff --git a/src/detail/reuse.hpp b/src/detail/can_reuse_conn.hpp
similarity index 83%
rename from src/detail/reuse.hpp
rename to src/detail/can_reuse_conn.hpp
index 36b6e6e..4343d99 100644
--- a/src/detail/reuse.hpp
+++ b/src/detail/can_reuse_conn.hpp
@@ -7,8 +7,8 @@
// Official repository: https://github.com/cppalliance/burl
//
-#ifndef BOOST_BURL_SRC_DETAIL_REUSE_HPP
-#define BOOST_BURL_SRC_DETAIL_REUSE_HPP
+#ifndef BOOST_BURL_SRC_DETAIL_CAN_REUSE_CONN_HPP
+#define BOOST_BURL_SRC_DETAIL_CAN_REUSE_CONN_HPP
#include
diff --git a/src/detail/connection_pool.cpp b/src/detail/connection_pool.cpp
new file mode 100644
index 0000000..4592016
--- /dev/null
+++ b/src/detail/connection_pool.cpp
@@ -0,0 +1,370 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#include "connection_pool.hpp"
+
+#include
+
+#include "http_tunnel.hpp"
+#include "socks5_tunnel.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+namespace
+{
+
+std::string_view
+effective_port(const urls::url_view& url)
+{
+ if(url.has_port())
+ return url.port();
+
+ if(url.scheme() == "https")
+ return "443";
+
+ if(url.scheme() == "http")
+ return "80";
+
+ if(url.scheme() == "socks5" || url.scheme() == "socks5h")
+ return "1080";
+
+ return {};
+}
+
+std::string
+origin(urls::url_view url)
+{
+ std::string key{ url.scheme() };
+ key += "://";
+ key += url.encoded_host_and_port();
+ return key;
+}
+
+capy::io_task<>
+connect_tcp(
+ corosio::tcp_socket& socket,
+ capy::executor_ref exec,
+ const client::config& cfg,
+ std::string_view host,
+ std::string_view port)
+{
+ corosio::resolver resolver(exec);
+ auto [rec, eps] = co_await resolver.resolve(host, port);
+ if(rec)
+ co_return rec;
+
+ if(auto [cec, ep] = co_await corosio::connect(socket, eps); cec)
+ co_return cec;
+
+ if(cfg.tcp_nodelay)
+ socket.set_option(corosio::socket_option::no_delay(true));
+
+ co_return {};
+}
+
+class tcp_connection final : public connection
+{
+ corosio::tcp_socket socket_;
+
+public:
+ explicit tcp_connection(corosio::tcp_socket socket)
+ : socket_(std::move(socket))
+ {
+ }
+
+ bool
+ is_open() const noexcept override
+ {
+ return socket_.is_open();
+ }
+
+ capy::io_task<>
+ shutdown() override
+ {
+ socket_.shutdown(corosio::shutdown_both);
+ co_return {};
+ }
+
+private:
+ capy::io_task
+ do_read_some(std::span buffers) override
+ {
+ co_return co_await socket_.read_some(buffers);
+ }
+
+ capy::io_task
+ do_write_some(std::span buffers) override
+ {
+ co_return co_await socket_.write_some(buffers);
+ }
+};
+
+class tls_connection final : public connection
+{
+ corosio::tcp_socket socket_;
+ corosio::openssl_stream stream_;
+
+public:
+ tls_connection(corosio::tcp_socket socket, const corosio::tls_context& ctx)
+ : socket_(std::move(socket))
+ , stream_(&socket_, ctx)
+ {
+ }
+
+ capy::io_task<>
+ handshake()
+ {
+ return stream_.handshake(corosio::openssl_stream::client);
+ }
+
+ bool
+ is_open() const noexcept override
+ {
+ return socket_.is_open();
+ }
+
+ capy::io_task<>
+ shutdown() override
+ {
+ return stream_.shutdown();
+ }
+
+private:
+ capy::io_task
+ do_read_some(std::span buffers) override
+ {
+ return stream_.read_some(buffers);
+ }
+
+ capy::io_task
+ do_write_some(std::span buffers) override
+ {
+ return stream_.write_some(buffers);
+ }
+};
+
+class stream_connection final : public connection
+{
+ capy::any_stream stream_;
+ bool open_ = true;
+
+public:
+ explicit stream_connection(capy::any_stream stream)
+ : stream_(std::move(stream))
+ {
+ }
+
+ bool
+ is_open() const noexcept override
+ {
+ return open_;
+ }
+
+ capy::io_task<>
+ shutdown() override
+ {
+ open_ = false;
+ co_return {};
+ }
+
+private:
+ capy::io_task
+ do_read_some(std::span buffers) override
+ {
+ auto [ec, n] = co_await stream_.read_some(buffers);
+ if(ec)
+ open_ = false;
+ co_return { ec, n };
+ }
+
+ capy::io_task
+ do_write_some(std::span buffers) override
+ {
+ auto [ec, n] = co_await stream_.write_some(buffers);
+ if(ec)
+ open_ = false;
+ co_return { ec, n };
+ }
+};
+
+} // namespace
+
+connection_pool::connection_pool(
+ capy::executor_ref exec,
+ corosio::tls_context tls_ctx,
+ config cfg)
+ : exec_(exec)
+ , tls_ctx_(std::move(tls_ctx))
+ , config_(std::move(cfg))
+{
+}
+
+capy::io_task
+connection_pool::acquire(urls::url_view url)
+{
+ auto key = origin(url);
+ auto [it, last] = idle_.equal_range(key);
+ while(it != last)
+ {
+ auto entry = std::move(it->second);
+ it = idle_.erase(it);
+
+ if(config::clock::now() - entry.idle_since >= config_.pool_idle_timeout)
+ continue;
+
+ if(!entry.conn->is_open())
+ continue;
+
+ co_return {
+ {},
+ { std::move(entry.conn),
+ weak_from_this(),
+ std::move(key),
+ config_.io_timeout }
+ };
+ }
+
+ auto [ec, conn] =
+ co_await capy::timeout(connect(url), config_.connect_timeout);
+ if(ec)
+ co_return { ec, {} };
+
+ co_return {
+ {},
+ { std::move(conn),
+ weak_from_this(),
+ std::move(key),
+ config_.io_timeout }
+ };
+}
+
+void
+connection_pool::release(pooled_connection pc)
+{
+ if(!pc.conn_ || !pc.conn_->is_open())
+ return;
+
+ if(idle_.count(pc.key_) >= config_.pool_max_idle_per_host)
+ return;
+
+ idle_.emplace(
+ std::move(pc.key_),
+ idle_connection{ std::move(pc.conn_), config::clock::now() });
+}
+
+capy::io_task>
+connection_pool::connect(urls::url_view url) const
+{
+ if(config_.connect_handler)
+ {
+ auto [ec, stream] = co_await config_.connect_handler(url);
+ if(ec)
+ co_return { ec, {} };
+ co_return {
+ {}, std::make_unique(std::move(stream)) };
+ }
+
+ auto target_port = effective_port(url);
+ if(target_port.empty())
+ co_return { error::unsupported_url_scheme, {} };
+
+ corosio::tcp_socket socket(exec_);
+
+ if(config_.proxy)
+ {
+ auto const& proxy = *config_.proxy;
+ auto proxy_port = effective_port(proxy);
+ if(proxy_port.empty())
+ co_return { error::unsupported_proxy_scheme, {} };
+
+ auto [ec] = co_await connect_tcp(
+ socket, exec_, config_, proxy.encoded_host(), proxy_port);
+ if(ec)
+ co_return { ec, {} };
+
+ if(proxy.scheme() == "http")
+ {
+ auto [ec] = co_await open_http_tunnel(
+ capy::any_stream(&socket),
+ url.encoded_host(),
+ target_port,
+ proxy);
+ if(ec)
+ co_return { ec, {} };
+ }
+ else if(proxy.scheme() == "socks5" || proxy.scheme() == "socks5h")
+ {
+ auto [ec] = co_await open_socks5_tunnel(
+ capy::any_stream(&socket),
+ url.encoded_host(),
+ target_port,
+ proxy);
+ if(ec)
+ co_return { ec, {} };
+ }
+ else
+ {
+ co_return { error::unsupported_proxy_scheme, {} };
+ }
+ }
+ else
+ {
+ auto [ec] = co_await connect_tcp(
+ socket, exec_, config_, url.encoded_host(), target_port);
+ if(ec)
+ co_return { ec, {} };
+ }
+
+ if(url.scheme_id() == urls::scheme::https)
+ {
+ auto tls_ctx = tls_ctx_;
+ tls_ctx.set_hostname(url.encoded_host());
+
+ auto conn =
+ std::make_unique(std::move(socket), tls_ctx);
+ auto [hec] = co_await conn->handshake();
+ if(hec)
+ co_return { hec, {} };
+
+ co_return { {}, std::move(conn) };
+ }
+
+ co_return { {}, std::make_unique(std::move(socket)) };
+}
+
+void
+pooled_connection::return_to_pool()
+{
+ if(auto pool = pool_.lock())
+ pool->release(std::move(*this));
+}
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
diff --git a/src/detail/connection_pool.hpp b/src/detail/connection_pool.hpp
new file mode 100644
index 0000000..9329793
--- /dev/null
+++ b/src/detail/connection_pool.hpp
@@ -0,0 +1,69 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#ifndef BOOST_BURL_SRC_DETAIL_CONNECTION_POOL_HPP
+#define BOOST_BURL_SRC_DETAIL_CONNECTION_POOL_HPP
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+class connection_pool
+ : public std::enable_shared_from_this
+{
+ using config = client::config;
+
+ struct idle_connection
+ {
+ std::unique_ptr conn;
+ config::clock::time_point idle_since;
+ };
+
+ capy::executor_ref exec_;
+ corosio::tls_context tls_ctx_;
+ std::unordered_multimap idle_;
+ config config_;
+
+public:
+ connection_pool(
+ capy::executor_ref exec,
+ corosio::tls_context tls_ctx,
+ config cfg);
+
+ capy::io_task
+ acquire(urls::url_view url);
+
+ void
+ release(pooled_connection pc);
+
+private:
+ capy::io_task>
+ connect(urls::url_view url) const;
+};
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
+
+#endif
diff --git a/src/detail/drain.hpp b/src/detail/drain_body.cpp
similarity index 77%
rename from src/detail/drain.hpp
rename to src/detail/drain_body.cpp
index 40a5ff2..d424813 100644
--- a/src/detail/drain.hpp
+++ b/src/detail/drain_body.cpp
@@ -7,15 +7,10 @@
// Official repository: https://github.com/cppalliance/burl
//
-#ifndef BOOST_BURL_SRC_DETAIL_DRAIN_HPP
-#define BOOST_BURL_SRC_DETAIL_DRAIN_HPP
+#include "drain_body.hpp"
#include
#include
-#include
-#include
-
-#include
namespace boost
{
@@ -24,13 +19,10 @@ namespace burl
namespace detail
{
-/** Read and discard the remaining body.
-*/
-template
capy::io_task<>
drain_body(
http::response_parser& parser,
- Stream& conn,
+ capy::any_stream conn,
std::uint64_t limit)
{
auto source = parser.source_for(conn);
@@ -54,5 +46,3 @@ drain_body(
} // namespace detail
} // namespace burl
} // namespace boost
-
-#endif
diff --git a/src/detail/drain_body.hpp b/src/detail/drain_body.hpp
new file mode 100644
index 0000000..1552c81
--- /dev/null
+++ b/src/detail/drain_body.hpp
@@ -0,0 +1,38 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#ifndef BOOST_BURL_SRC_DETAIL_DRAIN_BODY_HPP
+#define BOOST_BURL_SRC_DETAIL_DRAIN_BODY_HPP
+
+#include
+#include
+#include
+
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+/** Read and discard the remaining body.
+*/
+capy::io_task<>
+drain_body(
+ http::response_parser& parser,
+ capy::any_stream conn,
+ std::uint64_t limit);
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
+
+#endif
diff --git a/src/detail/http_tunnel.cpp b/src/detail/http_tunnel.cpp
new file mode 100644
index 0000000..79bc127
--- /dev/null
+++ b/src/detail/http_tunnel.cpp
@@ -0,0 +1,78 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#include "http_tunnel.hpp"
+
+#include
+
+#include "base64.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+capy::io_task<>
+open_http_tunnel(
+ capy::any_stream stream,
+ std::string_view target_host,
+ std::string_view target_port,
+ urls::url_view proxy)
+{
+ std::string host_port(target_host);
+ host_port += ':';
+ host_port += target_port;
+
+ http::request req(http::method::connect, host_port);
+ req.set(http::field::host, host_port);
+ req.set(http::field::proxy_connection, "keep-alive");
+
+ if(proxy.has_userinfo())
+ {
+ std::string value = "Basic ";
+ detail::base64_encode(value, proxy.encoded_userinfo().decode());
+ req.set(http::field::proxy_authorization, value);
+ }
+
+ if(auto [ec, n] =
+ co_await capy::write(stream, capy::make_buffer(req.buffer()));
+ ec)
+ co_return ec;
+
+ auto parser_cfg = http::make_parser_config(http::parser_config{ false });
+ http::response_parser parser(parser_cfg);
+ parser.reset();
+ parser.start();
+ if(auto [ec] = co_await parser.read_header(stream); ec)
+ co_return { error::proxy_connect_failed };
+
+ auto status = parser.get().status();
+ if(status == http::status::proxy_authentication_required)
+ co_return { error::proxy_auth_failed };
+ if(to_status_class(status) != http::status_class::successful)
+ co_return { error::proxy_connect_failed };
+
+ co_return {};
+}
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
diff --git a/src/detail/http_tunnel.hpp b/src/detail/http_tunnel.hpp
new file mode 100644
index 0000000..f92bba6
--- /dev/null
+++ b/src/detail/http_tunnel.hpp
@@ -0,0 +1,37 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#ifndef BOOST_BURL_SRC_DETAIL_HTTP_TUNNEL_HPP
+#define BOOST_BURL_SRC_DETAIL_HTTP_TUNNEL_HPP
+
+#include
+#include
+#include
+
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+capy::io_task<>
+open_http_tunnel(
+ capy::any_stream stream,
+ std::string_view target_host,
+ std::string_view target_port,
+ urls::url_view proxy);
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
+
+#endif
diff --git a/src/detail/redirect.cpp b/src/detail/redirect.cpp
new file mode 100644
index 0000000..b7c2794
--- /dev/null
+++ b/src/detail/redirect.cpp
@@ -0,0 +1,71 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#include "redirect.hpp"
+
+#include
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+is_redirect_result
+is_redirect(
+ http::status status,
+ const client::config& cfg) noexcept
+{
+ // The specifications do not intend for 301 and 302
+ // redirects to change the HTTP method, but most
+ // user agents do change the method in practice.
+ switch(status)
+ {
+ case http::status::moved_permanently:
+ return { true, !cfg.post301 };
+ case http::status::found:
+ return { true, !cfg.post302 };
+ case http::status::see_other:
+ return { true, !cfg.post303 };
+ case http::status::temporary_redirect:
+ case http::status::permanent_redirect:
+ return { true, false };
+ default:
+ return { false, false };
+ }
+}
+
+urls::url
+resolve_location(
+ http::response_base const& response,
+ const urls::url_view& base)
+{
+ auto it = response.find(http::field::location);
+ if(it != response.end())
+ {
+ auto rs = urls::parse_uri_reference(it->value);
+ if(rs.has_value())
+ {
+ urls::url url;
+ urls::resolve(base, rs.value(), url);
+ // RFC 9110, Section 10.2.2: a Location without a
+ // fragment inherits the fragment of the target URI.
+ if(!url.has_fragment() && base.has_fragment())
+ url.set_encoded_fragment(base.encoded_fragment());
+ return url;
+ }
+ }
+ return {};
+}
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
diff --git a/src/detail/redirect.hpp b/src/detail/redirect.hpp
new file mode 100644
index 0000000..e50a3f0
--- /dev/null
+++ b/src/detail/redirect.hpp
@@ -0,0 +1,47 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#ifndef BOOST_BURL_SRC_DETAIL_REDIRECT_HPP
+#define BOOST_BURL_SRC_DETAIL_REDIRECT_HPP
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+struct is_redirect_result
+{
+ bool is_redirect = false;
+ bool need_method_change = false;
+};
+
+is_redirect_result
+is_redirect(
+ http::status status,
+ const client::config& cfg) noexcept;
+
+urls::url
+resolve_location(
+ http::response_base const& response,
+ const urls::url_view& base);
+
+} // namespace detail
+} // namespace burl
+} // namespace boost
+
+#endif
diff --git a/src/detail/socks5_tunnel.cpp b/src/detail/socks5_tunnel.cpp
new file mode 100644
index 0000000..3ea923f
--- /dev/null
+++ b/src/detail/socks5_tunnel.cpp
@@ -0,0 +1,153 @@
+//
+// Copyright (c) 2026 Mohammad Nejati
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/cppalliance/burl
+//
+
+#include "socks5_tunnel.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace boost
+{
+namespace burl
+{
+namespace detail
+{
+
+capy::io_task<>
+open_socks5_tunnel(
+ capy::any_stream stream,
+ std::string_view target_host,
+ std::string_view target_port,
+ urls::url_view proxy)
+{
+ // Greeting: offer username/password auth only when credentials are present.
+ if(proxy.has_userinfo())
+ {
+ std::uint8_t greeting[4] = { 0x05, 0x02, 0x00, 0x02 };
+ auto [ec, n] =
+ co_await capy::write(stream, capy::make_buffer(greeting));
+ if(ec)
+ co_return ec;
+ }
+ else
+ {
+ std::uint8_t greeting[3] = { 0x05, 0x01, 0x00 };
+ auto [ec, n] =
+ co_await capy::write(stream, capy::make_buffer(greeting));
+ if(ec)
+ co_return ec;
+ }
+
+ std::uint8_t greeting_resp[2];
+ if(auto [ec, n] =
+ co_await capy::read(stream, capy::make_buffer(greeting_resp));
+ ec)
+ co_return ec;
+
+ if(greeting_resp[0] != 0x05)
+ co_return { error::proxy_unsupported_version };
+
+ switch(greeting_resp[1])
+ {
+ case 0x00: // no authentication required
+ break;
+ case 0x02: // username/password (RFC 1929)
+ {
+ std::string auth_req;
+ auth_req.push_back(0x01); // sub-negotiation version
+
+ auto user = proxy.encoded_user();
+ auth_req.push_back(static_cast(user.decoded_size()));
+ user.decode({}, urls::string_token::append_to(auth_req));
+
+ auto pass = proxy.encoded_password();
+ auth_req.push_back(static_cast(pass.decoded_size()));
+ pass.decode({}, urls::string_token::append_to(auth_req));
+
+ if(auto [ec, n] =
+ co_await capy::write(stream, capy::make_buffer(auth_req));
+ ec)
+ co_return ec;
+
+ std::uint8_t auth_resp[2];
+ if(auto [ec, n] =
+ co_await capy::read(stream, capy::make_buffer(auth_resp));
+ ec)
+ co_return ec;
+
+ if(auth_resp[1] != 0x00)
+ co_return { error::proxy_auth_failed };
+ break;
+ }
+ default: // no acceptable method (0xFF) or anything unexpected
+ co_return { error::proxy_auth_failed };
+ }
+
+ // connection request
+ std::string conn_req = { 0x05, 0x01, 0x00, 0x03 };
+
+ conn_req.push_back(static_cast