Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions doc/http2-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,9 @@ void doc_h2_client_submit(uvcpp::uvcpp_h2_connection* conn) {

| 项 | 值 | 位置 |
|---|---|---|
| 单个待发头部块 | 64 KiB(`H2_MAX_SEND_HEADER_BLOCK`) | `src/http2/uvcpp_h2_common.h:57`、设置点 `src/http2/uvcpp_h2_session.cpp:914-914`、自查 `:1216-1216` |
| 单个待发头部块 | 64 KiB(`H2_MAX_SEND_HEADER_BLOCK`) | `src/http2/uvcpp_h2_common.h:57`、设置点 `src/http2/uvcpp_h2_session.cpp:914-914`、自查 `:1235-1235` |
| 单流 body | 64 MiB(`H2_DEFAULT_MAX_BODY_BYTES`)→ RST | `src/http2/uvcpp_h2_common.h:32`、`src/http2/uvcpp_h2_session.cpp:669-669` |
| 单流**待发**队列 | 4 MiB(`H2_DEFAULT_MAX_OUT_STREAM_BYTES`)→ `submit_data` 同步返 `UV_ENOBUFS` | `src/http2/uvcpp_h2_common.h:79`、`src/http2/uvcpp_h2_session.cpp:1322-1322` |
| 单流**待发**队列 | 4 MiB(`H2_DEFAULT_MAX_OUT_STREAM_BYTES`)→ `submit_data` 同步返 `UV_ENOBUFS` | `src/http2/uvcpp_h2_common.h:79`、`src/http2/uvcpp_h2_session.cpp:1345-1345` |
| `content-length` 荒谬值 | `> 1<<40` 判非法 | `src/http2/uvcpp_h2_session.cpp:466-466` |
| 控制帧令牌桶 | burst 64、补充 32 个/秒 | `src/http2/uvcpp_h2_session.cpp:75-76`、`control_frame_ok()` `:217-217` |
| `:scheme` 与实际传输层一致 | TLS 只接受 `https`、明文只接受 `http`,不符即 RST(两个都收等于给混淆代理开门,RFC 9113 §8.1.1) | `src/http2/uvcpp_h2_session.cpp:384-387` |
Expand Down
4 changes: 2 additions & 2 deletions doc/http2-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
NGHTTP2_ERR_FRAME_SIZE_ERROR`,而那个错误码是 `is_non_fatal` 的 —— 它在上层
被处理成"丢掉整帧、关掉这条流、继续跑",既不通知我们、也不发 RST_STREAM。
本层在 `submit_*` 里按同一个公式先算一遍,换成同步的 `UV_EMSGSIZE`
(`src/http2/uvcpp_h2_session.cpp:1210-1216`)。
(`src/http2/uvcpp_h2_session.cpp:1229-1235`)。
- **收到对端 GOAWAY 之后不再接受新流。** `on_frame_recv` 记下 `last_stream_id` 与
错误码,`submit_request` 用 `nghttp2_session_check_request_allowed()` 提前拦,
同步返回 `UV_ENOTCONN`;`peer_goaway_received()` 等三个取值函数把它暴露出去。
Expand Down Expand Up @@ -314,7 +314,7 @@ NONE"的实现也能全绿;**m3** h1 不在头完成时记状态码 ⇒ `web_h
### 4.3 盘查时判为缺陷、**核下来不是**的(免得下次再盘一遍)

- **"本层可能提交超过对端 `SETTINGS_MAX_CONCURRENT_STREAMS` 的并发流"—— 不成立,
此处更正。** `peer_max_concurrent_streams()`(`src/http2/uvcpp_h2_session.cpp:1691-1691`)确实
此处更正。** `peer_max_concurrent_streams()`(`src/http2/uvcpp_h2_session.cpp:1718-1718`)确实
零生产调用方,但 nghttp2 自己就按这个上限**排队**而不是拒绝:超出的请求 HEADERS
留在 `ob_syn`(`nghttp2:nghttp2_session.c:2315,2346` 上的
`session_is_outgoing_concurrent_streams_max()` 闸门),流一关
Expand Down
71 changes: 49 additions & 22 deletions src/http2/uvcpp_h2_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1052,8 +1052,40 @@ namespace {
* 之类的优化,必须先把 `store` 的生命周期延到该帧真的写出去为止 —— 那和现在
* 这个"提交完就扔"的形状是冲突的。
*/
/// HTTP/2 的 field name 在线上必须全小写(RFC 9113 §8.2.1)。应用层经常设置
/// 混合大小写的名字;在组帧边界统一规范化,否则 nghttp2 的校验会拒掉整个
/// 头块,流停在“提交过、一个字节都没发”的状态,客户端只能等到超时。
std::string lower_header_name(const std::string& name) {
std::string out = name;
for (char& c : out) {
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
}
return out;
}

/// 校验并追加一个普通字段。`names` 指向 `name_store`;调用方提前 reserve,
/// 保证这些指针在 `nghttp2_submit_*` 之前一直有效。
bool append_lower_header(const http_header& h,
std::vector<std::string>& name_store,
std::vector<const char*>& names,
std::vector<std::string>& store) {
std::string lower = lower_header_name(h.name);
if (lower.empty() ||
!nghttp2_check_header_name(
reinterpret_cast<const uint8_t*>(lower.data()), lower.size()) ||
!nghttp2_check_header_value_rfc9113(
reinterpret_cast<const uint8_t*>(h.value.data()), h.value.size())) {
return false;
}
name_store.push_back(std::move(lower));
names.push_back(name_store.back().c_str());
store.push_back(h.value);
return true;
}

bool build_response_nv(const uvcpp_http_response& resp, bool omit_body,
std::vector<std::string>& store,
std::vector<std::string>& name_store,
std::vector<nghttp2_nv>& nv) {
const int status = static_cast<int>(resp.status_code);

Expand Down Expand Up @@ -1090,14 +1122,7 @@ bool build_response_nv(const uvcpp_http_response& resp, bool omit_body,
has_cl = true;
continue; // 值已记下,末尾统一发
}
if (!nghttp2_check_header_name(
reinterpret_cast<const uint8_t*>(h.name.data()), h.name.size()) ||
!nghttp2_check_header_value_rfc9113(
reinterpret_cast<const uint8_t*>(h.value.data()), h.value.size())) {
return false;
}
names.push_back(h.name.c_str());
store.push_back(h.value);
if (!append_lower_header(h, name_store, names, store)) return false;
}

// body 非空却没有 `content-length`:补一个。h2 里它只是参考值,
Expand Down Expand Up @@ -1149,6 +1174,7 @@ bool build_response_nv(const uvcpp_http_response& resp, bool omit_body,
*/
bool build_trailer_nv(const http_headers& trailers,
std::vector<std::string>& store,
std::vector<std::string>& name_store,
std::vector<nghttp2_nv>& nv) {
store.clear();
store.reserve(trailers.size());
Expand All @@ -1160,14 +1186,7 @@ bool build_trailer_nv(const http_headers& trailers,
if (h.name.empty()) return false;
if (h.name[0] == ':') return false; // 伪首部:拒,不剥
if (is_connection_specific(h.name)) continue; // 连接专属:剥掉
if (!nghttp2_check_header_name(
reinterpret_cast<const uint8_t*>(h.name.data()), h.name.size()) ||
!nghttp2_check_header_value_rfc9113(
reinterpret_cast<const uint8_t*>(h.value.data()), h.value.size())) {
return false;
}
names.push_back(h.name.c_str());
store.push_back(h.value);
if (!append_lower_header(h, name_store, names, store)) return false;
}

nv.clear();
Expand Down Expand Up @@ -1235,8 +1254,10 @@ int uvcpp_h2_session::submit_response(int32_t stream_id,
if (status_is_bodyless(static_cast<int>(resp.status_code))) omit_body = true;

std::vector<std::string> store;
std::vector<std::string> name_store;
std::vector<nghttp2_nv> nv;
if (!build_response_nv(resp, omit_body, store, nv)) return UV_EINVAL;
name_store.reserve(resp.headers.size());
if (!build_response_nv(resp, omit_body, store, name_store, nv)) return UV_EINVAL;
// 两条退路都必须在**置 SENT 之前**:置了再退,这条流就停在"说过要发、其实
// 一个字节都没发"的状态上,而 nghttp2 那边连流都没建 —— 对端只会一直等。
if (!header_block_fits(nv)) return UV_EMSGSIZE;
Expand Down Expand Up @@ -1280,10 +1301,12 @@ int uvcpp_h2_session::submit_headers(int32_t stream_id,
if (sit->second.state != h2_stream_state::OPEN) return UV_EALREADY;

std::vector<std::string> store;
std::vector<std::string> name_store;
std::vector<nghttp2_nv> nv;
name_store.reserve(resp.headers.size());
// `omit_body = true` 走的是"不补 content-length"那一支,正是流式要的:
// 长度此刻不知道,补一个就是在说谎。`resp` 里若已写明就原样发出。
if (!build_response_nv(resp, /*omit_body=*/true, store, nv)) return UV_EINVAL;
if (!build_response_nv(resp, /*omit_body=*/true, store, name_store, nv)) return UV_EINVAL;
// 同上:置位之前退,否则流停在"头部发过了"而实际没发。
if (!header_block_fits(nv)) return UV_EMSGSIZE;
sit->second.state = h2_stream_state::HEADERS_SENT;
Expand Down Expand Up @@ -1414,8 +1437,10 @@ int uvcpp_h2_session::submit_trailer(int32_t stream_id,
if (!os.chunks.empty()) return UV_EBUSY;

std::vector<std::string> store;
std::vector<std::string> name_store;
std::vector<nghttp2_nv> nv;
if (!build_trailer_nv(trailers, store, nv)) return UV_EINVAL;
name_store.reserve(trailers.size());
if (!build_trailer_nv(trailers, store, name_store, nv)) return UV_EINVAL;
// 与另两条提交一样:超限在**动状态之前**退,且退得同步(理由见
// `header_block_fits()` 那段注释 —— nghttp2 把这个错误处理成"静默丢帧 + 关流")。
if (!header_block_fits(nv)) return UV_EMSGSIZE;
Expand Down Expand Up @@ -1561,13 +1586,15 @@ int32_t uvcpp_h2_session::submit_request(const uvcpp_http_request& req,
const http_header& h = req.headers[i];
if (name_is(h.name, "host")) continue; // 已经进了 `:authority`
if (is_connection_specific(h.name)) continue;
if (!nghttp2_check_header_name(
reinterpret_cast<const uint8_t*>(h.name.data()), h.name.size()) ||
std::string lower = lower_header_name(h.name);
if (lower.empty() ||
!nghttp2_check_header_name(
reinterpret_cast<const uint8_t*>(lower.data()), lower.size()) ||
!nghttp2_check_header_value_rfc9113(
reinterpret_cast<const uint8_t*>(h.value.data()), h.value.size())) {
return UV_EINVAL;
}
names.push_back(h.name);
names.push_back(std::move(lower));
store.push_back(h.value);
}

Expand Down
26 changes: 25 additions & 1 deletion tests/functional/web_ssl_app_h2_func.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ int build_app(uvcpp_web_app& app, app_probe& st, bool set_flag, bool enable_h2,
resp.end();
});

// 回归:HTTP/2 要求 field name 全小写。应用层设置混合大小写的 Header 时,
// 之前的 h2 响应构建器直接返回 false,流永远不结束,客户端只能等到超时。
app.get("/mixed-header", [](uvcpp_web_request&, uvcpp_web_response& resp,
uvcpp_web_next) {
resp.set_header("X-Mixed-Case", "ok");
resp.text("mixed-ok");
resp.end();
});

add_stream_routes(app, st);

// 场景 4 才挂静态;路由表在 `start_background()` 之后不再变,所以另外几个场景
Expand Down Expand Up @@ -634,8 +643,10 @@ void run_h2_requests(h2_client& c, app_probe& st, const char* tag) {
check(s1 > 0, std::string(tag) + ": /hello 没提交上");
const int32_t s2 = c.submit(http_method::HTTP_GET, "/echo/abc");
check(s2 > 0, std::string(tag) + ": /echo/abc 没提交上");
const int32_t s3 = c.submit(http_method::HTTP_GET, "/mixed-header");
check(s3 > 0, std::string(tag) + ": /mixed-header 没提交上");

check(c.wait_responses(2), std::string(tag) + ": 两条响应没在 " +
check(c.wait_responses(3), std::string(tag) + ": 三条响应没在 " +
std::to_string(kWaitMs) + "ms 内收齐");
uvcpp_test::pump_for(c.loop(), 80);

Expand Down Expand Up @@ -663,6 +674,19 @@ void run_h2_requests(h2_client& c, app_probe& st, const char* tag) {
std::string(tag) + ": /echo/abc body = \"" + r2.body + "\"");
}

// HTTP/2 的 field name 必须全小写。应用设置了 `X-Mixed-Case`,框架必须把它
// 规范化后发出去,而不是丢掉整条响应。
seen_response r3;
const bool has3 = c.find_response(s3, r3);
check(has3, std::string(tag) + ": 流 " + std::to_string(s3) + " 没有响应");
if (has3) {
check(r3.end, std::string(tag) + ": /mixed-header 的响应没收完");
check(r3.status == 200,
std::string(tag) + ": /mixed-header 状态 " + std::to_string(r3.status));
check(r3.body == "mixed-ok",
std::string(tag) + ": /mixed-header body = \"" + r3.body + "\"");
}

// 服务端侧同号对座:框架处理函数看到的 `stream_id` 必须就是客户端发出去的那个。
// 少了这一半,"响应体对"可能只是路由恰好命中,而流身份其实是错的。
// 对不上时必须**自报家门**:把两边看到的流号都打出来。只报"没匹配上"
Expand Down
8 changes: 4 additions & 4 deletions tests/tools/doc_line_refs.lock
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,10 @@ src/http2/uvcpp_h2_connection.h:58-58 ae8b03418f41a26f ae8b03418f41a26f -
src/http2/uvcpp_h2_connection.h:70-70 d6e15e85dd93eaf0 d6e15e85dd93eaf0 -
src/http2/uvcpp_h2_connection.h:74-74 ba98e21b46a7f3c3 ba98e21b46a7f3c3 -
src/http2/uvcpp_h2_session.cpp:1002-1002 8a9f9b8835829f00 8a9f9b8835829f00 -
src/http2/uvcpp_h2_session.cpp:1210-1216 cd15d5a39dc61848 9ec3f4c602e73a0a 48d033810df113d5
src/http2/uvcpp_h2_session.cpp:1216-1216 9ec3f4c602e73a0a 9ec3f4c602e73a0a -
src/http2/uvcpp_h2_session.cpp:1322-1322 b1e5239df8d0f29d b1e5239df8d0f29d -
src/http2/uvcpp_h2_session.cpp:1691-1691 fec5526e48281d9a fec5526e48281d9a -
src/http2/uvcpp_h2_session.cpp:1229-1235 cd15d5a39dc61848 9ec3f4c602e73a0a 48d033810df113d5
src/http2/uvcpp_h2_session.cpp:1235-1235 9ec3f4c602e73a0a 9ec3f4c602e73a0a -
src/http2/uvcpp_h2_session.cpp:1345-1345 b1e5239df8d0f29d b1e5239df8d0f29d -
src/http2/uvcpp_h2_session.cpp:1718-1718 fec5526e48281d9a fec5526e48281d9a -
src/http2/uvcpp_h2_session.cpp:217-217 18f9128271eb71ab 18f9128271eb71ab -
src/http2/uvcpp_h2_session.cpp:234-234 ee8859e452fea4f4 ee8859e452fea4f4 -
src/http2/uvcpp_h2_session.cpp:360-360 e4ce298e0aeea921 e4ce298e0aeea921 -
Expand Down
Loading