mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-09-30 20:52:31 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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:
|
||||
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
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install clang-format
|
||||
run: |
|
||||
brew update
|
||||
brew install clang-format coreutils
|
||||
- name: run style check
|
||||
run: |
|
||||
clang-format --version
|
||||
@@ -475,7 +482,6 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
@@ -585,7 +591,6 @@ jobs:
|
||||
- name: Build ${{ matrix.config.name }}
|
||||
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
|
||||
- name: Run tests ${{ matrix.config.name }}
|
||||
id: run_tests
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
shell: pwsh
|
||||
working-directory: build/test
|
||||
@@ -618,34 +623,6 @@ jobs:
|
||||
}
|
||||
if ($failed) { exit 1 }
|
||||
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:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v18.1.8 # 最新バージョンを使用
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: clang-format
|
||||
files: \.(cpp|cc|h)$
|
||||
args: [-i] # インプレースで修正
|
||||
name: clang-format
|
||||
# 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]
|
||||
|
||||
+34
-5
@@ -57,14 +57,30 @@ if (ws.connect()) {
|
||||
|
||||
```cpp
|
||||
enum ReadResult : int {
|
||||
Fail = 0, // Connection closed or error
|
||||
Text = 1, // UTF-8 text message
|
||||
Binary = 2, // Binary message
|
||||
Fail = 0, // Connection closed or error
|
||||
Text = 1, // UTF-8 text 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.
|
||||
|
||||
`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
|
||||
|
||||
```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
|
||||
|
||||
```cpp
|
||||
@@ -409,7 +437,8 @@ if (ws.connect()) {
|
||||
| Macro | Default | Description |
|
||||
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
||||
| `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_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 |
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -347,6 +347,25 @@ int port = svr.bind_to_any_port("0.0.0.0");
|
||||
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
|
||||
|
||||
```cpp
|
||||
@@ -545,14 +564,15 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
||||
|
||||
```
|
||||
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
|
||||
│ └─ returns Handled → stop here
|
||||
│
|
||||
├─ 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
|
||||
│
|
||||
├─ pre_request_handler route matched, body NOT read yet
|
||||
@@ -568,6 +588,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.
|
||||
|
||||
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
|
||||
|
||||
`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 +851,9 @@ svr.Get("/content", [&](const Request &req, Response &res) {
|
||||
|
||||
### '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
|
||||
// Send a '417 Expectation Failed' response.
|
||||
@@ -1474,6 +1500,19 @@ The server can apply compression to the following MIME type contents:
|
||||
- application/protobuf
|
||||
- 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.54.1"
|
||||
version = "0.58.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
Same as pre-routing — return `HandlerResponse`.
|
||||
|
||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
||||
|
||||
## 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
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
`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()`.
|
||||
|
||||
@@ -36,6 +36,7 @@ The `read()` return value is a `ReadResult` enum:
|
||||
- `ReadResult::Text`: received a text message
|
||||
- `ReadResult::Binary`: received a binary message
|
||||
- `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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -42,6 +42,9 @@ switch (result) {
|
||||
case httplib::ws::ReadResult::Fail:
|
||||
// error or closed
|
||||
break;
|
||||
case httplib::ws::ReadResult::Timeout:
|
||||
// read timeout elapsed; the connection is still open
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ status: "draft"
|
||||
| Kind | API | Default |
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -40,9 +40,44 @@ ws.set_read_timeout(30s);
|
||||
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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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や名前に左右されません。
|
||||
|
||||
`svr.WebSocket()`で登録したルートでも、Pre-requestハンドラは呼ばれます。呼ばれるのは`101 Switching Protocols`を返す前なので、`Handled`を返すとそのHTTPレスポンス(403など)がそのまま返り、WebSocketへのアップグレードは行われません。
|
||||
|
||||
## 戻り値の意味
|
||||
|
||||
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
||||
|
||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
||||
|
||||
## 戻り値のチェック
|
||||
|
||||
`bind_to_port()`は失敗すると`false`を返します。ポートが既に使われている場合などです。必ずチェックしてください。
|
||||
`bind_to_port()`は失敗すると`false`を返します。ポートにbindする権限が無い場合などです。必ずチェックしてください。
|
||||
|
||||
```cpp
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
`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()`の組み合わせです。
|
||||
|
||||
@@ -31,11 +31,12 @@ int main() {
|
||||
|
||||
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
||||
|
||||
`read()`の返り値は`ReadResult`列挙値で、次の3種類です。
|
||||
`read()`の返り値は`ReadResult`列挙値で、次の4種類です。
|
||||
|
||||
- `ReadResult::Text`: テキストメッセージを受信
|
||||
- `ReadResult::Binary`: バイナリメッセージを受信
|
||||
- `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`以上を設定してください。
|
||||
|
||||
ただし`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)を参照してください。
|
||||
|
||||
@@ -42,6 +42,9 @@ switch (result) {
|
||||
case httplib::ws::ReadResult::Fail:
|
||||
// エラーまたは切断
|
||||
break;
|
||||
case httplib::ws::ReadResult::Timeout:
|
||||
// 読み取りタイムアウト。接続は開いたまま
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ status: "draft"
|
||||
| 種類 | API | デフォルト |
|
||||
| --- | --- | --- |
|
||||
| 接続タイムアウト | `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秒 |
|
||||
|
||||
## 基本の使い方
|
||||
@@ -26,7 +26,7 @@ if (ws.connect()) {
|
||||
}
|
||||
```
|
||||
|
||||
`connect()`を呼ぶ前に設定してください。
|
||||
接続タイムアウトと書き込みタイムアウトは`connect()`を呼ぶ前に設定してください。読み取りタイムアウトはいつでも変更でき、接続済みの状態で設定した場合は次の`read()`から効きます。
|
||||
|
||||
## `std::chrono`で指定する
|
||||
|
||||
@@ -40,9 +40,44 @@ ws.set_read_timeout(30s);
|
||||
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)を参照してください。
|
||||
|
||||
|
||||
@@ -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で使う
|
||||
|
||||
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."
|
||||
else
|
||||
echo "==> Updating httplib.h..."
|
||||
sed -i '' "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
||||
sed -i '' "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
||||
# `-i.bak` is the in-place form GNU and BSD sed both accept (`-i ''` is
|
||||
# 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_NUM = \"$VERSION_HEX\""
|
||||
|
||||
echo ""
|
||||
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\""
|
||||
|
||||
# --- Step 6: Commit, tag, and push ---
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
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)
|
||||
UNAME_S := $(shell uname -s)
|
||||
|
||||
@@ -18,3 +18,24 @@ services:
|
||||
context: ./
|
||||
args:
|
||||
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;
|
||||
}
|
||||
}
|
||||
+1129
-30
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) {
|
||||
Client cli("httpbingo.org");
|
||||
Client cli("httpbin");
|
||||
ProxyTest(cli, true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(ProxyTest, SSLBasic) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
ProxyTest(cli, true);
|
||||
}
|
||||
|
||||
TEST(ProxyTest, NoSSLDigest) {
|
||||
Client cli("httpbingo.org");
|
||||
Client cli("httpbin");
|
||||
ProxyTest(cli, false);
|
||||
}
|
||||
|
||||
TEST(ProxyTest, SSLDigest) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
ProxyTest(cli, false);
|
||||
}
|
||||
#endif
|
||||
@@ -63,23 +65,25 @@ void RedirectProxyText(T &cli, const char *path, bool basic) {
|
||||
}
|
||||
|
||||
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
||||
Client cli("httpbingo.org");
|
||||
Client cli("httpbin");
|
||||
RedirectProxyText(cli, "/redirect/2", true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
||||
Client cli("httpbingo.org");
|
||||
Client cli("httpbin");
|
||||
RedirectProxyText(cli, "/redirect/2", false);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, HTTPBinSSLBasic) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
RedirectProxyText(cli, "/redirect/2", true);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, HTTPBinSSLDigest) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
RedirectProxyText(cli, "/redirect/2", false);
|
||||
}
|
||||
#endif
|
||||
@@ -173,7 +177,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.Get("/basic-auth/hello/world",
|
||||
Headers{make_basic_authentication_header("hello", "world")});
|
||||
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));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
@@ -182,7 +187,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_basic_auth("hello", "world");
|
||||
auto res = cli.Get("/basic-auth/hello/world");
|
||||
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));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
@@ -203,13 +209,14 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
}
|
||||
|
||||
TEST(BaseAuthTest, NoSSL) {
|
||||
Client cli("httpcan.org");
|
||||
Client cli("httpbin");
|
||||
BaseAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(BaseAuthTest, SSL) {
|
||||
SSLClient cli("httpcan.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
BaseAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
#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 = {
|
||||
"/digest-auth/auth/hello/world/MD5",
|
||||
"/digest-auth/auth/hello/world/SHA-256",
|
||||
"/digest-auth/auth/hello/world/SHA-512",
|
||||
};
|
||||
|
||||
cli.set_digest_auth("hello", "world");
|
||||
for (auto path : paths) {
|
||||
auto res = cli.Get(path.c_str());
|
||||
ASSERT_TRUE(res != nullptr);
|
||||
std::string algo(path.substr(path.rfind('/') + 1));
|
||||
EXPECT_EQ(
|
||||
normalizeJson("{\"algorithm\":\"" + algo +
|
||||
"\",\"authenticated\":true,\"user\":\"hello\"}\n"),
|
||||
normalizeJson(res->body));
|
||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||
"\"authorized\":true}\n"),
|
||||
normalizeJson(res->body));
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
@@ -263,12 +270,13 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
||||
}
|
||||
|
||||
TEST(DigestAuthTest, SSL) {
|
||||
SSLClient cli("httpcan.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
DigestAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
|
||||
TEST(DigestAuthTest, NoSSL) {
|
||||
Client cli("httpcan.org");
|
||||
Client cli("httpbin");
|
||||
DigestAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
#endif
|
||||
@@ -329,22 +337,24 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(KeepAliveTest, NoSSLWithBasic) {
|
||||
Client cli("httpbingo.org");
|
||||
Client cli("httpbin");
|
||||
KeepAliveTest(cli, true);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, SSLWithBasic) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
KeepAliveTest(cli, true);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, NoSSLWithDigest) {
|
||||
Client cli("httpbingo.org");
|
||||
Client cli("httpbin");
|
||||
KeepAliveTest(cli, false);
|
||||
}
|
||||
|
||||
TEST(KeepAliveTest, SSLWithDigest) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
KeepAliveTest(cli, false);
|
||||
}
|
||||
#endif
|
||||
@@ -353,7 +363,8 @@ TEST(KeepAliveTest, SSLWithDigest) {
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(ProxyTest, SSLOpenStream) {
|
||||
SSLClient cli("httpbingo.org");
|
||||
SSLClient cli("httpbin");
|
||||
cli.enable_server_certificate_verification(false);
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
// without waiting 30 seconds.
|
||||
|
||||
#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 "gtest/gtest.h"
|
||||
|
||||
#include <future>
|
||||
|
||||
using namespace httplib;
|
||||
|
||||
class WebSocketHeartbeatTest : public ::testing::Test {
|
||||
@@ -191,6 +194,76 @@ TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
|
||||
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,
|
||||
// even with a small max_missed_pongs budget. This is the positive counterpart
|
||||
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
||||
|
||||
Reference in New Issue
Block a user