Compare commits

...

105 Commits

Author SHA1 Message Date
yhirose 781c55f120 Release v0.30.0 2025-12-31 22:19:05 -05:00
yhirose 40f7985e02 Update copyright year 2025-12-31 22:18:11 -05:00
PerseoGI f85f30a637 Apple frameworks: match CoreFoundation and CFNetwork linkage (#2317)
* Apple frameworks: match linkage with CoreFoundation and CFNetwork with actual code

* Fix appleframeworks in Meson code
2025-12-31 12:49:59 -05:00
yhirose 6da7f0c61c Fix port reuse problem 2025-12-31 00:34:15 -05:00
yhirose 2ba0e7a797 Fix build error 2025-12-30 19:46:21 -05:00
yhirose ded82448aa clang-format 2025-12-30 17:34:52 -05:00
yhirose 98048a033a Merge commit from fork 2025-12-30 17:32:42 -05:00
yhirose 7ae794a6bf Fix #2315 2025-12-26 16:17:43 -05:00
yhirose 385adefb11 Use HOST and PORT in test.cc 2025-12-26 00:24:19 -05:00
yhirose b7c2f04318 Fix potential arithmatic overflow problem 2025-12-25 22:19:37 -05:00
yhirose d23cf77cd0 Resolve #2313 2025-12-23 20:10:59 -05:00
yhirose 5304464a53 Release v0.29.0 2025-12-23 00:03:49 -05:00
yhirose db98efee5a Fix problem with Proxy test 2025-12-22 23:10:24 -05:00
yhirose cdf0d33258 Fix #2301 2025-12-22 22:37:56 -05:00
Aaron Gokaslan 25688258ad Add another missing std::move for _base_dirs vector (#2314) 2025-12-22 20:43:46 -05:00
Aaron Gokaslan f0990ca96d Use std::move for request redirection (#2311)
Prevents an additional copy
2025-12-17 12:04:37 -05:00
Aaron Gokaslan 0461cb770c Avoid unncessary copying of request and response objects (#2310) 2025-12-17 12:04:17 -05:00
yhirose 51b704b902 Implement SSEClient (#2308)
* Implement SSEClient

* Fix Windows problem
2025-12-15 00:00:42 -05:00
yhirose 7eb03e81fc Refactoring 2025-12-14 17:41:11 -05:00
yhirose 6a6d4161d1 Removed DigestAuthTest.FromHTTPWatch_Online_HTTPCan 2025-12-14 14:45:48 -05:00
Copilot 63b07ada43 Initial plan (#2307)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-12-14 13:54:29 -05:00
Jean-Francois Simoneau 2de4c59bc2 Add Zstd support through meson (#2293)
* Add Zstd support through meson

* Add libzstd-dev to abitest
2025-12-13 23:55:58 -05:00
Jean-Francois Simoneau b7097f1386 Replace httpbin.org with httpcan.org (#2300)
* Replace httpbin.org with httpcan.org

* Fix DigestAuthTest.FromHTTPWatch_Online test
2025-12-13 23:52:05 -05:00
Aaron Gokaslan 681d388247 Use move semantics for auth key and value (#2306) 2025-12-13 22:53:10 -05:00
Aaron Gokaslan ae94d64f67 Remove another unnecessary string copy (#2305) 2025-12-13 22:52:26 -05:00
Aaron Gokaslan 3401877d3d Change single char string literals to chars (#2304) 2025-12-13 22:52:12 -05:00
Aaron Gokaslan bce08e62f9 Remove unnecessary copies for AcceptEntry (#2303) 2025-12-13 22:49:58 -05:00
Carter Green f4ecb96e54 Fix linker error on macOS (#2299) 2025-12-11 20:12:31 -05:00
Aaron Gokaslan c23764269d Use std::move for boundary in set_boundary method (#2298) 2025-12-09 22:24:33 -05:00
Aaron Gokaslan f441cd2a44 Use std::move for content_provider in adapter (#2297) 2025-12-09 22:23:45 -05:00
Miko c3613c6977 Update the split.py file (#2295) 2025-12-08 22:18:01 -05:00
yhirose 87c2b4e584 Fix #2294 2025-12-08 19:32:35 -05:00
yhirose c795ad1c32 Fix #2259. Add query string normalization to preserve parameter order in requests 2025-12-05 21:39:40 -05:00
yhirose 3e0fa33559 Implement ETag and Last-Modified support for static file responses and If-Range requests (#2286)
* Fix #2242: Implement ETag and Last-Modified support for static file responses

* Add ETag and Last-Modified handling for If-Range requests

* Enhance HTTP date parsing with improved error handling and locale support

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/test.cc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor ETag handling: separate strong and weak ETag checks for If-Range requests

* Fix type for mtime in FileStat and improve ETag handling comments

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Resolved code review comments

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor ETag handling: use 'auto' for type inference and improve code readability

* Refactor ETag handling: extract check_if_not_modified and check_if_range methods for improved readability and maintainability

* Code cleanup

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/test.cc

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update httplib.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enhance ETag handling and validation in httplib.h and add comprehensive tests in test.cc

* Refactor ETag comparison logic and add test for If-None-Match with non-existent file

* Fix #2287

* Code cleanup

* Add tests for extreme date values and negative file modification time in ETag handling

* Update HTTP-date parsing comments to reference RFC 9110

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 18:58:54 -05:00
yhirose 27b73f050e Fix #2287 2025-12-05 08:50:12 -05:00
yhirose dbd5ca4bf2 Add error handling for stream read timeouts and connection closures 2025-12-04 19:19:18 -05:00
yhirose 143019a38c Fix #2284 2025-12-02 17:24:27 -05:00
yhirose 1d36013fc3 Update README 2025-12-02 17:08:53 -05:00
yhirose 8bba34eebc Add New Streaming API support (#2281) 2025-12-01 21:40:13 -05:00
Aaron Gokaslan 0a9102ff6b Optimize ThreadPool and MatcherBase constructors (#2283)
Add a missing reserve and missing std::move to each ctor respectively. The latter should really be caught by a clang-tidy perf linter.
2025-11-30 21:49:23 -05:00
bigmoonbit c1fa5e1710 chore: fix some typos in comments (#2282)
Signed-off-by: bigmoonbit <bigmoonbit@outlook.com>
2025-11-27 20:34:18 -05:00
yhirose 84796738fc Fix #2248 2025-11-25 22:22:07 -05:00
yhirose adf58bf474 Release v0.28.0 2025-11-25 21:06:51 -05:00
yhirose 337fbb0793 Fix #2279
Enhance request handling: add support for requests without Content-Length or Transfer-Encoding headers
2025-11-25 20:30:43 -05:00
Copilot 9e7861b0b4 Add #undef _res after including resolv.h to prevent macro conflicts (#2280)
* Initial plan

* Add #undef _res after including resolv.h to prevent macro conflicts

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

* Complete task - added #undef _res after resolv.h include

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

* Remove accidentally committed codeql build artifacts

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

* Add inline comment explaining why #undef _res is necessary

Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yhirose <357397+yhirose@users.noreply.github.com>
2025-11-25 20:00:10 -05:00
Clarkok Zhang 27ee115a60 Fix android getaddrinfo issue (#2273)
Co-authored-by: Clarkok Zhang <clarkok8@gmail.com>
2025-11-14 16:48:21 -05:00
Clarkok Zhang 59882752aa Add Client::Post with both content provider and receiver (#2268)
Co-authored-by: Clarkok Zhang <clarkok8@gmail.com>
2025-11-14 14:52:06 -05:00
yhirose 61e9f7ce8f Fix #2270 2025-11-14 14:17:04 -05:00
VZ 1acf18876f CMake: Add HTTPLIB_SHARED option, don't define BUILD_SHARED_LIBS (#2266)
To avoid surprises in the projects consuming the library, don't define
BUILD_SHARED_LIBS option ourselves but just use its value, if provided,
to define HTTPLIB_SHARED option which can be also set directly to
specify whether we should build static or shared library.

Closes #2263.
2025-11-10 22:17:43 -05:00
chansikpark 4b2b851dbb Fix HTTP 414 errors hanging until timeout (#2260)
* Fix HTTP 414 errors hanging until timeout

* All errors (status code 400+) close the connection

* 🧹

---------

Co-authored-by: Wor Ker <worker@factory>
2025-11-02 22:23:42 -05:00
yhirose 551f96d4a2 Remove REMOTE_PORT dependency from UnixSocketTest.PeerPid 2025-10-27 20:40:12 -04:00
yhirose eacc1ca98e Release v0.27.0 2025-10-27 19:57:53 -04:00
yhirose ac9ebb0ee3 Merge commit from fork
* Fix "Untrusted HTTP Header Handling (REMOTE*/LOCAL*)"

* Fix "Untrusted HTTP Header Handling (X-Forwarded-For)"

* Fix security problems in docker/main.cc
2025-10-27 19:54:12 -04:00
yhirose 11eed05ce7 Fix #2255 and #2256 2025-10-27 19:51:55 -04:00
yhirose f3bba0646a Fix benchmark test issue on Windows (#2258) 2025-10-27 18:27:55 -04:00
yhirose 2da189f88c Fix EventDispatcher problem (#2257) 2025-10-27 18:10:52 -04:00
yhirose 6e0f211cff Fix problem with .gitignore for examples 2025-10-27 17:54:27 -04:00
yhirose 318a3fe425 Fix problem with installing OpenSSL for Windows (#2254) 2025-10-25 22:35:21 -04:00
yhirose 2d8d524178 Fix #2251 2025-10-25 22:06:42 -04:00
yhirose afa88dbe70 Fix #2250 2025-10-25 21:36:53 -04:00
yhirose 08133b593b Merge branch 'staticlibs-ssl_error_reporting' 2025-10-25 19:32:31 -04:00
yhirose 8aedbf4547 Add a unit test 2025-10-25 19:31:48 -04:00
yhirose cde29362ef Merge branch 'ssl_error_reporting' of github.com:staticlibs/cpp-httplib into staticlibs-ssl_error_reporting 2025-10-25 19:09:09 -04:00
yhirose bae40fcdf2 Resolve #2237 2025-10-25 16:48:45 -04:00
crueter db561f5552 [cmake] FindBrotli: do not add Brotli:: targets if they already exist (#2249)
Not checking for this is terrible practice.
2025-10-16 09:59:15 -04:00
Andrea Pappacoda 35c52c1ab9 build(meson): use C++17 for gtest >= 1.17.0 (#2241) 2025-09-20 15:06:49 -04:00
Alex Kasko 23ff9a5605 Fix error reporting in SSLClient
When the `SSLClient` is used to connect to a plain-HTTP server (which
can happen in clients due to some end-user misconfiguration) it can
return a failure from the `send()` call without setting the `Error`
reference to the corresponding error code. This can cause problems to
callers, that may expect that, when the check like this is passed on
the response:

```c++
if (res.error() == Error::Success)
```

then they can access the response contents with `res.value()`. When
`SSLClient`'s connection fails - the contents `unique_ptr` is not set
and an attemt to access it causes UB.

This change fixes the `SSLClient::create_and_connect_socket` method
making sure that, the `Error` value is set correctly when the
`is_valid()` check fails.
2025-09-18 13:44:39 +01:00
yhirose 41be1e24e3 Code cleanup 2025-09-15 07:59:53 -04:00
Jonas van den Berg 6e52d0a057 Fix UB by use of dangling references in getaddrinfo_with_timeout (#2232)
* Fix use of dangling references

When the resolve thread is detached, local variables were still used, which could lead to a program crash.

* Add test to verify dangling ref fix

* Add missing brace initialization

* Assert that the remaining fields are really zeroed

* Fix use of chrono literals
2025-09-14 20:05:09 -04:00
apocelipes f72b4582e6 Fix: Fix Windows Cross-Compilation (#2234) 2025-09-14 08:05:51 -04:00
yhirose 89c932f313 Release v0.26.0 2025-08-29 16:05:44 -04:00
yhirose eb5a65e0df Fix #2217 2025-08-29 15:01:59 -04:00
kgokalp 7a3b92bbd9 Fix: handle EAI_ALLDONE from gai_suspend in getaddrinfo_with_timeout (#2228) 2025-08-28 11:08:32 -04:00
yhirose eb11032797 Fix platform problem 2025-08-27 00:31:14 -04:00
yhirose 54e75dc8ef Add manual run 2025-08-26 23:34:18 -04:00
yhirose b20b5fdd1f Add 'release-docker' workflow 2025-08-26 23:18:59 -04:00
yhirose f4cc542d4b Fix Dockerfile problem with CMD 2025-08-26 22:17:54 -04:00
yhirose 4285d33992 Fix #2223 (#2224)
* Fix #2223

* Fix build error
2025-08-26 21:42:13 -04:00
yhirose 92b4f53012 clang-format 2025-08-26 20:29:43 -04:00
tejas b8e21eac89 Initialize start time for server (#2220)
* Initialize start time for server

By initializing start_time_ for server, I hope to measure the time taken to process a request at the end maybe in the set_logger callback and print it.

I only see current usage in client with server retaining the inital min value

* Add test to verify start time is initialized

* Address review comments

* run clang format
2025-08-26 15:34:13 -04:00
Sergey 3fae5f1473 osx: fix inconsistent use of the macro TARGET_OS_OSX (#2222)
* osx: fix inconsistent use of the macro `TARGET_OS_OSX`

Fixed the build error on iOS:

```
httplib.h:3583:3: error: unknown type name 'CFStringRef'
  870 |   CFStringRef hostname_ref = CFStringCreateWithCString(
```

Note, `TARGET_OS_OSX` is defined but is 0 when `TARGET_OS_IOS` is 1,
and vise versa. Hence, `TARGET_OS_MAC` should have been used, that is
set to 1 for the both targets.

* improve: non-blocking getaddrinfo() for all mac target variants

`TARGET_OS_MAC` should have been used, that is set to 1 for all other
targets: OSX, IPHONE (IOS, TV, WATCH, VISION, BRIDGE), SIMULATOR,
DRIVERKIT.
2025-08-26 12:46:51 -04:00
Andrea Pappacoda fe7fe15d2e build(meson): fix new build option names (#2208)
This is a follow-up to commit 4ff7a1c858,
which introduced new simplified build options and deprecated the old
ones. I forgot to also change the various get_option() calls,
effectively rendering the new option names useless, as they would not
get honoured.
2025-08-19 15:22:08 -04:00
Thomas Beutlich fbd6ce7a3f Make code sample compilable (#2207) 2025-08-14 06:57:39 -04:00
Thomas Beutlich dffce89514 #2201 Fix 32-bit MSVC compiler error due to unknown command #warning (#2202) 2025-08-12 17:06:09 -04:00
yhirose 3f44c80fd3 Release v0.25.0 2025-08-07 20:58:39 -04:00
yhirose a2bb6f6c1e Update docker/main.cc 2025-08-07 20:57:37 -04:00
Thomas Beutlich 7012e765e1 CMake: Check pointer size at configure time (#2197)
* Check pointer size at configure time

* Relax check to warning
2025-08-07 17:14:19 -04:00
yhirose b1c1fa2dc6 Code cleanup 2025-08-07 00:09:09 -04:00
yhirose fbee136dca Fix #2193. Allow _WIN32 2025-08-06 23:12:33 -04:00
yhirose 70cca55caf Workaround for chocolatey issue with the latest OpenSSL 2025-08-06 17:54:08 -04:00
yhirose cdaed14925 Update README 2025-08-06 17:41:03 -04:00
yhirose b52d7d8411 ErrorLogger support (#870) (#2195) 2025-08-06 17:38:18 -04:00
Thomas Beutlich acf28a362d #2191 Check for minimum required Windows version (#2192) 2025-08-01 20:16:43 -04:00
yhirose 0b3758ec36 Fix problem with Windows version check 2025-07-30 17:39:40 -04:00
yhirose a5d4c143e5 Release v0.24.0 2025-07-29 19:47:48 -04:00
yhirose c0c36f021d Fix #2184, #2185 (#2190)
* Fix #2184, #2185

* Fix build error

* Update

* Update
2025-07-29 19:29:37 -04:00
yhirose 8e8a23e3d2 Fix #2187 2025-07-24 19:35:47 -04:00
yhirose 890a2dd85d Fix #2189 2025-07-24 17:04:59 -04:00
yhirose ca5fe354fb Release v0.23.1 2025-07-16 17:59:52 -04:00
Benjamin Gilbert dd98d2a24d build(meson): warn/fail on 32-bit machines (#2181)
On 32-bit Windows, meson setup fails with an unclear error:

    meson.build:25:16: ERROR: Could not get define 'CPPHTTPLIB_VERSION'

The actual problem is that httplib.h #errors out.

Have the Meson logic explicitly check for a 32-bit host and warn or error,
matching the check in httplib.h.  Phrase the Windows error in a way that
triggers WrapDB CI's unsupported architecture check.
2025-07-16 12:47:51 -04:00
yhirose 1f110b54d8 Chang #error to #warning for the 32-bit environment check except 32-bit Windows 2025-07-11 22:44:29 -04:00
yhirose 7b6867bcdf Fix #2021 (#2180) 2025-07-10 22:01:41 -04:00
yhirose 8b28577ec6 Resolve #366 2025-07-10 01:07:44 -04:00
yhirose 55b38907dc Resolve #1264 2025-07-10 00:58:52 -04:00
yhirose 53ea9e8bb4 Fix #2111 (#2179) 2025-07-10 00:47:45 -04:00
31 changed files with 8194 additions and 980 deletions
+1
View File
@@ -29,6 +29,7 @@ jobs:
git
libbrotli-dev
libssl-dev
libzstd-dev
meson
pkg-config
python3
+51
View File
@@ -0,0 +1,51 @@
name: Release Docker Image
on:
release:
types: [published]
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history and tags
- name: Extract tag (manual)
if: github.event_name == 'workflow_dispatch'
id: set_tag_manual
run: |
# Checkout the latest tag and set output
git fetch --tags
LATEST_TAG=$(git describe --tags --abbrev=0)
git checkout $LATEST_TAG
echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT
- name: Extract tag (release)
if: github.event_name == 'release'
id: set_tag_release
run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64 # Build for both amd64 and arm64
# Use extracted tag without leading 'v'
tags: |
yhirose4dockerhub/cpp-httplib-server:latest
yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag_manual.outputs.tag || steps.set_tag_release.outputs.tag }}
+19 -1
View File
@@ -1,16 +1,34 @@
tags
# Ignore executables (no extension) but not source files
example/server
!example/server.*
example/client
!example/client.*
example/hello
!example/hello.*
example/simplecli
!example/simplecli.*
example/simplesvr
!example/simplesvr.*
example/benchmark
!example/benchmark.*
example/redirect
example/sse*
!example/redirect.*
example/ssecli
!example/ssecli.*
example/ssecli-stream
!example/ssecli-stream.*
example/ssesvr
!example/ssesvr.*
example/upload
!example/upload.*
example/one_time_request
!example/one_time_request.*
example/server_and_client
!example/server_and_client.*
example/accept_header
!example/accept_header.*
example/*.pem
test/httplib.cc
test/httplib.h
+41 -15
View File
@@ -1,6 +1,6 @@
#[[
Build options:
* BUILD_SHARED_LIBS (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
* Standard BUILD_SHARED_LIBS is supported and sets HTTPLIB_SHARED default value.
* HTTPLIB_USE_OPENSSL_IF_AVAILABLE (default on)
* HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on)
* HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on)
@@ -13,6 +13,7 @@
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
* HTTPLIB_COMPILE (default off)
* HTTPLIB_INSTALL (default on)
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
* HTTPLIB_TEST (default off)
* BROTLI_USE_STATIC_LIBS - tells Cmake to use the static Brotli libs (only works if you have them installed).
* OPENSSL_USE_STATIC_LIBS - tells Cmake to use the static OpenSSL libs (only works if you have them installed).
@@ -109,12 +110,34 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer
option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON)
option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF)
option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON)
# Defaults to static library
option(BUILD_SHARED_LIBS "Build the library as a shared library instead of static. Has no effect if using header-only." OFF)
if (BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
# Necessary for Windows if building shared libs
# See https://stackoverflow.com/a/40743080
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
# Defaults to static library but respects standard BUILD_SHARED_LIBS if set
include(CMakeDependentOption)
cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only."
"${BUILD_SHARED_LIBS}" HTTPLIB_COMPILE OFF
)
if(HTTPLIB_SHARED)
set(HTTPLIB_LIB_TYPE SHARED)
if(WIN32)
# Necessary for Windows if building shared libs
# See https://stackoverflow.com/a/40743080
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
else()
set(HTTPLIB_LIB_TYPE STATIC)
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
if(CMAKE_SYSTEM_VERSION)
if(${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
endif()
else()
set(CMAKE_SYSTEM_VERSION "10.0.19041.0")
message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.")
endif()
endif()
if(CMAKE_SIZEOF_VOID_P LESS 8)
message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.")
endif()
# Set some variables that are used in-tree and while building based on our options
@@ -162,6 +185,8 @@ elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND})
endif()
# NOTE: When using cpp-httplib as a subproject (e.g., via FetchContent), the zstd::libzstd target may not be visible in the parent project scope.
# If you encounter a "target not found" error, see https://github.com/yhirose/cpp-httplib/issues/2313 for a workaround.
if(HTTPLIB_REQUIRE_ZSTD)
find_package(zstd)
if(NOT zstd_FOUND)
@@ -216,8 +241,7 @@ if(HTTPLIB_COMPILE)
# split.py puts output in "out"
set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out")
# This will automatically be either static or shared based on the value of BUILD_SHARED_LIBS
add_library(${PROJECT_NAME} "${_httplib_build_includedir}/httplib.cc")
add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc")
target_sources(${PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
@@ -243,18 +267,20 @@ add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC}
$<BUILD_INTERFACE:${_httplib_build_includedir}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
$<BUILD_INTERFACE:${_httplib_build_includedir}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
# Always require threads
target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
# Always require threads
Threads::Threads
# Needed for Windows libs on Mingw, as the pragma comment(lib, "xyz") aren't triggered.
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Windows>:crypt32>
# Needed for API from MacOS Security framework
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CoreFoundation -framework Security>"
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
# Needed for non-blocking getaddrinfo on MacOS
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
# Can't put multiple targets in a single generator expression or it bugs out.
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
@@ -340,6 +366,6 @@ if(HTTPLIB_INSTALL)
endif()
if(HTTPLIB_TEST)
include(CTest)
add_subdirectory(test)
include(CTest)
add_subdirectory(test)
endif()
+3 -1
View File
@@ -8,4 +8,6 @@ FROM scratch
COPY --from=builder /build/server /server
COPY docker/html/index.html /html/index.html
EXPOSE 80
CMD ["/server"]
ENTRYPOINT ["/server"]
CMD ["--host", "0.0.0.0", "--port", "80", "--mount", "/:./html"]
+182
View File
@@ -0,0 +1,182 @@
# SSEClient - Server-Sent Events Client
A simple, EventSource-like SSE client for C++11.
## Features
- **Auto-reconnect**: Automatically reconnects on connection loss
- **Last-Event-ID**: Sends last received ID on reconnect for resumption
- **retry field**: Respects server's reconnect interval
- **Event types**: Supports custom event types via `on_event()`
- **Async support**: Run in background thread with `start_async()`
- **C++11 compatible**: No C++14/17/20 features required
## Quick Start
```cpp
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << "Event: " << msg.event << std::endl;
std::cout << "Data: " << msg.data << std::endl;
});
sse.start(); // Blocking, with auto-reconnect
```
## API Reference
### SSEMessage
```cpp
struct SSEMessage {
std::string event; // Event type (default: "message")
std::string data; // Event payload
std::string id; // Event ID
};
```
### SSEClient
#### Constructor
```cpp
// Basic
SSEClient(Client &client, const std::string &path);
// With custom headers
SSEClient(Client &client, const std::string &path, const Headers &headers);
```
#### Event Handlers
```cpp
// Called for all events (or events without a specific handler)
sse.on_message([](const SSEMessage &msg) { });
// Called for specific event types
sse.on_event("update", [](const SSEMessage &msg) { });
sse.on_event("delete", [](const SSEMessage &msg) { });
// Called when connection is established
sse.on_open([]() { });
// Called on connection errors
sse.on_error([](httplib::Error err) { });
```
#### Configuration
```cpp
// Set reconnect interval (default: 3000ms)
sse.set_reconnect_interval(5000);
// Set max reconnect attempts (default: 0 = unlimited)
sse.set_max_reconnect_attempts(10);
```
#### Control
```cpp
// Blocking start with auto-reconnect
sse.start();
// Non-blocking start (runs in background thread)
sse.start_async();
// Stop the client (thread-safe)
sse.stop();
```
#### State
```cpp
bool connected = sse.is_connected();
const std::string &id = sse.last_event_id();
```
## Examples
### Basic Usage
```cpp
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << msg.data << std::endl;
});
sse.start();
```
### With Custom Event Types
```cpp
httplib::sse::SSEClient sse(cli, "/events");
sse.on_event("notification", [](const httplib::sse::SSEMessage &msg) {
std::cout << "Notification: " << msg.data << std::endl;
});
sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
std::cout << "Update: " << msg.data << std::endl;
});
sse.start();
```
### Async with Stop
```cpp
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << msg.data << std::endl;
});
sse.start_async(); // Returns immediately
// ... do other work ...
sse.stop(); // Stop when done
```
### With Custom Headers (e.g., Authentication)
```cpp
httplib::Headers headers = {
{"Authorization", "Bearer token123"}
};
httplib::sse::SSEClient sse(cli, "/events", headers);
sse.start();
```
### Error Handling
```cpp
sse.on_error([](httplib::Error err) {
std::cerr << "Error: " << httplib::to_string(err) << std::endl;
});
sse.set_reconnect_interval(1000);
sse.set_max_reconnect_attempts(5);
sse.start();
```
## SSE Protocol
The client parses SSE format according to the [W3C specification](https://html.spec.whatwg.org/multipage/server-sent-events.html):
```
event: custom-type
id: 123
data: {"message": "hello"}
data: simple message
: this is a comment (ignored)
```
+317
View File
@@ -0,0 +1,317 @@
# cpp-httplib Streaming API
This document describes the streaming extensions for cpp-httplib, providing an iterator-style API for handling HTTP responses incrementally with **true socket-level streaming**.
> **Important Notes**:
>
> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed after the response is fully read. For connection reuse, use `Client::Get()`.
> - **Single iteration only**: The `next()` method can only iterate through the body once.
> - **Result is not thread-safe**: While `stream::Get()` can be called from multiple threads simultaneously, the returned `stream::Result` must be used from a single thread only.
## Overview
The streaming API allows you to process HTTP response bodies chunk by chunk using an iterator-style pattern. Data is read directly from the network socket, enabling low-memory processing of large responses. This is particularly useful for:
- **LLM/AI streaming responses** (e.g., ChatGPT, Claude, Ollama)
- **Server-Sent Events (SSE)**
- **Large file downloads** with progress tracking
- **Reverse proxy implementations**
## Quick Start
```cpp
#include "httplib.h"
int main() {
httplib::Client cli("http://localhost:8080");
// Get streaming response
auto result = httplib::stream::Get(cli, "/stream");
if (result) {
// Process response body in chunks
while (result.next()) {
std::cout.write(result.data(), result.size());
}
}
return 0;
}
```
## API Layers
cpp-httplib provides multiple API layers for different use cases:
```text
┌─────────────────────────────────────────────┐
│ SSEClient (planned) │ ← SSE-specific, parsed events
│ - on_message(), on_event() │
│ - Auto-reconnect, Last-Event-ID │
├─────────────────────────────────────────────┤
│ stream::Get() / stream::Result │ ← Iterator-based streaming
│ - while (result.next()) { ... } │
├─────────────────────────────────────────────┤
│ open_stream() / StreamHandle │ ← General-purpose streaming
│ - handle.read(buf, len) │
├─────────────────────────────────────────────┤
│ Client::Get() │ ← Traditional, full buffering
└─────────────────────────────────────────────┘
```
| Use Case | Recommended API |
|----------|----------------|
| SSE with auto-reconnect | SSEClient (planned) or `ssecli-stream.cc` example |
| LLM streaming (JSON Lines) | `stream::Get()` |
| Large file download | `stream::Get()` or `open_stream()` |
| Reverse proxy | `open_stream()` |
| Small responses with Keep-Alive | `Client::Get()` |
## API Reference
### Low-Level API: `StreamHandle`
The `StreamHandle` struct provides direct control over streaming responses. It takes ownership of the socket connection and reads data directly from the network.
> **Note:** When using `open_stream()`, the connection is dedicated to streaming and **Keep-Alive is not supported**. For Keep-Alive connections, use `client.Get()` instead.
```cpp
// Open a stream (takes ownership of socket)
httplib::Client cli("http://localhost:8080");
auto handle = cli.open_stream("GET", "/path");
// Check validity
if (handle.is_valid()) {
// Access response headers immediately
int status = handle.response->status;
auto content_type = handle.response->get_header_value("Content-Type");
// Read body incrementally
char buf[4096];
ssize_t n;
while ((n = handle.read(buf, sizeof(buf))) > 0) {
process(buf, n);
}
}
```
#### StreamHandle Members
| Member | Type | Description |
|--------|------|-------------|
| `response` | `std::unique_ptr<Response>` | HTTP response with headers |
| `error` | `Error` | Error code if request failed |
| `is_valid()` | `bool` | Returns true if response is valid |
| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket |
| `get_read_error()` | `Error` | Get the last read error |
| `has_read_error()` | `bool` | Check if a read error occurred |
### High-Level API: `stream::Get()` and `stream::Result`
The `httplib.h` header provides a more ergonomic iterator-style API.
```cpp
#include "httplib.h"
httplib::Client cli("http://localhost:8080");
cli.set_follow_location(true);
...
// Simple GET
auto result = httplib::stream::Get(cli, "/path");
// GET with custom headers
httplib::Headers headers = {{"Authorization", "Bearer token"}};
auto result = httplib::stream::Get(cli, "/path", headers);
// Process the response
if (result) {
while (result.next()) {
process(result.data(), result.size());
}
}
// Or read entire body at once
auto result2 = httplib::stream::Get(cli, "/path");
if (result2) {
std::string body = result2.read_all();
}
```
#### stream::Result Members
| Member | Type | Description |
|--------|------|-------------|
| `operator bool()` | `bool` | Returns true if response is valid |
| `is_valid()` | `bool` | Same as `operator bool()` |
| `status()` | `int` | HTTP status code |
| `headers()` | `const Headers&` | Response headers |
| `get_header_value(key, def)` | `std::string` | Get header value (with optional default) |
| `has_header(key)` | `bool` | Check if header exists |
| `next()` | `bool` | Read next chunk, returns false when done |
| `data()` | `const char*` | Pointer to current chunk data |
| `size()` | `size_t` | Size of current chunk |
| `read_all()` | `std::string` | Read entire remaining body into string |
| `error()` | `Error` | Get the connection/request error |
| `read_error()` | `Error` | Get the last read error |
| `has_read_error()` | `bool` | Check if a read error occurred |
## Usage Examples
### Example 1: SSE (Server-Sent Events) Client
```cpp
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:1234");
auto result = httplib::stream::Get(cli, "/events");
if (!result) { return 1; }
while (result.next()) {
std::cout.write(result.data(), result.size());
std::cout.flush();
}
return 0;
}
```
For a complete SSE client with auto-reconnection and event parsing, see `example/ssecli-stream.cc`.
### Example 2: LLM Streaming Response
```cpp
#include "httplib.h"
#include <iostream>
int main() {
httplib::Client cli("http://localhost:11434"); // Ollama
auto result = httplib::stream::Get(cli, "/api/generate");
if (result && result.status() == 200) {
while (result.next()) {
std::cout.write(result.data(), result.size());
std::cout.flush();
}
}
// Check for connection errors
if (result.read_error() != httplib::Error::Success) {
std::cerr << "Connection lost\n";
}
return 0;
}
```
### Example 3: Large File Download with Progress
```cpp
#include "httplib.h"
#include <fstream>
#include <iostream>
int main() {
httplib::Client cli("http://example.com");
auto result = httplib::stream::Get(cli, "/large-file.zip");
if (!result || result.status() != 200) {
std::cerr << "Download failed\n";
return 1;
}
std::ofstream file("download.zip", std::ios::binary);
size_t total = 0;
while (result.next()) {
file.write(result.data(), result.size());
total += result.size();
std::cout << "\rDownloaded: " << (total / 1024) << " KB" << std::flush;
}
std::cout << "\nComplete!\n";
return 0;
}
```
### Example 4: Reverse Proxy Streaming
```cpp
#include "httplib.h"
httplib::Server svr;
svr.Get("/proxy/(.*)", [](const httplib::Request& req, httplib::Response& res) {
httplib::Client upstream("http://backend:8080");
auto handle = upstream.open_stream("/" + req.matches[1].str());
if (!handle.is_valid()) {
res.status = 502;
return;
}
res.status = handle.response->status;
res.set_chunked_content_provider(
handle.response->get_header_value("Content-Type"),
[handle = std::move(handle)](size_t, httplib::DataSink& sink) mutable {
char buf[8192];
auto n = handle.read(buf, sizeof(buf));
if (n > 0) {
sink.write(buf, static_cast<size_t>(n));
return true;
}
sink.done();
return true;
}
);
});
svr.listen("0.0.0.0", 3000);
```
## Comparison with Existing APIs
| Feature | `Client::Get()` | `open_stream()` | `stream::Get()` |
|---------|----------------|-----------------|----------------|
| Headers available | After complete | Immediately | Immediately |
| Body reading | All at once | Direct from socket | Iterator-based |
| Memory usage | Full body in RAM | Minimal (controlled) | Minimal (controlled) |
| Keep-Alive support | ✅ Yes | ❌ No | ❌ No |
| Compression | Auto-handled | Auto-handled | Auto-handled |
| Best for | Small responses, Keep-Alive | Low-level streaming | Easy streaming |
## Features
- **True socket-level streaming**: Data is read directly from the network socket
- **Low memory footprint**: Only the current chunk is held in memory
- **Compression support**: Automatic decompression for gzip, brotli, and zstd
- **Chunked transfer**: Full support for chunked transfer encoding
- **SSL/TLS support**: Works with HTTPS connections
## Important Notes
### Keep-Alive Behavior
The streaming API (`stream::Get()` / `open_stream()`) takes ownership of the socket connection for the duration of the stream. This means:
- **Keep-Alive is not supported** for streaming connections
- The socket is closed when `StreamHandle` is destroyed
- For Keep-Alive scenarios, use the standard `client.Get()` API instead
```cpp
// Use for streaming (no Keep-Alive)
auto result = httplib::stream::Get(cli, "/large-stream");
while (result.next()) { /* ... */ }
// Use for Keep-Alive connections
auto res = cli.Get("/api/data"); // Connection can be reused
```
## Related
- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request
- [example/ssecli-stream.cc](./example/ssecli-stream.cc) - SSE client with auto-reconnection
+200 -19
View File
@@ -44,9 +44,10 @@ httplib::Client cli("http://yhirose.github.io");
// HTTPS
httplib::Client cli("https://yhirose.github.io");
auto res = cli.Get("/hi");
res->status;
res->body;
if (auto res = cli.Get("/hi")) {
res->status;
res->body;
}
```
SSL Support
@@ -98,32 +99,31 @@ httplib::Client cli("https://example.com");
auto res = cli.Get("/");
if (!res) {
// Check the error type
auto err = res.error();
const auto err = res.error();
switch (err) {
case httplib::Error::SSLConnection:
std::cout << "SSL connection failed, SSL error: "
<< res->ssl_error() << std::endl;
<< res.ssl_error() << std::endl;
break;
case httplib::Error::SSLLoadingCerts:
std::cout << "SSL cert loading failed, OpenSSL error: "
<< std::hex << res->ssl_openssl_error() << std::endl;
<< std::hex << res.ssl_openssl_error() << std::endl;
break;
case httplib::Error::SSLServerVerification:
std::cout << "SSL verification failed, X509 error: "
<< res->ssl_openssl_error() << std::endl;
<< res.ssl_openssl_error() << std::endl;
break;
case httplib::Error::SSLServerHostnameVerification:
std::cout << "SSL hostname verification failed, X509 error: "
<< res->ssl_openssl_error() << std::endl;
<< res.ssl_openssl_error() << std::endl;
break;
default:
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
}
}
}
```
@@ -191,7 +191,7 @@ int main(void)
}
```
`Post`, `Put`, `Delete` and `Options` methods are also supported.
`Post`, `Put`, `Patch`, `Delete` and `Options` methods are also supported.
### Bind a socket to multiple interfaces and any available port
@@ -269,23 +269,47 @@ svr.set_file_request_handler([](const Request &req, Response &res) {
### Logging
cpp-httplib provides separate logging capabilities for access logs and error logs, similar to web servers like Nginx and Apache.
#### Access Logging
Access loggers capture successful HTTP requests and responses:
```cpp
svr.set_logger([](const auto& req, const auto& res) {
your_logger(req, res);
svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
std::cout << req.method << " " << req.path << " -> " << res.status << std::endl;
});
```
You can also set a pre-compression logger to capture request/response data before compression is applied. This is useful for debugging and monitoring purposes when you need to see the original, uncompressed response content:
#### Pre-compression Logging
You can also set a pre-compression logger to capture request/response data before compression is applied:
```cpp
svr.set_pre_compression_logger([](const auto& req, const auto& res) {
svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Response& res) {
// Log before compression - res.body contains uncompressed content
// Content-Encoding header is not yet set
your_pre_compression_logger(req, res);
});
```
The pre-compression logger is only called when compression would be applied. For responses without compression, only the regular logger is called.
The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called.
#### Error Logging
Error loggers capture failed requests and connection issues. Unlike access loggers, error loggers only receive the Error and Request information, as errors typically occur before a meaningful Response can be generated.
```cpp
svr.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
std::cerr << httplib::to_string(err) << " while processing request";
if (req) {
std::cerr << ", client: " << req->get_header_value("X-Forwarded-For")
<< ", request: '" << req->method << " " << req->path << " " << req->version << "'"
<< ", host: " << req->get_header_value("Host");
}
std::cerr << std::endl;
});
```
### Error handler
@@ -698,7 +722,7 @@ httplib::SSLClient cli("localhost");
Here is the list of errors from `Result::error()`.
```c++
enum Error {
enum class Error {
Success = 0,
Unknown,
Connection,
@@ -715,9 +739,72 @@ enum Error {
Compression,
ConnectionTimeout,
ProxyConnection,
ConnectionClosed,
Timeout,
ResourceExhaustion,
TooManyFormDataFiles,
ExceedMaxPayloadSize,
ExceedUriMaxLength,
ExceedMaxSocketDescriptorCount,
InvalidRequestLine,
InvalidHTTPMethod,
InvalidHTTPVersion,
InvalidHeaders,
MultipartParsing,
OpenFile,
Listen,
GetSockName,
UnsupportedAddressFamily,
HTTPParsing,
InvalidRangeHeader,
};
```
### Client Logging
#### Access Logging
```cpp
cli.set_logger([](const httplib::Request& req, const httplib::Response& res) {
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time).count();
std::cout << "✓ " << req.method << " " << req.path
<< " -> " << res.status << " (" << res.body.size() << " bytes, "
<< duration << "ms)" << std::endl;
});
```
#### Error Logging
```cpp
cli.set_error_logger([](const httplib::Error& err, const httplib::Request* req) {
std::cerr << "✗ ";
if (req) {
std::cerr << req->method << " " << req->path << " ";
}
std::cerr << "failed: " << httplib::to_string(err);
// Add specific guidance based on error type
switch (err) {
case httplib::Error::Connection:
std::cerr << " (verify server is running and reachable)";
break;
case httplib::Error::SSLConnection:
std::cerr << " (check SSL certificate and TLS configuration)";
break;
case httplib::Error::ConnectionTimeout:
std::cerr << " (increase timeout or check network latency)";
break;
case httplib::Error::Read:
std::cerr << " (server may have closed connection prematurely)";
break;
default:
break;
}
std::cerr << std::endl;
});
```
### GET with HTTP headers
```c++
@@ -785,6 +872,12 @@ auto res = cli.Post("/multipart", items);
res = cli.Put("/resource/foo", "text", "text/plain");
```
### PATCH
```c++
res = cli.Patch("/resource/foo", "text", "text/plain");
```
### DELETE
```c++
@@ -871,7 +964,7 @@ auto res = cli.Post(
httplib::Client cli(url, port);
// prints: 0 / 000 bytes => 50% complete
auto res = cli.Get("/", [](uint64_t len, uint64_t total) {
auto res = cli.Get("/", [](size_t len, size_t total) {
printf("%lld / %lld bytes => %d%% complete\n",
len, total,
(int)(len*100/total));
@@ -919,7 +1012,7 @@ cli.set_proxy_bearer_token_auth("pass");
### Range
```cpp
httplib::Client cli("httpbin.org");
httplib::Client cli("httpcan.org");
auto res = cli.Get("/range/32", {
httplib::make_range_header({{1, 10}}) // 'Range: bytes=1-10'
@@ -990,6 +1083,25 @@ auto res = cli.Get("/already%20encoded/path"); // Use pre-encoded paths
- `true` (default): Automatically encodes spaces, plus signs, newlines, and other special characters
- `false`: Sends paths as-is without encoding (useful for pre-encoded URLs)
### Performance Note for Local Connections
> [!WARNING]
> On Windows systems with improperly configured IPv6 settings, using "localhost" as the hostname may cause significant connection delays (up to 2 seconds per request) due to DNS resolution issues. This affects both client and server operations. For better performance when connecting to local services, use "127.0.0.1" instead of "localhost".
>
> See: https://github.com/yhirose/cpp-httplib/issues/366#issuecomment-593004264
```cpp
// May be slower on Windows due to DNS resolution delays
httplib::Client cli("localhost", 8080);
httplib::Server svr;
svr.listen("localhost", 8080);
// Faster alternative for local connections
httplib::Client cli("127.0.0.1", 8080);
httplib::Server svr;
svr.listen("127.0.0.1", 8080);
```
Compression
-----------
@@ -1065,6 +1177,8 @@ cli.set_address_family(AF_UNIX);
"my-socket.sock" can be a relative path or an absolute path. Your application must have the appropriate permissions for the path. You can also use an abstract socket address on Linux. To use an abstract socket address, prepend a null byte ('\x00') to the path.
This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior:
URI Encoding/Decoding Utilities
-------------------------------
@@ -1092,6 +1206,55 @@ std::string decoded_component = httplib::decode_uri_component(encoded_component)
Use `encode_uri()` for full URLs and `encode_uri_component()` for individual query parameters or path segments.
Stream API
----------
Process large responses without loading everything into memory.
```c++
httplib::Client cli("localhost", 8080);
cli.set_follow_location(true);
...
auto result = httplib::stream::Get(cli, "/large-file");
if (result) {
while (result.next()) {
process(result.data(), result.size()); // Process each chunk as it arrives
}
}
// Or read the entire body at once
auto result2 = httplib::stream::Get(cli, "/file");
if (result2) {
std::string body = result2.read_all();
}
```
All HTTP methods are supported: `stream::Get`, `Post`, `Put`, `Patch`, `Delete`, `Head`, `Options`.
See [README-stream.md](README-stream.md) for more details.
SSE Client
----------
```cpp
#include <httplib.h>
int main() {
httplib::Client cli("http://localhost:8080");
httplib::sse::SSEClient sse(cli, "/events");
sse.on_message([](const httplib::sse::SSEMessage &msg) {
std::cout << "Event: " << msg.event << std::endl;
std::cout << "Data: " << msg.data << std::endl;
});
sse.start(); // Blocking, with auto-reconnect
return 0;
}
```
See [README-sse.md](README-sse.md) for more details.
Split httplib.h into .h and .cc
-------------------------------
@@ -1141,6 +1304,24 @@ Serving HTTP on 0.0.0.0 port 80 ...
NOTE
----
### Regular Expression Stack Overflow
> [!CAUTION]
> When using complex regex patterns in route handlers, be aware that certain patterns may cause stack overflow during pattern matching. This is a known issue with `std::regex` implementations and affects the `dispatch_request()` method.
>
> ```cpp
> // This pattern can cause stack overflow with large input
> svr.Get(".*", handler);
> ```
>
> Consider using simpler patterns or path parameters to avoid this issue:
>
> ```cpp
> // Safer alternatives
> svr.Get("/users/:id", handler); // Path parameters
> svr.Get(R"(/api/v\d+/.*)", handler); // More specific patterns
> ```
### g++
g++ 4.8 and below cannot build this library since `<regex>` in the versions are [broken](https://stackoverflow.com/questions/12530406/is-gcc-4-8-or-earlier-buggy-about-regular-expressions).
@@ -1169,7 +1350,7 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
License
-------
MIT license (© 2025 Yuji Hirose)
MIT license (© 2026 Yuji Hirose)
Special Thanks To
-----------------
+7 -2
View File
@@ -86,7 +86,9 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
# Check if the target was already found by Pkgconf
if(TARGET PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
# ALIAS since we don't want the PkgConfig namespace on the Cmake library (for end-users)
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
if (NOT TARGET Brotli::${_component_name})
add_library(Brotli::${_component_name} ALIAS PkgConfig::Brotli_${_component_name}${_brotli_stat_str})
endif()
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND TRUE)
@@ -139,7 +141,10 @@ foreach(_listvar "common;common" "decoder;dec" "encoder;enc")
# Tells HANDLE_COMPONENTS we found the component
set(Brotli_${_component_name}_FOUND TRUE)
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
if (NOT TARGET Brotli::${_component_name})
add_library("Brotli::${_component_name}" UNKNOWN IMPORTED)
endif()
# Attach the literal library and include dir to the IMPORTED target for the end-user
set_target_properties("Brotli::${_component_name}" PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${Brotli_INCLUDE_DIR}"
+265 -43
View File
@@ -1,81 +1,303 @@
//
// main.cc
//
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
#include <atomic>
#include <chrono>
#include <ctime>
#include <format>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <sstream>
#include <httplib.h>
constexpr auto error_html = R"(<html>
<head><title>{} {}</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>cpp-httplib/{}</center>
</body>
</html>
)";
using namespace httplib;
void sigint_handler(int s) { exit(1); }
const auto SERVER_NAME =
std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION);
std::string time_local() {
auto p = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(p);
Server svr;
void signal_handler(int signal) {
if (signal == SIGINT || signal == SIGTERM) {
std::cout << "\nReceived signal, shutting down gracefully...\n";
svr.stop();
}
}
std::string get_time_format() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&t), "%d/%b/%Y:%H:%M:%S %z");
ss << std::put_time(std::localtime(&time_t), "%d/%b/%Y:%H:%M:%S %z");
return ss.str();
}
std::string log(auto &req, auto &res) {
auto remote_user = "-"; // TODO:
auto request = std::format("{} {} {}", req.method, req.path, req.version);
auto body_bytes_sent = res.get_header_value("Content-Length");
auto http_referer = "-"; // TODO:
auto http_user_agent = req.get_header_value("User-Agent", "-");
std::string get_error_time_format() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
// NOTE: From NGINX default access log format
// log_format combined '$remote_addr - $remote_user [$time_local] '
// '"$request" $status $body_bytes_sent '
// '"$http_referer" "$http_user_agent"';
return std::format(R"({} - {} [{}] "{}" {} {} "{}" "{}")", req.remote_addr,
remote_user, time_local(), request, res.status,
body_bytes_sent, http_referer, http_user_agent);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
return ss.str();
}
int main(int argc, const char **argv) {
signal(SIGINT, sigint_handler);
// NGINX Combined log format:
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
// "$http_referer" "$http_user_agent"
void nginx_access_logger(const Request &req, const Response &res) {
std::string remote_user =
"-"; // cpp-httplib doesn't have built-in auth user tracking
auto time_local = get_time_format();
auto request = std::format("{} {} {}", req.method, req.path, req.version);
auto status = res.status;
auto body_bytes_sent = res.body.size();
auto http_referer = req.get_header_value("Referer");
if (http_referer.empty()) http_referer = "-";
auto http_user_agent = req.get_header_value("User-Agent");
if (http_user_agent.empty()) http_user_agent = "-";
auto base_dir = "./html";
auto host = "0.0.0.0";
auto port = 80;
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
req.remote_addr, remote_user, time_local, request,
status, body_bytes_sent, http_referer,
http_user_agent)
<< std::endl;
}
httplib::Server svr;
// NGINX Error log format:
// YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request",
// host: "host"
void nginx_error_logger(const Error &err, const Request *req) {
auto time_local = get_error_time_format();
std::string level = "error";
svr.set_error_handler([](auto & /*req*/, auto &res) {
auto body =
std::format(error_html, res.status, httplib::status_message(res.status),
CPPHTTPLIB_VERSION);
if (req) {
auto request =
std::format("{} {} {}", req->method, req->path, req->version);
auto host = req->get_header_value("Host");
if (host.empty()) host = "-";
res.set_content(body, "text/html");
std::cerr << std::format("{} [{}] {}, client: {}, request: "
"\"{}\", host: \"{}\"",
time_local, level, to_string(err),
req->remote_addr, request, host)
<< std::endl;
} else {
// If no request context, just log the error
std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err))
<< std::endl;
}
}
void print_usage(const char *program_name) {
std::cout << "Usage: " << program_name << " [OPTIONS]" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --host <hostname> Server hostname (default: localhost)"
<< std::endl;
std::cout << " --port <port> Server port (default: 8080)"
<< std::endl;
std::cout << " --mount <mount:path> Mount point and document root"
<< std::endl;
std::cout << " Format: mount_point:document_root"
<< std::endl;
std::cout << " (default: /:./html)" << std::endl;
std::cout << " --trusted-proxy <ip> Add trusted proxy IP address"
<< std::endl;
std::cout << " (can be specified multiple times)"
<< std::endl;
std::cout << " --version Show version information"
<< std::endl;
std::cout << " --help Show this help message" << std::endl;
std::cout << std::endl;
std::cout << "Examples:" << std::endl;
std::cout << " " << program_name
<< " --host localhost --port 8080 --mount /:./html" << std::endl;
std::cout << " " << program_name
<< " --host 0.0.0.0 --port 3000 --mount /api:./api" << std::endl;
std::cout << " " << program_name
<< " --trusted-proxy 192.168.1.100 --trusted-proxy 10.0.0.1"
<< std::endl;
}
struct ServerConfig {
std::string hostname = "localhost";
int port = 8080;
std::string mount_point = "/";
std::string document_root = "./html";
std::vector<std::string> trusted_proxies;
};
enum class ParseResult { SUCCESS, HELP_REQUESTED, VERSION_REQUESTED, ERROR };
ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
print_usage(argv[0]);
return ParseResult::HELP_REQUESTED;
} else if (strcmp(argv[i], "--host") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --host requires a hostname argument" << std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.hostname = argv[++i];
} else if (strcmp(argv[i], "--port") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --port requires a port number argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.port = std::atoi(argv[++i]);
if (config.port <= 0 || config.port > 65535) {
std::cerr << "Error: Invalid port number. Must be between 1 and 65535"
<< std::endl;
return ParseResult::ERROR;
}
} else if (strcmp(argv[i], "--mount") == 0) {
if (i + 1 >= argc) {
std::cerr
<< "Error: --mount requires mount_point:document_root argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
std::string mount_arg = argv[++i];
auto colon_pos = mount_arg.find(':');
if (colon_pos == std::string::npos) {
std::cerr << "Error: --mount argument must be in format "
"mount_point:document_root"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.mount_point = mount_arg.substr(0, colon_pos);
config.document_root = mount_arg.substr(colon_pos + 1);
if (config.mount_point.empty() || config.document_root.empty()) {
std::cerr
<< "Error: Both mount_point and document_root must be non-empty"
<< std::endl;
return ParseResult::ERROR;
}
} else if (strcmp(argv[i], "--version") == 0) {
std::cout << CPPHTTPLIB_VERSION << std::endl;
return ParseResult::VERSION_REQUESTED;
} else if (strcmp(argv[i], "--trusted-proxy") == 0) {
if (i + 1 >= argc) {
std::cerr << "Error: --trusted-proxy requires an IP address argument"
<< std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
config.trusted_proxies.push_back(argv[++i]);
} else {
std::cerr << "Error: Unknown option '" << argv[i] << "'" << std::endl;
print_usage(argv[0]);
return ParseResult::ERROR;
}
}
return ParseResult::SUCCESS;
}
bool setup_server(Server &svr, const ServerConfig &config) {
svr.set_logger(nginx_access_logger);
svr.set_error_logger(nginx_error_logger);
// Set trusted proxies if specified
if (!config.trusted_proxies.empty()) {
svr.set_trusted_proxies(config.trusted_proxies);
}
auto ret = svr.set_mount_point(config.mount_point, config.document_root);
if (!ret) {
std::cerr
<< std::format(
"Error: Cannot mount '{}' to '{}'. Directory may not exist.",
config.mount_point, config.document_root)
<< std::endl;
return false;
}
svr.set_file_extension_and_mimetype_mapping("html", "text/html");
svr.set_file_extension_and_mimetype_mapping("htm", "text/html");
svr.set_file_extension_and_mimetype_mapping("css", "text/css");
svr.set_file_extension_and_mimetype_mapping("js", "text/javascript");
svr.set_file_extension_and_mimetype_mapping("json", "application/json");
svr.set_file_extension_and_mimetype_mapping("xml", "application/xml");
svr.set_file_extension_and_mimetype_mapping("png", "image/png");
svr.set_file_extension_and_mimetype_mapping("jpg", "image/jpeg");
svr.set_file_extension_and_mimetype_mapping("jpeg", "image/jpeg");
svr.set_file_extension_and_mimetype_mapping("gif", "image/gif");
svr.set_file_extension_and_mimetype_mapping("svg", "image/svg+xml");
svr.set_file_extension_and_mimetype_mapping("ico", "image/x-icon");
svr.set_file_extension_and_mimetype_mapping("pdf", "application/pdf");
svr.set_file_extension_and_mimetype_mapping("zip", "application/zip");
svr.set_file_extension_and_mimetype_mapping("txt", "text/plain");
svr.set_error_handler([](const Request & /*req*/, Response &res) {
if (res.status == 404) {
res.set_content(
std::format(
"<html><head><title>404 Not Found</title></head>"
"<body><h1>404 Not Found</h1>"
"<p>The requested resource was not found on this server.</p>"
"<hr><p>{}</p></body></html>",
SERVER_NAME),
"text/html");
}
});
svr.set_logger(
[](auto &req, auto &res) { std::cout << log(req, res) << std::endl; });
svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
res.set_header("Server", SERVER_NAME);
return Server::HandlerResponse::Unhandled;
});
svr.set_mount_point("/", base_dir);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
std::cout << std::format("Serving HTTP on {0} port {1} ...", host, port)
return true;
}
int main(int argc, char *argv[]) {
ServerConfig config;
auto result = parse_command_line(argc, argv, config);
switch (result) {
case ParseResult::HELP_REQUESTED:
case ParseResult::VERSION_REQUESTED: return 0;
case ParseResult::ERROR: return 1;
case ParseResult::SUCCESS: break;
}
if (!setup_server(svr, config)) { return 1; }
std::cout << "Serving HTTP on " << config.hostname << ":" << config.port
<< std::endl;
std::cout << "Mount point: " << config.mount_point << " -> "
<< config.document_root << std::endl;
auto ret = svr.listen(host, port);
if (!config.trusted_proxies.empty()) {
std::cout << "Trusted proxies: ";
for (size_t i = 0; i < config.trusted_proxies.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << config.trusted_proxies[i];
}
std::cout << std::endl;
}
std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl;
auto ret = svr.listen(config.hostname, config.port);
std::cout << "Server has been shut down." << std::endl;
return ret ? 0 : 1;
}
+1 -1
View File
@@ -21,7 +21,7 @@ struct StopWatch {
int main(void) {
string body(1024 * 5, 'a');
httplib::Client cli("httpbin.org", 80);
httplib::Client cli("httpcan.org", 80);
for (int i = 0; i < 3; i++) {
StopWatch sw(to_string(i).c_str());
+1 -1
View File
@@ -1,7 +1,7 @@
//
// client.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// hello.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// redirect.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// sample.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// server_and_client.cc
//
// Copyright (c) 2025 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// simplecli.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+1 -1
View File
@@ -1,7 +1,7 @@
//
// simplesvr.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+234
View File
@@ -0,0 +1,234 @@
//
// ssecli-stream.cc
//
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
// SSE (Server-Sent Events) client example using Streaming API
// with automatic reconnection support (similar to JavaScript's EventSource)
//
#include <httplib.h>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
//------------------------------------------------------------------------------
// SSE Event Parser
//------------------------------------------------------------------------------
// Parses SSE events from the stream according to the SSE specification.
// SSE format:
// event: <event-type> (optional, defaults to "message")
// data: <payload> (can have multiple lines)
// id: <event-id> (optional, used for reconnection)
// retry: <milliseconds> (optional, reconnection interval)
// <blank line> (signals end of event)
//
struct SSEEvent {
std::string event = "message"; // Event type (default: "message")
std::string data; // Event payload
std::string id; // Event ID for Last-Event-ID header
void clear() {
event = "message";
data.clear();
id.clear();
}
};
// Parse a single SSE field line (e.g., "data: hello")
// Returns true if this line ends an event (blank line)
bool parse_sse_line(const std::string &line, SSEEvent &event, int &retry_ms) {
// Blank line signals end of event
if (line.empty() || line == "\r") { return true; }
// Find the colon separator
auto colon_pos = line.find(':');
if (colon_pos == std::string::npos) {
// Line with no colon is treated as field name with empty value
return false;
}
std::string field = line.substr(0, colon_pos);
std::string value;
// Value starts after colon, skip optional single space
if (colon_pos + 1 < line.size()) {
size_t value_start = colon_pos + 1;
if (line[value_start] == ' ') { value_start++; }
value = line.substr(value_start);
// Remove trailing \r if present
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
}
// Handle known fields
if (field == "event") {
event.event = value;
} else if (field == "data") {
// Multiple data lines are concatenated with newlines
if (!event.data.empty()) { event.data += "\n"; }
event.data += value;
} else if (field == "id") {
// Empty id is valid (clears the last event ID)
event.id = value;
} else if (field == "retry") {
// Parse retry interval in milliseconds
try {
retry_ms = std::stoi(value);
} catch (...) {
// Invalid retry value, ignore
}
}
// Unknown fields are ignored per SSE spec
return false;
}
//------------------------------------------------------------------------------
// Main - SSE Client with Auto-Reconnection
//------------------------------------------------------------------------------
int main(void) {
// Configuration
const std::string host = "http://localhost:1234";
const std::string path = "/event1";
httplib::Client cli(host);
// State for reconnection (persists across connections)
std::string last_event_id; // Sent as Last-Event-ID header on reconnect
int retry_ms = 3000; // Reconnection delay (server can override via retry:)
int connection_count = 0;
std::cout << "SSE Client starting...\n";
std::cout << "Target: " << host << path << "\n";
std::cout << "Press Ctrl+C to exit\n\n";
//----------------------------------------------------------------------------
// Main reconnection loop
// This mimics JavaScript's EventSource behavior:
// - Automatically reconnects on connection failure
// - Sends Last-Event-ID header to resume from last received event
// - Respects server's retry interval
//----------------------------------------------------------------------------
while (true) {
connection_count++;
std::cout << "[Connection #" << connection_count << "] Connecting...\n";
// Build headers, including Last-Event-ID if we have one
httplib::Headers headers;
if (!last_event_id.empty()) {
headers.emplace("Last-Event-ID", last_event_id);
std::cout << "[Connection #" << connection_count
<< "] Resuming from event ID: " << last_event_id << "\n";
}
// Open streaming connection
auto result = httplib::stream::Get(cli, path, headers);
//--------------------------------------------------------------------------
// Connection error handling
//--------------------------------------------------------------------------
if (!result) {
std::cerr << "[Connection #" << connection_count
<< "] Failed: " << httplib::to_string(result.error()) << "\n";
std::cerr << "Reconnecting in " << retry_ms << "ms...\n\n";
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
continue;
}
if (result.status() != 200) {
std::cerr << "[Connection #" << connection_count
<< "] HTTP error: " << result.status() << "\n";
// For certain errors, don't reconnect
if (result.status() == 204 || // No Content - server wants us to stop
result.status() == 404 || // Not Found
result.status() == 401 || // Unauthorized
result.status() == 403) { // Forbidden
std::cerr << "Permanent error, not reconnecting.\n";
return 1;
}
std::cerr << "Reconnecting in " << retry_ms << "ms...\n\n";
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
continue;
}
// Verify Content-Type (optional but recommended)
auto content_type = result.get_header_value("Content-Type");
if (content_type.find("text/event-stream") == std::string::npos) {
std::cerr << "[Warning] Content-Type is not text/event-stream: "
<< content_type << "\n";
}
std::cout << "[Connection #" << connection_count << "] Connected!\n\n";
//--------------------------------------------------------------------------
// Event receiving loop
// Reads chunks from the stream and parses SSE events
//--------------------------------------------------------------------------
std::string buffer;
SSEEvent current_event;
int event_count = 0;
// Read data from stream using httplib::stream API
while (result.next()) {
buffer.append(result.data(), result.size());
// Process complete lines in the buffer
size_t line_start = 0;
size_t newline_pos;
while ((newline_pos = buffer.find('\n', line_start)) !=
std::string::npos) {
std::string line = buffer.substr(line_start, newline_pos - line_start);
line_start = newline_pos + 1;
// Parse the line and check if event is complete
bool event_complete = parse_sse_line(line, current_event, retry_ms);
if (event_complete && !current_event.data.empty()) {
// Event received - process it
event_count++;
std::cout << "--- Event #" << event_count << " ---\n";
std::cout << "Type: " << current_event.event << "\n";
std::cout << "Data: " << current_event.data << "\n";
if (!current_event.id.empty()) {
std::cout << "ID: " << current_event.id << "\n";
}
std::cout << "\n";
// Update last_event_id for reconnection
// Note: Empty id clears the last event ID per SSE spec
if (!current_event.id.empty()) { last_event_id = current_event.id; }
current_event.clear();
}
}
// Keep unprocessed data in buffer
buffer.erase(0, line_start);
}
//--------------------------------------------------------------------------
// Connection ended - check why
//--------------------------------------------------------------------------
if (result.read_error() != httplib::Error::Success) {
std::cerr << "\n[Connection #" << connection_count
<< "] Error: " << httplib::to_string(result.read_error())
<< "\n";
} else {
std::cout << "\n[Connection #" << connection_count
<< "] Stream ended normally\n";
}
std::cout << "Received " << event_count << " events in this connection\n";
std::cout << "Reconnecting in " << retry_ms << "ms...\n\n";
std::this_thread::sleep_for(std::chrono::milliseconds(retry_ms));
}
return 0;
}
+43 -7
View File
@@ -1,21 +1,57 @@
//
// ssecli.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
#include <httplib.h>
#include <csignal>
#include <iostream>
using namespace std;
int main(void) {
httplib::Client("http://localhost:1234")
.Get("/event1", [&](const char *data, size_t data_length) {
std::cout << string(data, data_length);
return true;
});
// Global SSEClient pointer for signal handling
httplib::sse::SSEClient *g_sse = nullptr;
void signal_handler(int) {
if (g_sse) { g_sse->stop(); }
}
int main(void) {
// Configuration
const string host = "http://localhost:1234";
const string path = "/event1";
cout << "SSE Client using httplib::sse::SSEClient\n";
cout << "Connecting to: " << host << path << "\n";
cout << "Press Ctrl+C to exit\n\n";
httplib::Client cli(host);
httplib::sse::SSEClient sse(cli, path);
// Set up signal handler for graceful shutdown
g_sse = &sse;
signal(SIGINT, signal_handler);
// Event handlers
sse.on_open([]() { cout << "[Connected]\n\n"; });
sse.on_message([](const httplib::sse::SSEMessage &msg) {
cout << "Event: " << msg.event << "\n";
cout << "Data: " << msg.data << "\n";
if (!msg.id.empty()) { cout << "ID: " << msg.id << "\n"; }
cout << "\n";
});
sse.on_error([](httplib::Error err) {
cerr << "[Error] " << httplib::to_string(err) << "\n";
});
// Start with auto-reconnect (blocking)
sse.start();
cout << "\n[Disconnected]\n";
return 0;
}
+11 -6
View File
@@ -14,11 +14,18 @@ class EventDispatcher {
public:
EventDispatcher() {}
void wait_event(DataSink *sink) {
bool wait_event(DataSink *sink) {
unique_lock<mutex> lk(m_);
int id = id_;
cv_.wait(lk, [&] { return cid_ == id; });
// Wait with timeout to prevent hanging if client disconnects
if (!cv_.wait_for(lk, std::chrono::seconds(5),
[&] { return cid_ == id; })) {
return false; // Timeout occurred
}
sink->write(message_.data(), message_.size());
return true;
}
void send_event(const string &message) {
@@ -71,8 +78,7 @@ int main(void) {
cout << "connected to event1..." << endl;
res.set_chunked_content_provider("text/event-stream",
[&](size_t /*offset*/, DataSink &sink) {
ed.wait_event(&sink);
return true;
return ed.wait_event(&sink);
});
});
@@ -80,8 +86,7 @@ int main(void) {
cout << "connected to event2..." << endl;
res.set_chunked_content_provider("text/event-stream",
[&](size_t /*offset*/, DataSink &sink) {
ed.wait_event(&sink);
return true;
return ed.wait_event(&sink);
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
//
// upload.cc
//
// Copyright (c) 2019 Yuji Hirose. All rights reserved.
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
// MIT License
//
+3166 -709
View File
File diff suppressed because it is too large Load Diff
+24 -10
View File
@@ -18,6 +18,14 @@ project(
cxx = meson.get_compiler('cpp')
if cxx.sizeof('void *') != 8
if host_machine.system() == 'windows'
error('unsupported architecture: cpp-httplib doesn\'t support 32-bit Windows. Please use a 64-bit compiler.')
else
warning('cpp-httplib doesn\'t support 32-bit platforms. Please use a 64-bit compiler.')
endif
endif
# Check just in case downstream decides to edit the source
# and add a project version
version = meson.project_version()
@@ -31,12 +39,12 @@ endif
deps = [dependency('threads')]
args = []
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('cpp-httplib_openssl'))
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('openssl'))
if openssl_dep.found()
deps += openssl_dep
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
if host_machine.system() == 'darwin'
macosx_keychain_dep = dependency('appleframeworks', modules: ['CoreFoundation', 'Security'], required: get_option('cpp-httplib_macosx_keychain'))
macosx_keychain_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
if macosx_keychain_dep.found()
deps += macosx_keychain_dep
args += '-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN'
@@ -44,15 +52,15 @@ if openssl_dep.found()
endif
endif
zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))
zlib_dep = dependency('zlib', required: get_option('zlib'))
if zlib_dep.found()
deps += zlib_dep
args += '-DCPPHTTPLIB_ZLIB_SUPPORT'
endif
brotli_deps = [dependency('libbrotlicommon', required: get_option('cpp-httplib_brotli'))]
brotli_deps += dependency('libbrotlidec', required: get_option('cpp-httplib_brotli'))
brotli_deps += dependency('libbrotlienc', required: get_option('cpp-httplib_brotli'))
brotli_deps = [dependency('libbrotlicommon', required: get_option('brotli'))]
brotli_deps += dependency('libbrotlidec', required: get_option('brotli'))
brotli_deps += dependency('libbrotlienc', required: get_option('brotli'))
brotli_found_all = true
foreach brotli_dep : brotli_deps
@@ -66,12 +74,18 @@ if brotli_found_all
args += '-DCPPHTTPLIB_BROTLI_SUPPORT'
endif
async_ns_opt = get_option('cpp-httplib_non_blocking_getaddrinfo')
zstd_dep = dependency('libzstd', required: get_option('zstd'))
if zstd_dep.found()
deps += zstd_dep
args += '-DCPPHTTPLIB_ZSTD_SUPPORT'
endif
async_ns_opt = get_option('non_blocking_getaddrinfo')
if host_machine.system() == 'windows'
async_ns_dep = cxx.find_library('ws2_32', required: async_ns_opt)
elif host_machine.system() == 'darwin'
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork'], required: async_ns_opt)
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation'], required: async_ns_opt)
else
async_ns_dep = cxx.find_library('anl', required: async_ns_opt)
endif
@@ -83,7 +97,7 @@ endif
cpp_httplib_dep = dependency('', required: false)
if get_option('cpp-httplib_compile')
if get_option('compile')
python3 = find_program('python3')
httplib_ch = custom_target(
@@ -127,6 +141,6 @@ endif
meson.override_dependency('cpp-httplib', cpp_httplib_dep)
if get_option('cpp-httplib_test')
if get_option('test')
subdir('test')
endif
+1
View File
@@ -5,6 +5,7 @@
option('openssl', type: 'feature', value: 'auto', description: 'Enable OpenSSL support')
option('zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
option('brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
option('zstd', type: 'feature', value: 'auto', description: 'Enable zstd support')
option('macosx_keychain', type: 'feature', value: 'auto', description: 'Enable loading certs from the Keychain on Apple devices')
option('non_blocking_getaddrinfo', type: 'feature', value: 'auto', description: 'Enable asynchronous name lookup')
option('compile', type: 'boolean', value: false, description: 'Split the header into a compilable header & source file (requires python3)')
+57 -51
View File
@@ -2,66 +2,72 @@
"""This script splits httplib.h into .h and .cc parts."""
import argparse
import os
import sys
from argparse import ArgumentParser, Namespace
from typing import List
border = '// ----------------------------------------------------------------------------'
args_parser = argparse.ArgumentParser(description=__doc__)
args_parser.add_argument(
"-e", "--extension", help="extension of the implementation file (default: cc)",
default="cc"
)
args_parser.add_argument(
"-o", "--out", help="where to write the files (default: out)", default="out"
)
args = args_parser.parse_args()
def main() -> None:
"""Main entry point for the script."""
BORDER: str = '// ----------------------------------------------------------------------------'
cur_dir = os.path.dirname(sys.argv[0])
lib_name = 'httplib'
header_name = '/' + lib_name + '.h'
source_name = '/' + lib_name + '.' + args.extension
# get the input file
in_file = cur_dir + header_name
# get the output file
h_out = args.out + header_name
cc_out = args.out + source_name
args_parser: ArgumentParser = ArgumentParser(description=__doc__)
args_parser.add_argument(
"-e", "--extension", help="extension of the implementation file (default: cc)",
default="cc"
)
args_parser.add_argument(
"-o", "--out", help="where to write the files (default: out)", default="out"
)
args: Namespace = args_parser.parse_args()
# if the modification time of the out file is after the in file,
# don't split (as it is already finished)
do_split = True
cur_dir: str = os.path.dirname(sys.argv[0])
if not cur_dir:
cur_dir = '.'
lib_name: str = 'httplib'
header_name: str = f"/{lib_name}.h"
source_name: str = f"/{lib_name}.{args.extension}"
# get the input file
in_file: str = cur_dir + header_name
# get the output file
h_out: str = args.out + header_name
cc_out: str = args.out + source_name
if os.path.exists(h_out):
in_time = os.path.getmtime(in_file)
out_time = os.path.getmtime(h_out)
do_split = in_time > out_time
# if the modification time of the out file is after the in file,
# don't split (as it is already finished)
do_split: bool = True
if do_split:
with open(in_file) as f:
lines = f.readlines()
if os.path.exists(h_out):
in_time: float = os.path.getmtime(in_file)
out_time: float = os.path.getmtime(h_out)
do_split: bool = in_time > out_time
if do_split:
with open(in_file) as f:
lines: List[str] = f.readlines()
python_version = sys.version_info[0]
if python_version < 3:
os.makedirs(args.out)
else:
os.makedirs(args.out, exist_ok=True)
in_implementation = False
cc_out = args.out + source_name
with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
fc.write('#include "httplib.h"\n')
fc.write('namespace httplib {\n')
for line in lines:
is_border_line = border in line
if is_border_line:
in_implementation = not in_implementation
elif in_implementation:
fc.write(line.replace('inline ', ''))
else:
fh.write(line)
fc.write('} // namespace httplib\n')
in_implementation: bool = False
cc_out: str = args.out + source_name
with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
fc.write('#include "httplib.h"\n')
fc.write('namespace httplib {\n')
for line in lines:
is_border_line: bool = BORDER in line
if is_border_line:
in_implementation: bool = not in_implementation
elif in_implementation:
fc.write(line.replace('inline ', ''))
else:
fh.write(line)
fc.write('} // namespace httplib\n')
print("Wrote {} and {}".format(h_out, cc_out))
else:
print("{} and {} are up to date".format(h_out, cc_out))
print(f"Wrote {h_out} and {cc_out}")
else:
print(f"{h_out} and {cc_out} are up to date")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -993,7 +993,7 @@ void UniversalTersePrint(const T& value, ::std::ostream* os) {
// NUL-terminated string.
template <typename T>
void UniversalPrint(const T& value, ::std::ostream* os) {
// A workarond for the bug in VC++ 7.1 that prevents us from instantiating
// A workaround for the bug in VC++ 7.1 that prevents us from instantiating
// UniversalPrinter with T directly.
typedef T T1;
UniversalPrinter<T1>::Print(value, os);
@@ -753,7 +753,7 @@ class ParameterizedTestSuiteRegistry {
};
// Keep track of what type-parameterized test suite are defined and
// where as well as which are intatiated. This allows susequently
// where as well as which are intatiated. This allows subsequently
// identifying suits that are defined but never used.
class TypeParameterizedTestSuiteRegistry {
public:
@@ -764,7 +764,7 @@ class TypeParameterizedTestSuiteRegistry {
// Add an instantiation of a suit.
void RegisterInstantiation(const char* test_suite_name);
// For each suit repored as defined but not reported as instantiation,
// For each suit reported as defined but not reported as instantiation,
// emit a test that reports that fact (configurably, as an error).
void CheckForInstantiations();
+4 -2
View File
@@ -108,9 +108,11 @@ subdir('www')
subdir('www2'/'dir')
subdir('www3'/'dir')
# GoogleTest 1.13.0 requires C++14
# New GoogleTest versions require new C++ standards
test_options = []
if gtest_dep.version().version_compare('>=1.13.0')
if gtest_dep.version().version_compare('>=1.17.0')
test_options += 'cpp_std=c++17'
elif gtest_dep.version().version_compare('>=1.13.0')
test_options += 'cpp_std=c++14'
endif
+3541 -87
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -153,13 +153,13 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
}
TEST(BaseAuthTest, NoSSL) {
Client cli("httpbin.org");
Client cli("httpcan.org");
BaseAuthTestFromHTTPWatch(cli);
}
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
TEST(BaseAuthTest, SSL) {
SSLClient cli("httpbin.org");
SSLClient cli("httpcan.org");
BaseAuthTestFromHTTPWatch(cli);
}
#endif
@@ -182,15 +182,17 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
"/digest-auth/auth/hello/world/MD5",
"/digest-auth/auth/hello/world/SHA-256",
"/digest-auth/auth/hello/world/SHA-512",
"/digest-auth/auth-int/hello/world/MD5",
};
cli.set_digest_auth("hello", "world");
for (auto path : paths) {
auto res = cli.Get(path.c_str());
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
normalizeJson(res->body));
std::string algo(path.substr(path.rfind('/') + 1));
EXPECT_EQ(
normalizeJson("{\"algorithm\":\"" + algo +
"\",\"authenticated\":true,\"user\":\"hello\"}\n"),
normalizeJson(res->body));
EXPECT_EQ(StatusCode::OK_200, res->status);
}
@@ -201,24 +203,22 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
}
// NOTE: Until httpbin.org fixes issue #46, the following test is commented
// out. Please see https://httpbin.org/digest-auth/auth/hello/world
// cli.set_digest_auth("bad", "world");
// for (auto path : paths) {
// auto res = cli.Get(path.c_str());
// ASSERT_TRUE(res != nullptr);
// EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
// }
cli.set_digest_auth("bad", "world");
for (auto path : paths) {
auto res = cli.Get(path.c_str());
ASSERT_TRUE(res != nullptr);
EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
}
}
}
TEST(DigestAuthTest, SSL) {
SSLClient cli("httpbin.org");
SSLClient cli("httpcan.org");
DigestAuthTestFromHTTPWatch(cli);
}
TEST(DigestAuthTest, NoSSL) {
Client cli("httpbin.org");
Client cli("httpcan.org");
DigestAuthTestFromHTTPWatch(cli);
}
#endif