mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-09 16:52:52 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf3693cb5c | ||
|
|
4fd9ae8f42 | ||
|
|
3d40dfc727 | ||
|
|
dd71728110 | ||
|
|
7255a7e979 | ||
|
|
8a3abfb597 | ||
|
|
10aadd57f7 | ||
|
|
43863e1f67 | ||
|
|
0db1df7cf2 | ||
|
|
639391ad7f | ||
|
|
0715c2739e | ||
|
|
3330d0eb06 | ||
|
|
c1c2b1f4b4 | ||
|
|
e11dbec7b3 | ||
|
|
174bce5ccf | ||
|
|
57c4f7f385 | ||
|
|
2fb2dbbe1e | ||
|
|
8b6ab24159 | ||
|
|
5a202d3d5f | ||
|
|
4f3f9ef19b | ||
|
|
6d59d1e2df | ||
|
|
9386b25dd7 | ||
|
|
91c55a4385 | ||
|
|
ad88645a83 | ||
|
|
82722fcb13 | ||
|
|
8b872605e0 | ||
|
|
52f214bf2e | ||
|
|
09c02f1335 | ||
|
|
2e5480ad65 | ||
|
|
4cb363e3f2 | ||
|
|
deb520e26b | ||
|
|
f37a5b1407 | ||
|
|
f15992c7ed | ||
|
|
278c2979e8 | ||
|
|
6b8c3f5387 | ||
|
|
e640f8376b | ||
|
|
73a4092f8a | ||
|
|
3517f92e2f | ||
|
|
0480ff77b8 | ||
|
|
8d25b6a3ac | ||
|
|
515b8f84af | ||
|
|
88956ccad8 | ||
|
|
f83d06538b | ||
|
|
dc41dbd954 | ||
|
|
6303a99ce1 | ||
|
|
d5f8858731 | ||
|
|
199d7ee248 | ||
|
|
9e2e33da56 | ||
|
|
7d53a31d23 |
@@ -31,12 +31,19 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
style-check:
|
style-check:
|
||||||
runs-on: ubuntu-latest
|
# Uses the macOS runner's pre-installed Homebrew so clang-format tracks
|
||||||
|
# whatever version `brew install clang-format` currently resolves to on
|
||||||
|
# the maintainer's own Mac, instead of a version pinned in this file.
|
||||||
|
runs-on: macos-latest
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
- name: install clang-format
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install clang-format coreutils
|
||||||
- name: run style check
|
- name: run style check
|
||||||
run: |
|
run: |
|
||||||
clang-format --version
|
clang-format --version
|
||||||
@@ -475,7 +482,6 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
|
||||||
if: >
|
if: >
|
||||||
(github.event_name == 'push') ||
|
(github.event_name == 'push') ||
|
||||||
(github.event_name == 'pull_request' &&
|
(github.event_name == 'pull_request' &&
|
||||||
@@ -585,7 +591,6 @@ jobs:
|
|||||||
- name: Build ${{ matrix.config.name }}
|
- name: Build ${{ matrix.config.name }}
|
||||||
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
|
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
|
||||||
- name: Run tests ${{ matrix.config.name }}
|
- name: Run tests ${{ matrix.config.name }}
|
||||||
id: run_tests
|
|
||||||
if: ${{ matrix.config.run_tests }}
|
if: ${{ matrix.config.run_tests }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
working-directory: build/test
|
working-directory: build/test
|
||||||
@@ -618,34 +623,6 @@ jobs:
|
|||||||
}
|
}
|
||||||
if ($failed) { exit 1 }
|
if ($failed) { exit 1 }
|
||||||
Write-Host "All shards passed."
|
Write-Host "All shards passed."
|
||||||
- name: Report flaky failure on issue #2533
|
|
||||||
if: >
|
|
||||||
failure() && steps.run_tests.conclusion == 'failure'
|
|
||||||
&& matrix.config.name == 'without SSL'
|
|
||||||
&& github.event_name == 'push'
|
|
||||||
continue-on-error: true
|
|
||||||
shell: pwsh
|
|
||||||
working-directory: build/test
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
$summary = ""
|
|
||||||
for ($i = 0; $i -lt 4; $i++) {
|
|
||||||
$log = "shard_${i}.log"
|
|
||||||
if (Test-Path $log) {
|
|
||||||
$failedLines = Select-String -Path $log -Pattern "\[ FAILED \]"
|
|
||||||
if ($failedLines) {
|
|
||||||
$summary += "**Shard ${i}:**`n" + (($failedLines | ForEach-Object { $_.Line }) -join "`n") + "`n`n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $summary) {
|
|
||||||
Write-Host "No [ FAILED ] line in any shard log; not a test failure. Skipping the report."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
$runUrl = "$($env:GITHUB_SERVER_URL)/$($env:GITHUB_REPOSITORY)/actions/runs/$($env:GITHUB_RUN_ID)"
|
|
||||||
$body = "Reoccurred on push: $runUrl`n`nCommit: $($env:GITHUB_SHA)`n`n$summary"
|
|
||||||
gh issue comment 2533 --repo $env:GITHUB_REPOSITORY --body $body
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VCPKG_ROOT: "C:/vcpkg"
|
VCPKG_ROOT: "C:/vcpkg"
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: local
|
||||||
rev: v18.1.8 # 最新バージョンを使用
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
files: \.(cpp|cc|h)$
|
name: clang-format
|
||||||
args: [-i] # インプレースで修正
|
# Uses whatever `clang-format` resolves to on PATH (the Homebrew
|
||||||
|
# install on macOS) instead of a version pinned here, so it tracks
|
||||||
|
# the same version CI installs via `brew install clang-format`.
|
||||||
|
entry: clang-format
|
||||||
|
language: system
|
||||||
|
files: ^(httplib\.h|(example|fuzzing|test)/[^/]+\.(cpp|cc|h))$
|
||||||
|
args: [-i]
|
||||||
|
|||||||
+21
-3
@@ -15,7 +15,7 @@
|
|||||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||||
* HTTPLIB_REQUIRE_ZSTD (default off)
|
* HTTPLIB_REQUIRE_ZSTD (default off)
|
||||||
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
||||||
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
|
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on when supported)
|
||||||
* HTTPLIB_COMPILE (default off)
|
* HTTPLIB_COMPILE (default off)
|
||||||
* HTTPLIB_INSTALL (default on)
|
* HTTPLIB_INSTALL (default on)
|
||||||
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
||||||
@@ -181,6 +181,24 @@ if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
|
|||||||
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
||||||
endif()
|
endif()
|
||||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
||||||
|
if(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO AND WIN32)
|
||||||
|
include(CheckCXXSymbolExists)
|
||||||
|
|
||||||
|
set(_httplib_cmake_required_definitions ${CMAKE_REQUIRED_DEFINITIONS})
|
||||||
|
set(_httplib_cmake_required_libraries ${CMAKE_REQUIRED_LIBRARIES})
|
||||||
|
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_WIN32_WINNT=0x0A00)
|
||||||
|
list(APPEND CMAKE_REQUIRED_LIBRARIES ws2_32)
|
||||||
|
check_cxx_symbol_exists(GetAddrInfoExCancel "winsock2.h;ws2tcpip.h" HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||||
|
set(CMAKE_REQUIRED_DEFINITIONS ${_httplib_cmake_required_definitions})
|
||||||
|
set(CMAKE_REQUIRED_LIBRARIES ${_httplib_cmake_required_libraries})
|
||||||
|
unset(_httplib_cmake_required_definitions)
|
||||||
|
unset(_httplib_cmake_required_libraries)
|
||||||
|
|
||||||
|
if(NOT HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||||
|
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO FALSE)
|
||||||
|
message(WARNING "GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
||||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||||
@@ -367,7 +385,7 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
|||||||
# Needed for API from MacOS Security framework
|
# Needed for API from MacOS Security framework
|
||||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
||||||
# Needed for non-blocking getaddrinfo on MacOS
|
# Needed for non-blocking getaddrinfo on MacOS
|
||||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||||
# Can't put multiple targets in a single generator expression or it bugs out.
|
# 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::common>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
||||||
@@ -390,7 +408,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
|||||||
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
||||||
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
||||||
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
$<$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
||||||
)
|
)
|
||||||
|
|
||||||
# CMake configuration files installation directory
|
# CMake configuration files installation directory
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ sse.on_error([](httplib::Error err) { });
|
|||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Set reconnect interval (default: 3000ms)
|
// Set reconnect interval (default: 3000ms, minimum: 100ms)
|
||||||
sse.set_reconnect_interval(5000);
|
sse.set_reconnect_interval(5000);
|
||||||
|
|
||||||
// Set max reconnect attempts (default: 0 = unlimited)
|
// Set max reconnect attempts (default: 0 = unlimited)
|
||||||
|
|||||||
+34
-5
@@ -57,14 +57,30 @@ if (ws.connect()) {
|
|||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
enum ReadResult : int {
|
enum ReadResult : int {
|
||||||
Fail = 0, // Connection closed or error
|
Fail = 0, // Connection closed or error
|
||||||
Text = 1, // UTF-8 text message
|
Text = 1, // UTF-8 text message
|
||||||
Binary = 2, // Binary message
|
Binary = 2, // Binary message
|
||||||
|
Timeout = 3, // Read timeout elapsed; connection still open
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Returned by `read()`. Since `Fail` is `0`, the result works naturally in boolean contexts — `while (ws.read(msg))` continues until the connection closes. When you need to distinguish text from binary, check the return value directly.
|
Returned by `read()`. Since `Fail` is `0`, the result works naturally in boolean contexts — `while (ws.read(msg))` continues until the connection closes. When you need to distinguish text from binary, check the return value directly.
|
||||||
|
|
||||||
|
`Timeout` is only returned for a read timeout you set yourself with `set_read_timeout()`. It means the timeout elapsed on a message boundary: nothing was consumed and the connection is still open, so you can send on it and read again. The compile-time defaults (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`, 300 seconds on the server; a client waits forever) are a backstop against a peer that has gone quiet, not a request for control: when one of them elapses, `read()` returns `Fail` and closes the connection, so code that never calls `set_read_timeout()` can keep using `while (ws.read(msg))`.
|
||||||
|
|
||||||
|
**`msg` is left untouched on `Timeout`.** Because `Timeout` is non-zero, `while (ws.read(msg))` keeps looping — with the *previous* message still in `msg`. Once you set a read timeout, test the result instead:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ws.set_read_timeout(std::chrono::milliseconds(100));
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) { continue; } // nothing yet; send if you like
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### CloseStatus
|
### CloseStatus
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -327,6 +343,18 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The check above runs after the handshake, so the client sees a successful upgrade followed by a close frame. To refuse the upgrade itself with an HTTP status, use a pre-routing or pre-request handler. Both run before the `101 Switching Protocols` response, and `req.matched_route` is available in the pre-request handler:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
if (req.matched_route == "/ws" && req.get_header_value("Authorization").empty()) {
|
||||||
|
res.status = httplib::StatusCode::Unauthorized_401;
|
||||||
|
return httplib::Server::HandlerResponse::Handled; // not upgraded
|
||||||
|
}
|
||||||
|
return httplib::Server::HandlerResponse::Unhandled;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Custom Headers and Timeouts
|
### Custom Headers and Timeouts
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -409,7 +437,8 @@ if (ws.connect()) {
|
|||||||
| Macro | Default | Description |
|
| Macro | Default | Description |
|
||||||
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
||||||
| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message |
|
| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND` | `0` | Client read timeout (seconds); `0` waits forever |
|
||||||
|
| `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` | `300` | Server read timeout (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
||||||
@@ -446,7 +475,7 @@ The server side has the same `set_websocket_max_missed_pongs()`.
|
|||||||
|
|
||||||
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
|
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
|
||||||
|
|
||||||
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. Even so, a dead connection does not linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. `max_missed_pongs` is the knob for detecting an unresponsive peer faster than that 5-minute fallback.
|
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. On the server side a dead connection still does not linger: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no such backstop — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side it is also the knob for noticing one *faster* than the 5-minute fallback.
|
||||||
|
|
||||||
## Threading Model
|
## Threading Model
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
|
|||||||
| Platform | Behavior | Disable (compile time) |
|
| Platform | Behavior | Disable (compile time) |
|
||||||
| :------- | :------- | :--------------------- |
|
| :------- | :------- | :--------------------- |
|
||||||
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
||||||
| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend, with revocation checking. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||||
|
|
||||||
On Windows, verification can also be disabled at runtime:
|
On Windows, verification can also be disabled at runtime:
|
||||||
|
|
||||||
@@ -347,6 +347,25 @@ int port = svr.bind_to_any_port("0.0.0.0");
|
|||||||
svr.listen_after_bind();
|
svr.listen_after_bind();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Port sharing and exclusive binding
|
||||||
|
|
||||||
|
By default, the server socket enables address/port reuse: `SO_REUSEPORT` where it is available (Linux, macOS), and `SO_REUSEADDR` otherwise (Windows). A restarted server can bind again immediately, but binding to a port that another server is already listening on also succeeds, and connections are distributed between them.
|
||||||
|
|
||||||
|
If you want `listen()` to fail when the port is already in use, replace the default socket options with `set_socket_options`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Setting only `SO_REUSEADDR` is not enough on Windows. There, `SO_REUSEADDR` allows two sockets that both set it to bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
|
||||||
|
|
||||||
### Static File Server
|
### Static File Server
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -433,6 +452,9 @@ svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `req.path` is percent-decoded and may contain control characters such as CR/LF. Escape request data before writing it to a log file (see [docker/main.cc](docker/main.cc) for an example).
|
||||||
|
|
||||||
#### Pre-compression Logging
|
#### Pre-compression Logging
|
||||||
|
|
||||||
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
||||||
@@ -545,14 +567,15 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
|||||||
|
|
||||||
```
|
```
|
||||||
Request received
|
Request received
|
||||||
|
│
|
||||||
|
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
|
||||||
|
│ └─ returns a status other than 100 → stop here
|
||||||
│
|
│
|
||||||
├─ pre_routing_handler route not matched yet, body not read
|
├─ pre_routing_handler route not matched yet, body not read
|
||||||
│ └─ returns Handled → stop here
|
│ └─ returns Handled → stop here
|
||||||
│
|
│
|
||||||
├─ file_request_handler (GET/HEAD, static file serving)
|
├─ file_request_handler (GET/HEAD, static file serving)
|
||||||
│
|
│
|
||||||
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
|
|
||||||
│
|
|
||||||
├─ route matching → req.matched_route is set
|
├─ route matching → req.matched_route is set
|
||||||
│
|
│
|
||||||
├─ pre_request_handler route matched, body NOT read yet
|
├─ pre_request_handler route matched, body NOT read yet
|
||||||
@@ -568,6 +591,10 @@ Request received
|
|||||||
|
|
||||||
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
|
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
|
||||||
|
|
||||||
|
For a request with `Expect: 100-continue`, the `100 Continue` response is not sent until the body is about to be read. A request rejected before that point (by `pre_routing_handler`, `pre_request_handler`, or because no route matched) gets its final response without `100 Continue`, so the client never sends the body.
|
||||||
|
|
||||||
|
A WebSocket upgrade request that matches a route registered with `svr.WebSocket()` takes a shorter path: `pre_routing_handler`, then route matching (`req.matched_route` is set), then `pre_request_handler`, then the WebSocket handler. If either hook returns `Handled`, its response is sent as a regular HTTP response and the connection is not upgraded. Once the connection is upgraded, `post_routing_handler` does not run.
|
||||||
|
|
||||||
### Response user data
|
### Response user data
|
||||||
|
|
||||||
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||||
@@ -827,7 +854,9 @@ svr.Get("/content", [&](const Request &req, Response &res) {
|
|||||||
|
|
||||||
### 'Expect: 100-continue' handler
|
### 'Expect: 100-continue' handler
|
||||||
|
|
||||||
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.
|
By default, the server accepts an `Expect: 100-continue` header and sends a `100 Continue` response when it starts reading the request body. If the request is answered without reading the body, `100 Continue` is not sent and the connection is closed after the response.
|
||||||
|
|
||||||
|
The handler runs before `pre_routing_handler`. Returning `100` lets the request proceed; returning any other status sends that status as the final response and closes the connection.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Send a '417 Expectation Failed' response.
|
// Send a '417 Expectation Failed' response.
|
||||||
@@ -1474,6 +1503,19 @@ The server can apply compression to the following MIME type contents:
|
|||||||
- application/protobuf
|
- application/protobuf
|
||||||
- application/xhtml+xml
|
- application/xhtml+xml
|
||||||
|
|
||||||
|
A response that already carries `Content-Encoding` is sent as it is. A handler serving content it encoded itself, an asset compressed at build time for instance, keeps its own coding and its own bytes:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
svr.Get("/app.js", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_header("Content-Encoding", "gzip");
|
||||||
|
res.set_content(gzipped_asset, "application/javascript");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This holds for every kind of response, including the file-backed ones below.
|
||||||
|
|
||||||
|
`Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
|
||||||
|
|
||||||
### Static file compression
|
### Static file compression
|
||||||
|
|
||||||
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
|
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
|
||||||
|
|||||||
+26
-6
@@ -48,6 +48,23 @@ std::string get_error_time_format() {
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape a value for a log line the way NGINX does: '"', '\\', control
|
||||||
|
// bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
|
||||||
|
// (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
|
||||||
|
// writing them verbatim would let a client forge extra log lines.
|
||||||
|
std::string escape_log(const std::string &s) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (unsigned char c : s) {
|
||||||
|
if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
|
||||||
|
out += std::format("\\x{:02X}", c);
|
||||||
|
} else {
|
||||||
|
out += static_cast<char>(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// NGINX Combined log format:
|
// NGINX Combined log format:
|
||||||
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||||
// "$http_referer" "$http_user_agent"
|
// "$http_referer" "$http_user_agent"
|
||||||
@@ -55,7 +72,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
std::string remote_user =
|
std::string remote_user =
|
||||||
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
||||||
auto time_local = get_time_format();
|
auto time_local = get_time_format();
|
||||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
// $request is the original request line, so log the raw target rather than
|
||||||
|
// the percent-decoded req.path.
|
||||||
|
auto request = std::format("{} {} {}", req.method, req.target, req.version);
|
||||||
auto status = res.status;
|
auto status = res.status;
|
||||||
auto body_bytes_sent = res.body.size();
|
auto body_bytes_sent = res.body.size();
|
||||||
auto http_referer = req.get_header_value("Referer");
|
auto http_referer = req.get_header_value("Referer");
|
||||||
@@ -64,9 +83,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
if (http_user_agent.empty()) http_user_agent = "-";
|
if (http_user_agent.empty()) http_user_agent = "-";
|
||||||
|
|
||||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||||
req.remote_addr, remote_user, time_local, request,
|
req.remote_addr, remote_user, time_local,
|
||||||
status, body_bytes_sent, http_referer,
|
escape_log(request), status, body_bytes_sent,
|
||||||
http_user_agent)
|
escape_log(http_referer), escape_log(http_user_agent))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +98,15 @@ void nginx_error_logger(const Error &err, const Request *req) {
|
|||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
auto request =
|
auto request =
|
||||||
std::format("{} {} {}", req->method, req->path, req->version);
|
std::format("{} {} {}", req->method, req->target, req->version);
|
||||||
auto host = req->get_header_value("Host");
|
auto host = req->get_header_value("Host");
|
||||||
if (host.empty()) host = "-";
|
if (host.empty()) host = "-";
|
||||||
|
|
||||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||||
"\"{}\", host: \"{}\"",
|
"\"{}\", host: \"{}\"",
|
||||||
time_local, level, to_string(err),
|
time_local, level, to_string(err),
|
||||||
req->remote_addr, request, host)
|
req->remote_addr, escape_log(request),
|
||||||
|
escape_log(host))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
} else {
|
} else {
|
||||||
// If no request context, just log the error
|
// If no request context, just log the error
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.54.1"
|
version = "0.59.0"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ svr.set_pre_request_handler(
|
|||||||
|
|
||||||
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
|
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
|
||||||
|
|
||||||
|
The pre-request handler also runs for routes registered with `svr.WebSocket()`. It is called before the `101 Switching Protocols` response, so returning `Handled` sends your HTTP response (such as a 403) and the connection is never upgraded.
|
||||||
|
|
||||||
## Return values
|
## Return values
|
||||||
|
|
||||||
Same as pre-routing — return `HandlerResponse`.
|
Same as pre-routing — return `HandlerResponse`.
|
||||||
|
|||||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
|||||||
|
|
||||||
## Check the return values
|
## Check the return values
|
||||||
|
|
||||||
`bind_to_port()` returns `false` on failure — typically when the port is already taken. Always check it.
|
`bind_to_port()` returns `false` on failure, for example when you don't have permission to bind to the port. Always check it.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
std::cerr << "port already in use" << std::endl;
|
std::cerr << "bind failed" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
||||||
|
|
||||||
|
## Detect a port that's already in use
|
||||||
|
|
||||||
|
With the default settings, you can actually bind to a port another server is already using. That's because cpp-httplib sets `SO_REUSEPORT` (Linux, macOS) or `SO_REUSEADDR` (Windows) on the server socket. A restarted server can bind again right away. The flip side is that a second server on the same port starts without an error, and connections get split between the two.
|
||||||
|
|
||||||
|
To make `bind_to_port()` fail on a port in use, replace the socket options with `set_socket_options()`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
|
std::cerr << "port already in use" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_socket_options()` replaces the defaults entirely. Setting `SO_REUSEADDR` on Linux and macOS keeps the "restarted server can bind again right away" behavior.
|
||||||
|
|
||||||
|
> **Note:** `SO_REUSEADDR` alone isn't enough on Windows. Two sockets that both set it can bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
|
||||||
|
|
||||||
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](../s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](../s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ The `read()` return value is a `ReadResult` enum:
|
|||||||
- `ReadResult::Text`: received a text message
|
- `ReadResult::Text`: received a text message
|
||||||
- `ReadResult::Binary`: received a binary message
|
- `ReadResult::Binary`: received a binary message
|
||||||
- `ReadResult::Fail`: error, or connection closed
|
- `ReadResult::Fail`: error, or connection closed
|
||||||
|
- `ReadResult::Timeout`: a read timeout you set with `set_read_timeout()` elapsed with nothing received; the connection is still open. The compile-time default timeout closes the connection and is reported as `Fail` instead — see [W06. Set Timeouts](../w06-websocket-timeouts)
|
||||||
|
|
||||||
## Client: talk to the echo server
|
## Client: talk to the echo server
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ The counter is reset whenever `read()` consumes an incoming Pong frame, so this
|
|||||||
|
|
||||||
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
|
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
|
||||||
|
|
||||||
Even with `0`, a dead connection won't linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. Think of `max_missed_pongs` as the knob for detecting an unresponsive peer **faster** than that.
|
On the server side, even with `0`, a dead connection won't linger forever: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no backstop of its own — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side, it is also how you notice one **faster** than that 5-minute fallback.
|
||||||
|
|
||||||
> For handling a closed connection, see [W03. Handle connection close](../w03-websocket-close).
|
> For handling a closed connection, see [W03. Handle connection close](../w03-websocket-close).
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ switch (result) {
|
|||||||
case httplib::ws::ReadResult::Fail:
|
case httplib::ws::ReadResult::Fail:
|
||||||
// error or closed
|
// error or closed
|
||||||
break;
|
break;
|
||||||
|
case httplib::ws::ReadResult::Timeout:
|
||||||
|
// read timeout elapsed; the connection is still open
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ status: "draft"
|
|||||||
| Kind | API | Default |
|
| Kind | API | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Connection | `set_connection_timeout` | 300s |
|
| Connection | `set_connection_timeout` | 300s |
|
||||||
| Read | `set_read_timeout` | 300s (`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`) |
|
| Read | `set_read_timeout` | none — waits forever (`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
|
||||||
| Write | `set_write_timeout` | 5s |
|
| Write | `set_write_timeout` | 5s |
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
@@ -26,7 +26,7 @@ if (ws.connect()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Set these before calling `connect()`.
|
Set the connection and write timeouts before calling `connect()`. The read timeout can be changed at any time — setting it on an open connection takes effect on the next `read()`.
|
||||||
|
|
||||||
## Use `std::chrono`
|
## Use `std::chrono`
|
||||||
|
|
||||||
@@ -40,9 +40,44 @@ ws.set_read_timeout(30s);
|
|||||||
ws.set_write_timeout(10s);
|
ws.set_write_timeout(10s);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Watch out for what the read timeout means
|
## What the read timeout means
|
||||||
|
|
||||||
`set_read_timeout()` applies to a single `read()` call. If no message arrives within that time, `read()` returns `ReadResult::Fail`. For connections where long idle periods are normal — waiting on notifications, for example — set a longer timeout, or reconnect from your application code when the read fails.
|
`set_read_timeout()` applies to a single `read()` call. If no message arrives within that time, `read()` returns `ReadResult::Timeout`: **the connection is still open** and nothing was consumed, so you can send on it and read again. That is what separates it from `ReadResult::Fail`, which means the connection is gone.
|
||||||
|
|
||||||
|
This is what lets one thread own a connection in both directions:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_read_timeout(100ms);
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
flush_outgoing(ws); // nothing arrived — send whatever is queued
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without a read timeout, `read()` blocks until a message arrives, so the thread holding the connection never gets to its writes.
|
||||||
|
|
||||||
|
Two things to know about `Timeout`:
|
||||||
|
|
||||||
|
- It leaves `msg` untouched, and it is non-zero. So `while (ws.read(msg))` is not usable once a read timeout is set — the loop would keep running with the *previous* message still in `msg`.
|
||||||
|
- It is only reported on a message boundary. If the timeout elapses partway through a fragmented message, that message cannot be resumed and `read()` returns `Fail`.
|
||||||
|
|
||||||
|
For connections where long idle periods are normal — waiting on notifications, for example — either leave the read timeout unset, or treat `Timeout` as the no-op it is and keep looping.
|
||||||
|
|
||||||
|
## On the server side
|
||||||
|
|
||||||
|
A handler's `ws::WebSocket` has `set_read_timeout()` too, and the pattern above is how a handler relays between connections instead of parking in `read()`.
|
||||||
|
|
||||||
|
The server default is 300s (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`) rather than "forever": it is a backstop that reclaims a worker from a peer that has gone silent, since a WebSocket handler holds its worker for the life of the connection.
|
||||||
|
|
||||||
|
Because it is a backstop rather than something the handler asked for, it does not surface as `Timeout`. When it elapses, `read()` returns `Fail` and closes the connection, so a handler written as `while (ws.read(msg))` ends the way it always has. Only a timeout the handler set itself with `set_read_timeout()` comes back as `Timeout`.
|
||||||
|
|
||||||
> Unresponsive-peer detection via Ping/Pong is a separate mechanism. See [W02. Set a WebSocket Heartbeat](../w02-websocket-ping) for details.
|
> Unresponsive-peer detection via Ping/Pong is a separate mechanism. See [W02. Set a WebSocket Heartbeat](../w02-websocket-ping) for details.
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A check inside the handler runs after the handshake has completed. To refuse the connection with an HTTP status such as 401 before it is upgraded, use `set_pre_request_handler()` instead. It also runs for WebSocket routes. See [S11. Authenticate per route with a pre-request handler](../../cookbook/s11-pre-request).
|
||||||
|
|
||||||
## Using WSS
|
## Using WSS
|
||||||
|
|
||||||
WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`.
|
WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`.
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ svr.set_pre_request_handler(
|
|||||||
|
|
||||||
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`)です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
|
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`)です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
|
||||||
|
|
||||||
|
`svr.WebSocket()`で登録したルートでも、Pre-requestハンドラは呼ばれます。呼ばれるのは`101 Switching Protocols`を返す前なので、`Handled`を返すとそのHTTPレスポンス(403など)がそのまま返り、WebSocketへのアップグレードは行われません。
|
||||||
|
|
||||||
## 戻り値の意味
|
## 戻り値の意味
|
||||||
|
|
||||||
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
||||||
|
|||||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
|||||||
|
|
||||||
## 戻り値のチェック
|
## 戻り値のチェック
|
||||||
|
|
||||||
`bind_to_port()`は失敗すると`false`を返します。ポートが既に使われている場合などです。必ずチェックしてください。
|
`bind_to_port()`は失敗すると`false`を返します。ポートにbindする権限が無い場合などです。必ずチェックしてください。
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
std::cerr << "port already in use" << std::endl;
|
std::cerr << "bind failed" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
||||||
|
|
||||||
|
## 使用中のポートを検出する
|
||||||
|
|
||||||
|
実は、デフォルトの設定では、ほかのサーバーが使っているポートにもbindできてしまいます。cpp-httplibがサーバーソケットに`SO_REUSEPORT`(Linux、macOS)か`SO_REUSEADDR`(Windows)を設定しているからです。再起動したサーバーはすぐにbindし直せます。その代わり、同じポートで2つ目のサーバーを起動してもエラーにならず、接続が両方に振り分けられます。
|
||||||
|
|
||||||
|
使用中のポートで`bind_to_port()`を失敗させたいときは、`set_socket_options()`でソケットオプションを差し替えてください。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
|
std::cerr << "port already in use" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_socket_options()`はデフォルトの設定を丸ごと置き換えます。Linux、macOSで`SO_REUSEADDR`を設定しているのは、再起動したサーバーがすぐにbindし直せるようにするためです。
|
||||||
|
|
||||||
|
> **Note:** Windowsでは`SO_REUSEADDR`だけでは足りません。お互いに`SO_REUSEADDR`を設定したソケット同士は、同じポートにbindできてしまいます。`SO_EXCLUSIVEADDRUSE`を使ってください。
|
||||||
|
|
||||||
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](../s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](../s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ int main() {
|
|||||||
|
|
||||||
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
||||||
|
|
||||||
`read()`の返り値は`ReadResult`列挙値で、次の3種類です。
|
`read()`の返り値は`ReadResult`列挙値で、次の4種類です。
|
||||||
|
|
||||||
- `ReadResult::Text`: テキストメッセージを受信
|
- `ReadResult::Text`: テキストメッセージを受信
|
||||||
- `ReadResult::Binary`: バイナリメッセージを受信
|
- `ReadResult::Binary`: バイナリメッセージを受信
|
||||||
- `ReadResult::Fail`: エラー、または接続が閉じた
|
- `ReadResult::Fail`: エラー、または接続が閉じた
|
||||||
|
- `ReadResult::Timeout`: `set_read_timeout()`で自分が設定した読み取りタイムアウトが、何も受信しないまま経過した。接続は開いたまま。コンパイル時のデフォルトのタイムアウトは接続を閉じ、`Fail`として返る([W06. タイムアウトを設定する](../w06-websocket-timeouts)を参照)
|
||||||
|
|
||||||
## クライアント: エコーを叩く
|
## クライアント: エコーを叩く
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ cli.set_websocket_max_missed_pongs(2); // 2回連続でPongが返ってこなけ
|
|||||||
|
|
||||||
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
||||||
|
|
||||||
ただし`0`のままでも最終的に接続が残り続けることはありません。`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働き、フレームが一定時間来なければ`read()`が失敗します。つまり`max_missed_pongs`は「**もっと速く**無応答を検出したい」ときに使うオプションだと考えてください。
|
サーバ側は`0`のままでも接続が残り続けることはありません。ハンドラが`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働きます。一方クライアント側にはこの保険がなく、読み取りタイムアウトを設定しない限り無期限に待つので、無応答ピアを検出する手段は`max_missed_pongs`だけです。どちらの側でも「**もっと速く**検出したい」ときに使うオプションでもあります。
|
||||||
|
|
||||||
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](../w03-websocket-close)を参照してください。
|
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](../w03-websocket-close)を参照してください。
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ switch (result) {
|
|||||||
case httplib::ws::ReadResult::Fail:
|
case httplib::ws::ReadResult::Fail:
|
||||||
// エラーまたは切断
|
// エラーまたは切断
|
||||||
break;
|
break;
|
||||||
|
case httplib::ws::ReadResult::Timeout:
|
||||||
|
// 読み取りタイムアウト。接続は開いたまま
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ status: "draft"
|
|||||||
| 種類 | API | デフォルト |
|
| 種類 | API | デフォルト |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 接続タイムアウト | `set_connection_timeout` | 300秒 |
|
| 接続タイムアウト | `set_connection_timeout` | 300秒 |
|
||||||
| 読み取りタイムアウト | `set_read_timeout` | 300秒(`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`) |
|
| 読み取りタイムアウト | `set_read_timeout` | なし。無期限に待つ(`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
|
||||||
| 書き込みタイムアウト | `set_write_timeout` | 5秒 |
|
| 書き込みタイムアウト | `set_write_timeout` | 5秒 |
|
||||||
|
|
||||||
## 基本の使い方
|
## 基本の使い方
|
||||||
@@ -26,7 +26,7 @@ if (ws.connect()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`connect()`を呼ぶ前に設定してください。
|
接続タイムアウトと書き込みタイムアウトは`connect()`を呼ぶ前に設定してください。読み取りタイムアウトはいつでも変更でき、接続済みの状態で設定した場合は次の`read()`から効きます。
|
||||||
|
|
||||||
## `std::chrono`で指定する
|
## `std::chrono`で指定する
|
||||||
|
|
||||||
@@ -40,9 +40,44 @@ ws.set_read_timeout(30s);
|
|||||||
ws.set_write_timeout(10s);
|
ws.set_write_timeout(10s);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 読み取りタイムアウトの意味に注意
|
## 読み取りタイムアウトの意味
|
||||||
|
|
||||||
`set_read_timeout()`は「1回の`read()`呼び出し」に対するタイムアウトです。メッセージが届かないまま指定時間が経過すると`read()`が`ReadResult::Fail`を返します。通知の待受のように長時間メッセージが来ないことが正常な接続では、意図せず切断されないよう長めに設定するか、切断されたらアプリケーション側で再接続してください。
|
`set_read_timeout()`は「1回の`read()`呼び出し」に対するタイムアウトです。メッセージが届かないまま指定時間が経過すると`read()`は`ReadResult::Timeout`を返します。このとき**接続は開いたまま**で、1バイトも読み進めていないので、そのまま送信して読み直せます。接続が失われたことを意味する`ReadResult::Fail`とはここが違います。
|
||||||
|
|
||||||
|
1本の接続を1つのスレッドで双方向に扱えるのはこのためです。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_read_timeout(100ms);
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
flush_outgoing(ws); // 何も届いていない。溜まっている分を送る
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
読み取りタイムアウトを設定しないと`read()`はメッセージが届くまで戻らないので、接続を持っているスレッドは送信に手が回りません。
|
||||||
|
|
||||||
|
`Timeout`について2点あります。
|
||||||
|
|
||||||
|
- `msg`は書き換えられません。値も0以外なので、読み取りタイムアウトを設定した状態で`while (ws.read(msg))`と書くと、**前回のメッセージ**が`msg`に残ったままループが回り続けます。
|
||||||
|
- 報告されるのはメッセージの境界だけです。分割されたメッセージの途中でタイムアウトした場合、そのメッセージは再開できないので`read()`は`Fail`を返します。
|
||||||
|
|
||||||
|
通知の待受のように長時間メッセージが来ないことが正常な接続では、読み取りタイムアウトを設定しないままにするか、`Timeout`を「まだ何も来ていない」印として扱ってループを続けてください。
|
||||||
|
|
||||||
|
## サーバ側
|
||||||
|
|
||||||
|
ハンドラが受け取る`ws::WebSocket`にも`set_read_timeout()`があります。ハンドラが`read()`で止まったままにならないので、上と同じ書き方で複数の接続の間をメッセージが中継できます。
|
||||||
|
|
||||||
|
サーバ側のデフォルトは「無期限」ではなく300秒(`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`)です。WebSocketのハンドラは接続が続く限りワーカーを1つ占有するので、無言になったピアからワーカーを回収する保険として働きます。
|
||||||
|
|
||||||
|
この保険はハンドラが求めたタイムアウトではないので、`Timeout`としては返りません。経過すると`read()`は`Fail`を返して接続を閉じるため、`while (ws.read(msg))`と書いたハンドラは従来どおりそこで終わります。`Timeout`が返るのは、ハンドラ自身が`set_read_timeout()`で設定したタイムアウトだけです。
|
||||||
|
|
||||||
> Ping/Pongによる無応答ピア検出は別の仕組みです。詳しくは[W02. ハートビートを設定する](../w02-websocket-ping)を参照してください。
|
> Ping/Pongによる無応答ピア検出は別の仕組みです。詳しくは[W02. ハートビートを設定する](../w02-websocket-ping)を参照してください。
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ハンドラー内のチェックは、ハンドシェイクが完了した後に行われます。アップグレードする前に401などのHTTPステータスで接続を拒否したい場合は、`set_pre_request_handler()`を使ってください。WebSocketのルートでも呼ばれます。詳しくは[S11. Pre-request handlerでルート単位の認証を行う](../../cookbook/s11-pre-request)を参照してください。
|
||||||
|
|
||||||
## WSSで使う
|
## WSSで使う
|
||||||
|
|
||||||
HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。
|
HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。
|
||||||
|
|||||||
+7
-3
@@ -164,14 +164,18 @@ if [ "$DRY_RUN" -eq 1 ]; then
|
|||||||
echo "==> Dry run complete. No changes were made."
|
echo "==> Dry run complete. No changes were made."
|
||||||
else
|
else
|
||||||
echo "==> Updating httplib.h..."
|
echo "==> Updating httplib.h..."
|
||||||
sed -i '' "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
# `-i.bak` is the in-place form GNU and BSD sed both accept (`-i ''` is
|
||||||
sed -i '' "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
# BSD-only: GNU sed reads the '' as the script).
|
||||||
|
sed -i.bak "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
||||||
|
sed -i.bak "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
||||||
|
rm -f httplib.h.bak
|
||||||
echo " CPPHTTPLIB_VERSION = \"$NEW_VERSION\""
|
echo " CPPHTTPLIB_VERSION = \"$NEW_VERSION\""
|
||||||
echo " CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
|
echo " CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Updating docs-src/config.toml..."
|
echo "==> Updating docs-src/config.toml..."
|
||||||
sed -i '' "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
|
sed -i.bak "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
|
||||||
|
rm -f docs-src/config.toml.bak
|
||||||
echo " version = \"$NEW_VERSION\""
|
echo " version = \"$NEW_VERSION\""
|
||||||
|
|
||||||
# --- Step 6: Commit, tag, and push ---
|
# --- Step 6: Commit, tag, and push ---
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
CXX = clang++
|
CXX = clang++
|
||||||
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow $(EXTRA_CXXFLAGS) -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO -fsanitize=address # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS
|
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO -fsanitize=address $(EXTRA_CXXFLAGS) # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS
|
||||||
|
|
||||||
ifneq ($(OS), Windows_NT)
|
ifneq ($(OS), Windows_NT)
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
|
|||||||
@@ -18,3 +18,24 @@ services:
|
|||||||
context: ./
|
context: ./
|
||||||
args:
|
args:
|
||||||
auth: digest
|
auth: digest
|
||||||
|
|
||||||
|
# Self-hosted stand-in for the httpbin.org-style auth-testing endpoints
|
||||||
|
# (/basic-auth, /digest-auth) that BaseAuthTest/DigestAuthTest exercise
|
||||||
|
# through the proxies above, so those tests don't depend on an external
|
||||||
|
# site's uptime.
|
||||||
|
httpbin_backend:
|
||||||
|
image: mccutchen/go-httpbin:latest
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# TLS termination in front of httpbin_backend (which only speaks plain
|
||||||
|
# HTTP) so the SSL variants of those tests can CONNECT-tunnel through the
|
||||||
|
# proxies to "httpbin" on port 443, same as the NoSSL variants do on 80.
|
||||||
|
httpbin:
|
||||||
|
image: nginx:alpine
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- httpbin_backend
|
||||||
|
volumes:
|
||||||
|
- ./httpbin_nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ../cert.pem:/etc/nginx/certs/cert.pem:ro
|
||||||
|
- ../key.pem:/etc/nginx/certs/key.pem:ro
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name httpbin;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://httpbin_backend:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name httpbin;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://httpbin_backend:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1934
-296
File diff suppressed because it is too large
Load Diff
+36
-25
@@ -22,23 +22,25 @@ template <typename T> void ProxyTest(T &cli, bool basic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(ProxyTest, NoSSLBasic) {
|
TEST(ProxyTest, NoSSLBasic) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
ProxyTest(cli, true);
|
ProxyTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(ProxyTest, SSLBasic) {
|
TEST(ProxyTest, SSLBasic) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
ProxyTest(cli, true);
|
ProxyTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ProxyTest, NoSSLDigest) {
|
TEST(ProxyTest, NoSSLDigest) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
ProxyTest(cli, false);
|
ProxyTest(cli, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ProxyTest, SSLDigest) {
|
TEST(ProxyTest, SSLDigest) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
ProxyTest(cli, false);
|
ProxyTest(cli, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -63,23 +65,25 @@ void RedirectProxyText(T &cli, const char *path, bool basic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
RedirectProxyText(cli, "/redirect/2", true);
|
RedirectProxyText(cli, "/redirect/2", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
RedirectProxyText(cli, "/redirect/2", false);
|
RedirectProxyText(cli, "/redirect/2", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, HTTPBinSSLBasic) {
|
TEST(RedirectTest, HTTPBinSSLBasic) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
RedirectProxyText(cli, "/redirect/2", true);
|
RedirectProxyText(cli, "/redirect/2", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, HTTPBinSSLDigest) {
|
TEST(RedirectTest, HTTPBinSSLDigest) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
RedirectProxyText(cli, "/redirect/2", false);
|
RedirectProxyText(cli, "/redirect/2", false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -173,7 +177,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|||||||
cli.Get("/basic-auth/hello/world",
|
cli.Get("/basic-auth/hello/world",
|
||||||
Headers{make_basic_authentication_header("hello", "world")});
|
Headers{make_basic_authentication_header("hello", "world")});
|
||||||
ASSERT_TRUE(res != nullptr);
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||||
|
"\"authorized\":true}\n"),
|
||||||
normalizeJson(res->body));
|
normalizeJson(res->body));
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
@@ -182,7 +187,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|||||||
cli.set_basic_auth("hello", "world");
|
cli.set_basic_auth("hello", "world");
|
||||||
auto res = cli.Get("/basic-auth/hello/world");
|
auto res = cli.Get("/basic-auth/hello/world");
|
||||||
ASSERT_TRUE(res != nullptr);
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||||
|
"\"authorized\":true}\n"),
|
||||||
normalizeJson(res->body));
|
normalizeJson(res->body));
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
@@ -203,13 +209,14 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(BaseAuthTest, NoSSL) {
|
TEST(BaseAuthTest, NoSSL) {
|
||||||
Client cli("httpcan.org");
|
Client cli("httpbin");
|
||||||
BaseAuthTestFromHTTPWatch(cli);
|
BaseAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(BaseAuthTest, SSL) {
|
TEST(BaseAuthTest, SSL) {
|
||||||
SSLClient cli("httpcan.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
BaseAuthTestFromHTTPWatch(cli);
|
BaseAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -228,21 +235,21 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// go-httpbin (the "httpbin" test double) only implements MD5 and
|
||||||
|
// SHA-256 for digest auth, so SHA-256 is as far as this can exercise
|
||||||
|
// the client's digest-auth algorithm selection end-to-end.
|
||||||
std::vector<std::string> paths = {
|
std::vector<std::string> paths = {
|
||||||
"/digest-auth/auth/hello/world/MD5",
|
"/digest-auth/auth/hello/world/MD5",
|
||||||
"/digest-auth/auth/hello/world/SHA-256",
|
"/digest-auth/auth/hello/world/SHA-256",
|
||||||
"/digest-auth/auth/hello/world/SHA-512",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cli.set_digest_auth("hello", "world");
|
cli.set_digest_auth("hello", "world");
|
||||||
for (auto path : paths) {
|
for (auto path : paths) {
|
||||||
auto res = cli.Get(path.c_str());
|
auto res = cli.Get(path.c_str());
|
||||||
ASSERT_TRUE(res != nullptr);
|
ASSERT_TRUE(res != nullptr);
|
||||||
std::string algo(path.substr(path.rfind('/') + 1));
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||||
EXPECT_EQ(
|
"\"authorized\":true}\n"),
|
||||||
normalizeJson("{\"algorithm\":\"" + algo +
|
normalizeJson(res->body));
|
||||||
"\",\"authenticated\":true,\"user\":\"hello\"}\n"),
|
|
||||||
normalizeJson(res->body));
|
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,12 +270,13 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(DigestAuthTest, SSL) {
|
TEST(DigestAuthTest, SSL) {
|
||||||
SSLClient cli("httpcan.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
DigestAuthTestFromHTTPWatch(cli);
|
DigestAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DigestAuthTest, NoSSL) {
|
TEST(DigestAuthTest, NoSSL) {
|
||||||
Client cli("httpcan.org");
|
Client cli("httpbin");
|
||||||
DigestAuthTestFromHTTPWatch(cli);
|
DigestAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -329,22 +337,24 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
|||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(KeepAliveTest, NoSSLWithBasic) {
|
TEST(KeepAliveTest, NoSSLWithBasic) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
KeepAliveTest(cli, true);
|
KeepAliveTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeepAliveTest, SSLWithBasic) {
|
TEST(KeepAliveTest, SSLWithBasic) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
KeepAliveTest(cli, true);
|
KeepAliveTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeepAliveTest, NoSSLWithDigest) {
|
TEST(KeepAliveTest, NoSSLWithDigest) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
KeepAliveTest(cli, false);
|
KeepAliveTest(cli, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeepAliveTest, SSLWithDigest) {
|
TEST(KeepAliveTest, SSLWithDigest) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
KeepAliveTest(cli, false);
|
KeepAliveTest(cli, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -353,7 +363,8 @@ TEST(KeepAliveTest, SSLWithDigest) {
|
|||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(ProxyTest, SSLOpenStream) {
|
TEST(ProxyTest, SSLOpenStream) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
cli.set_proxy("localhost", 3128);
|
cli.set_proxy("localhost", 3128);
|
||||||
cli.set_proxy_basic_auth("hello", "world");
|
cli.set_proxy_basic_auth("hello", "world");
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
// without waiting 30 seconds.
|
// without waiting 30 seconds.
|
||||||
|
|
||||||
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
||||||
#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 3
|
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 3
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 3
|
||||||
#include <httplib.h>
|
#include <httplib.h>
|
||||||
|
|
||||||
#include "gtest/gtest.h"
|
#include "gtest/gtest.h"
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
|
||||||
using namespace httplib;
|
using namespace httplib;
|
||||||
|
|
||||||
class WebSocketHeartbeatTest : public ::testing::Test {
|
class WebSocketHeartbeatTest : public ::testing::Test {
|
||||||
@@ -191,6 +194,76 @@ TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
|
|||||||
EXPECT_FALSE(client.is_open());
|
EXPECT_FALSE(client.is_open());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The compile-time client read timeout (3s here) was never asked for through
|
||||||
|
// set_read_timeout(), so when it elapses read() reports Fail and closes the
|
||||||
|
// connection rather than handing back a Timeout on a still-open one.
|
||||||
|
TEST_F(WebSocketPongTimeoutTest, CompileTimeClientReadTimeoutIsFail) {
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||||
|
client.set_websocket_ping_interval(0);
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
// Server pings are off and its handler never sends, so nothing arrives.
|
||||||
|
std::string msg;
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Fail);
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The compile-time server read timeout (3s here) is a backstop that reclaims
|
||||||
|
// the worker from a peer gone quiet, not a timeout the handler asked for. When
|
||||||
|
// it elapses read() must return Fail, so a handler written as
|
||||||
|
// `while (ws.read(msg))` ends instead of re-running its body with the previous
|
||||||
|
// message still in `msg`.
|
||||||
|
class WebSocketServerReadTimeoutTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
svr_.set_websocket_ping_interval(0);
|
||||||
|
svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &ws) {
|
||||||
|
std::string msg;
|
||||||
|
while (ws.read(msg)) {
|
||||||
|
iterations_++;
|
||||||
|
ws.send(msg);
|
||||||
|
}
|
||||||
|
handler_done_.set_value();
|
||||||
|
});
|
||||||
|
|
||||||
|
port_ = svr_.bind_to_any_port("localhost");
|
||||||
|
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
|
||||||
|
svr_.wait_until_ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
svr_.stop();
|
||||||
|
thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
Server svr_;
|
||||||
|
int port_;
|
||||||
|
std::thread thread_;
|
||||||
|
std::atomic<int> iterations_{0};
|
||||||
|
std::promise<void> handler_done_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(WebSocketServerReadTimeoutTest, BackstopEndsHandlerLoop) {
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||||
|
client.set_websocket_ping_interval(0); // nothing reaches the server's read()
|
||||||
|
client.set_read_timeout(10, 0); // fail rather than hang
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
ASSERT_TRUE(client.send("hello"));
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_EQ(client.read(msg), ws::Text);
|
||||||
|
EXPECT_EQ("hello", msg);
|
||||||
|
|
||||||
|
// The client now stays silent. The server's backstop elapses and the
|
||||||
|
// handler returns, having run its loop body exactly once.
|
||||||
|
auto done = handler_done_.get_future();
|
||||||
|
ASSERT_EQ(done.wait_for(std::chrono::seconds(6)), std::future_status::ready);
|
||||||
|
EXPECT_EQ(1, iterations_.load());
|
||||||
|
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Fail);
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
|
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
|
||||||
// even with a small max_missed_pongs budget. This is the positive counterpart
|
// even with a small max_missed_pongs budget. This is the positive counterpart
|
||||||
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
||||||
|
|||||||
Reference in New Issue
Block a user