Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
70afdbe
add client
kekxv Dec 14, 2025
23d5dc7
add client
kekxv Dec 14, 2025
8eb6b59
test(framework): add websocket client tests and real http tests
kekxv Dec 14, 2025
96bdde1
add client
kekxv Dec 14, 2025
4812f65
feat: 增加`io_context_pool.hpp`管理io,增加client模式
kekxv Dec 14, 2025
2f58423
fix: windows macros build
kekxv Dec 14, 2025
543827b
test
kekxv Dec 14, 2025
204d0e3
test
kekxv Dec 14, 2025
b3d905a
fix: windows
kekxv Dec 14, 2025
dbaa4b7
feat: add TagInvoke.hpp
kekxv Dec 14, 2025
dc5e867
feat: add cron
kekxv Dec 14, 2025
47ad116
feat: add cron
kekxv Dec 14, 2025
0c2fdaf
readme
kekxv May 8, 2026
15858ab
完善以及测试用例
kekxv May 8, 2026
a2da17f
doc
kekxv May 8, 2026
75e9214
fix
kekxv May 9, 2026
ba926cc
API CALL
kekxv May 9, 2026
7aff4f2
API CALL
kekxv May 9, 2026
b71bb79
publish_to_bcr.yml
kekxv May 9, 2026
fbe2289
Merge branch 'main' into develop
kekxv May 9, 2026
7a1bd41
fix: ubuntu ci
kekxv May 9, 2026
6509c37
Merge branch 'ClangTools:main' into main
kekxv May 9, 2026
44baa70
update: version
kekxv Jun 24, 2026
d178396
Merge remote-tracking branch 'origin/main'
kekxv Jun 24, 2026
eb13e03
fix: 隐藏的问题修复
kekxv Jun 25, 2026
f84f896
fix: 隐藏的问题修复
kekxv Jun 25, 2026
722f788
fix: 隐藏的问题修复
kekxv Jun 25, 2026
49add75
fix: 隐藏的问题修复 - 1
kekxv Jun 25, 2026
d3a4b43
fix: 隐藏的问题修复 - 2
kekxv Jun 25, 2026
b97503f
fix: 修改日志为spdlog
kekxv Jun 25, 2026
96e8386
fix: windows ci/cd
kekxv Jun 25, 2026
a654fb8
fix: windows ci/cd
kekxv Jun 25, 2026
e1c9676
fix: windows ci/cd
kekxv Jun 25, 2026
a38e6c5
fix: windows ci/cd
kekxv Jun 25, 2026
23ff4b9
fix: Linux ci/cd
kekxv Jun 25, 2026
0d3e5b4
fix: Linux ci/cd
kekxv Jun 25, 2026
08cf472
fix: Linux ci/cd
kekxv Jun 25, 2026
a451d10
fix: Linux ci/cd
kekxv Jun 25, 2026
e4b9144
update: version
kekxv Jun 25, 2026
d9cdc63
update: version
kekxv Aug 9, 2026
4760acb
feat: add WebSocket dynamic routing and handshake context access
kekxv Aug 9, 2026
7d0051b
feat: add streaming HTTP client and proxy session
kekxv Aug 9, 2026
afb8c95
feat: add HTTPS support and WebSocket subprotocol negotiation
kekxv Aug 10, 2026
0cac2f6
feat: harden HTTP proxy and async auth handling
kekxv Aug 11, 2026
d6ffb16
Merge ClangTools main history after squash
kekxv Aug 11, 2026
e23d17f
update: refresh README and quick-start with khttpd 0.3.0 dependencies
kekxv Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .bcr/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,26 @@ module(
name = "khttpd",
version = "{{VERSION}}",
)

bazel_dep(name = "platforms", version = "1.1.0")
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "rules_cc", version = "0.2.20")
bazel_dep(name = "rules_shell", version = "0.8.0")
bazel_dep(name = "rules_perl", version = "1.1.1")
bazel_dep(name = "fmt", version = "12.1.0")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "sqlite3", version = "3.53.2")
bazel_dep(name = "openssl", version = "4.0.1.bcr.0")
bazel_dep(name = "boringssl", version = "0.20260616.0")
bazel_dep(name = "boost", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.asio", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.json", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.mysql", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.beast", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.filesystem", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.url", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.uuid", version = "1.90.0.bcr.1")
bazel_dep(name = "spdlog", version = "1.17.0")

cc_configure = use_extension("@rules_cc//cc:extensions.bzl", "cc_configure_extension")
use_repo(cc_configure, "local_config_cc")
17 changes: 15 additions & 2 deletions .github/workflows/bazel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:

- name: Bazel Test
run: |
bazel test framework/... --test_output=errors --test_verbose_timeout_warnings --verbose_failures
bazel test //framework/... --test_output=errors --test_verbose_timeout_warnings --verbose_failures
example:
needs: build
strategy:
Expand Down Expand Up @@ -108,7 +108,10 @@ jobs:
release:
needs: [build, example]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.repository == 'ClangTools/khttpd' && github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
tag_name: ${{ steps.version.outputs.tag_name }}
release_created: ${{ steps.check_tag.outputs.exists == 'false' }}
permissions:
contents: write
steps:
Expand All @@ -122,6 +125,7 @@ jobs:
run: |
VERSION=$(grep -oP 'version\s*=\s*"\K[^"]+' MODULE.bazel | head -1)
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag_name=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "Found version: ${VERSION}"

- name: Check if tag exists
Expand Down Expand Up @@ -153,3 +157,12 @@ jobs:
name: Release v${{ steps.version.outputs.version }}
generate_release_notes: true
draft: false

publish_to_bcr:
name: Publish release to BCR
needs: release
if: github.repository == 'ClangTools/khttpd' && needs.release.outputs.release_created == 'true'
uses: ./.github/workflows/publish_to_bcr.yml
with:
tag_name: ${{ needs.release.outputs.tag_name }}
secrets: inherit
12 changes: 11 additions & 1 deletion .github/workflows/publish_to_bcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@ name: Publish to BCR
on:
release:
types: [published]
workflow_call:
inputs:
tag_name:
description: Release tag to publish to the Bazel Central Registry
required: true
type: string
secrets:
BCR_PUBLISH_TOKEN:
required: true

jobs:
publish:
if: github.repository == 'ClangTools/khttpd'
uses: kekxv/bcr/.github/workflows/publish_to_bcr.yml@publish-to-bcr
with:
tag_name: ${{ github.event.release.tag_name }}
tag_name: ${{ inputs.tag_name || github.event.release.tag_name }}
module_name: "khttpd"
secrets:
publish_token: ${{ secrets.BCR_PUBLISH_TOKEN }}
4 changes: 2 additions & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module(
name = "khttpd",
version = "0.2.0",
version = "0.3.0",
)

bazel_dep(name = "platforms", version = "1.1.0")
Expand All @@ -11,7 +11,7 @@ bazel_dep(name = "rules_perl", version = "1.1.1")
bazel_dep(name = "fmt", version = "12.1.0")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "sqlite3", version = "3.53.2")
bazel_dep(name = "openssl", version = "3.5.5.bcr.4")
bazel_dep(name = "openssl", version = "4.0.1.bcr.0")
bazel_dep(name = "boringssl", version = "0.20260616.0")
bazel_dep(name = "boost", version = "1.90.0.bcr.1")
bazel_dep(name = "boost.asio", version = "1.90.0.bcr.1")
Expand Down
105 changes: 83 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and [Boost.Asio](https://www.boost.org/doc/libs/release/libs/asio/), managed wit
## Features

- **HTTP Server** — Multi-threaded, async I/O server powered by Boost.Asio strand-based concurrency
- **WebSocket Support** — Full WebSocket lifecycle management (onopen / onmessage / onclose / onerror)
- **WebSocket Support** — Dynamic routes, handshake metadata, typed text/binary/control frames, and full lifecycle management
- **Routing** — Express-style route registration with path parameters (`/users/:id`), query params, and method
specificity sorting
- **Controller Pattern** — CRTP-based `BaseController` with `KHTTPD_ROUTE` / `KHTTPD_WSROUTE` macros for clean route
Expand All @@ -20,6 +20,7 @@ and [Boost.Asio](https://www.boost.org/doc/libs/release/libs/asio/), managed wit
- **Interceptors** — Pre-request / post-response middleware pipeline
- **Exception Handling** — Type-safe exception dispatcher with per-type handlers
- **Chunked Streaming** — Server-sent chunked transfer encoding via `HttpContext::chunked()`
- **Bidirectional HTTP Streaming** — Header-first request routing, fixed-buffer upload/download, proxy backpressure, and Range forwarding
- **Cookie Support** — Read / write cookies with configurable `CookieOptions` (path, domain, SameSite, etc.)
- **Form & Multipart** — `application/x-www-form-urlencoded` and `multipart/form-data` parsing (file uploads)
- **JSON** — Native `boost::json` integration with `get_json()`, `set_body_json()`, `set_body_from()`
Expand All @@ -32,12 +33,12 @@ and [Boost.Asio](https://www.boost.org/doc/libs/release/libs/asio/), managed wit

| Component | Version |
|---------------------|----------------|
| Boost | 1.89.0 |
| Boost.Beast | 1.89.0 |
| Boost.Asio | 1.89.0 |
| fmt | 12.0.0 |
| OpenSSL / BoringSSL | 3.3.1 / latest |
| SQLite3 | 3.50.4 |
| Boost | 1.90.0 |
| Boost.Beast | 1.90.0 |
| Boost.Asio | 1.90.0 |
| fmt | 12.1.0 |
| OpenSSL / BoringSSL | 4.0.1 / 0.20260616.0 |
| SQLite3 | 3.53.2 |
| Build System | Bazel (bzlmod) |

## Quick Start
Expand All @@ -49,21 +50,23 @@ In your project's `MODULE.bazel`:
```python
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

bazel_dep(name="platforms", version="1.0.0")
bazel_dep(name="rules_cc", version="0.2.13")
bazel_dep(name="fmt", version="12.0.0")
bazel_dep(name="boost", version="1.89.0.bcr.2")
bazel_dep(name="boost.asio", version="1.89.0.bcr.2")
bazel_dep(name="boost.beast", version="1.89.0.bcr.2")
bazel_dep(name="boost.json", version="1.89.0.bcr.2")
bazel_dep(name="boost.filesystem", version="1.89.0.bcr.2")
bazel_dep(name="boost.url", version="1.89.0.bcr.2")
bazel_dep(name="boringssl", version="0.20251110.0")
bazel_dep(name="platforms", version="1.1.0")
bazel_dep(name="rules_cc", version="0.2.20")
bazel_dep(name="fmt", version="12.1.0")
bazel_dep(name="boost", version="1.90.0.bcr.1")
bazel_dep(name="boost.asio", version="1.90.0.bcr.1")
bazel_dep(name="boost.beast", version="1.90.0.bcr.1")
bazel_dep(name="boost.json", version="1.90.0.bcr.1")
bazel_dep(name="boost.filesystem", version="1.90.0.bcr.1")
bazel_dep(name="boost.url", version="1.90.0.bcr.1")
bazel_dep(name="boost.uuid", version="1.90.0.bcr.1")
bazel_dep(name="boringssl", version="0.20260616.0")
bazel_dep(name="spdlog", version="1.17.0")

http_archive(
name="khttpd",
strip_prefix="khttpd-0.1.0",
url="https://github.com/ClangTools/khttpd/archive/refs/tags/v0.1.0.tar.gz",
strip_prefix="khttpd-0.3.0",
url="https://github.com/ClangTools/khttpd/archive/refs/tags/v0.3.0.tar.gz",
)
```

Expand All @@ -87,6 +90,9 @@ int main() {

auto& router = server->get_http_router();

// Buffered JSON/form routes default to 16 MiB. Configure the limit in bytes.
server->set_max_buffered_request_body_size(32ULL * 1024 * 1024);

// Simple route
router.get("/hello", [](khttpd::framework::HttpContext& ctx) {
std::string name = ctx.get_query_param("name").value_or("World");
Expand Down Expand Up @@ -128,6 +134,8 @@ framework/
├── io_context_pool.hpp # Asio io_context thread pool
├── context/
│ ├── http_context.hpp/cpp # Request/response abstraction (params, body, cookies, streaming)
│ ├── http_request_stream.hpp # Fixed-buffer inbound request body
│ ├── http_response_stream.hpp # Fixed-buffer outbound response body
│ └── websocket_context.hpp/cpp # WebSocket session context (send, attributes)
├── router/
│ ├── http_router.hpp/cpp # Route matching, interceptors, exception dispatch
Expand All @@ -136,6 +144,8 @@ framework/
│ └── http_controller.hpp # CRTP BaseController + KHTTPD_ROUTE / KHTTPD_WSROUTE macros
├── client/
│ ├── http_client.hpp/cpp # Sync/async HTTP client with SSL
│ ├── http_client_stream.hpp/cpp # Fixed-buffer HTTP streaming client
│ ├── http_proxy_session.hpp/cpp # Bidirectional streaming proxy pump
│ └── websocket_client.hpp/cpp # WebSocket client
├── interceptor/
│ └── interceptor.hpp # Pre/Post middleware interface
Expand Down Expand Up @@ -184,9 +194,17 @@ framework/

```cpp
auto& ws = server->get_websocket_router();
ws.add_handler("/ws",
[](WebsocketContext& ctx) { /* onopen */ ctx.send("Welcome!"); },
[](WebsocketContext& ctx) { /* onmessage */ ctx.send("Echo: " + ctx.message, ctx.is_text); },
ws.add_handler("/gateway/:target",
[](WebsocketContext& ctx) {
auto target = ctx.get_path_param("target"); // may contain multiple path segments
auto token = ctx.get_header("Authorization");
auto trace = ctx.get_query_param("trace");
ctx.send("Welcome!");
},
[](WebsocketContext& ctx) {
// frame.type preserves text/binary; payload may contain arbitrary bytes.
ctx.send(ctx.frame);
},
[](WebsocketContext& ctx) { /* onclose */ },
[](WebsocketContext& ctx) { /* onerror */ }
);
Expand All @@ -212,6 +230,49 @@ class MyController : public khttpd::framework::BaseController<MyController> {
MyController::create()->register_routes(server->get_http_router());
```

### Streaming HTTP routes and proxying

Large request and response bodies can bypass `string_body` buffering by using a
stream route. Reads and writes are serialized through fixed-size buffers, so
backpressure is propagated between the downstream and upstream connections.

```cpp
router.stream("/gateway/upload", boost::beast::http::verb::post,
[](HttpContext& ctx,
std::shared_ptr<HttpRequestStream> request,
std::shared_ptr<HttpResponseStream> response,
HttpStreamComplete complete)
{
client::HttpClientStream::RequestHead head{
ctx.method(), "/upload", ctx.get_request().version()};
for (const auto& field : ctx.get_request())
head.insert(field.name_string(), field.value());

auto proxy = std::make_shared<client::HttpProxySession>(request, response);
proxy->start("http://upstream.internal/upload", std::move(head));
});
```

Normal routes remain buffered for JSON/form compatibility and reject request
bodies above the configurable limit (16 MiB by default; use
`Server::set_max_buffered_request_body_size(bytes)`). Stream routes are not
subject to this buffered-body limit and have no size-dependent allocation;
the proxy buffer defaults to 64 KiB and can be configured in its constructor.

`HttpClientStream` supports both `http://` and `https://` without changing its
fixed-buffer behavior. Its default TLS context verifies the system trust store;
an application can inject an `ssl::context` into `HttpClientStream` or
`HttpProxySession` for private CAs and test certificates.

### Tests

Run the complete framework suite, including buffered-body boundaries, streaming
edge cases, proxy cancellation, WebSocket dynamic routing, and frame fidelity:

```bash
bazel test //framework/... --test_output=errors
```

### Interceptors

```cpp
Expand Down
81 changes: 80 additions & 1 deletion doc/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ Request → Interceptor1.handle_request → Interceptor2.handle_request → Hand
- **前置拦截器**:按注册**正序**执行
- **后置拦截器**:按注册**逆序**执行(洋葱模型)
- 任一前置返回 `Stop` → 跳过剩余前置和 handler → 执行全部后置
- WebSocket Upgrade 在握手前也执行这条链,因此可复用 HTTP 鉴权

远程鉴权可覆盖异步入口,完成回调可以从任意线程调用,但必须恰好调用一次:

```cpp
void async_handle_request(HttpContext& ctx, RequestCompletion complete) override {
auth_client.check(ctx.get_header("Authorization"),
[&ctx, complete = std::move(complete)](bool allowed) mutable {
if (!allowed) {
ctx.set_status(http::status::unauthorized);
ctx.set_body("Unauthorized");
}
complete(allowed ? InterceptorResult::Continue : InterceptorResult::Stop);
});
}
```

做可信 `X-Forwarded-For` 解析时,应先用 `ctx.peer_endpoint()` 判断直连 peer 是否属于受信代理网段;IP 限流的默认 key 应使用 `ctx.peer_address()`,不能直接信任请求头。

### 上下文数据传递

Expand Down Expand Up @@ -122,9 +140,10 @@ router.set_unknown_exception_handler([](HttpContext& ctx) {
auto& ws_router = server->get_websocket_router();

ws_router.add_handler(
"/chat",
"/chat/:room",
// on_open
[](WebsocketContext& ctx) {
auto room = ctx.get_path_param("room").value_or("lobby");
ctx.send("Welcome to the chat!");
},
// on_message
Expand All @@ -142,6 +161,28 @@ ws_router.add_handler(
);
```

WebSocket 路由和 HTTP 路由一样支持动态参数。最后一个参数可以包含 `/`,因此 `/gateway/:target` 能匹配 `/gateway/orders/ws/v1`。静态路由始终优先于动态路由。

### 握手信息与帧类型

```cpp
ws_router.add_handler("/gateway/:target",
[](WebsocketContext& ctx) {
const auto& request = ctx.handshake(); // target/path/headers/query/subprotocols
auto authorization = ctx.get_header("Authorization");
auto cookies = ctx.get_headers("Cookie"); // 保留重复字段
auto trace = ctx.get_query_param("trace");
},
[](WebsocketContext& ctx) {
if (ctx.frame.type == WebsocketFrameType::binary) {
// payload 是原始字节,可包含 \0;不会转成文本帧。
ctx.send(ctx.frame);
}
});
```

`WebsocketFrame` 还可表示 ping、pong 和 close;close 帧包含 `close_code` 与 `close_reason`。

### 广播消息

```cpp
Expand Down Expand Up @@ -188,6 +229,8 @@ ChatController::create()->register_routes(ws_router);

## 分块流式响应

`HttpContext::chunked()` 适合服务端逐块生成响应,但请求体仍属于普通缓冲模型。需要流式上传、下载或代理时,应使用下一节的 `router.stream()`。

```cpp
router.get("/stream/:count", [](HttpContext& ctx) {
int count = std::stoi(ctx.get_path_param("count").value_or("10"));
Expand All @@ -209,6 +252,42 @@ router.get("/stream/:count", [](HttpContext& ctx) {

---

## 双向 HTTP 流与大文件代理

普通 JSON、form 和 multipart handler 需要完整请求体,默认最大 16 MiB。可以全局调整:

```cpp
server->set_max_buffered_request_body_size(32ULL * 1024 * 1024);
```

带 `Content-Length` 的超限请求会在读取 body、发送 `100 Continue` 之前返回 413;chunked 请求在累计越界时返回 413。大文件不要简单调高该上限,应注册流式路由:

```cpp
router.stream("/gateway/:target", http::verb::post,
[](HttpContext& ctx,
std::shared_ptr<HttpRequestStream> request,
std::shared_ptr<HttpResponseStream> response,
HttpStreamComplete)
{
client::HttpClientStream::RequestHead head{
ctx.method(), "/", ctx.get_request().version()};
for (const auto& field : ctx.get_request())
head.insert(field.name_string(), field.value());

auto proxy = std::make_shared<client::HttpProxySession>(
request, response, 64 * 1024);
proxy->start("http://upstream.internal/upload", std::move(head));
});
```

请求和响应各自只保持固定缓冲区,前一次写完成后才读取下一块。该模型支持 Content-Length、chunked、206 Range 响应和 hop-by-hop header 过滤。任一侧错误或取消时会联动取消其他方向。

若上游已提前拒绝请求,可调用 `request->cancel_read()` 或 `response->cancel_request_body()`。这只终止入站请求体读取,响应仍可正常写回;为避免未消费字节污染下一条请求,该连接不会再 keep-alive。

`HttpClientStream` 和 `HttpProxySession` 同时支持 `http://` 与 `https://`,两种传输都保持固定缓冲模型。默认 TLS context 使用系统信任库并校验证书;私有 CA 可通过接受 `ssl::context&` 的构造函数注入。

---

## Cron 定时任务

### Lambda 任务
Expand Down
Loading
Loading