返回 CodeWhale
headers.rs
根目录 / crates / tui / src / mcp / headers.rs
1 use std::collections::HashMap;
2
3 use reqwest::header::{ACCEPT, CONTENT_TYPE};
4
5 pub(super) const MCP_HTTP_ACCEPT: &str = "application/json, text/event-stream";
6
7 pub(super) fn with_default_mcp_http_headers(
8 request: reqwest::RequestBuilder,
9 json_body: bool,
10 ) -> reqwest::RequestBuilder {
11 let request = request.header(ACCEPT, MCP_HTTP_ACCEPT);
12 if json_body {
13 request.header(CONTENT_TYPE, "application/json")
14 } else {
15 request
16 }
17 }
18
19 /// Predicate for the custom-header pass used by MCP HTTP transports.
20 ///
21 /// We accept whatever reqwest's `HeaderName::try_from` /
22 /// `HeaderValue::try_from` would accept, but with three extra rules:
23 ///
24 /// 1. Reject empty / whitespace-only keys - these would surface as a
25 /// request-builder error mid-send and abort the whole connection.
26 /// 2. Reject keys that duplicate the framing we already emit
27 /// (`Accept`, `Content-Type`). The MCP Streamable HTTP transport
28 /// relies on those exact values for protocol negotiation; a stray
29 /// user override could silently break tool discovery.
30 /// 3. Reject values containing ASCII CR or LF. reqwest already
31 /// rejects those, but the explicit check makes the failure path
32 /// visible (a `tracing::warn!` instead of an obscure
33 /// builder error) and documents the response-splitting
34 /// defense.
35 ///
36 /// Returning `false` means "skip this header"; the rest of the
37 /// request still goes out.
38 pub(crate) fn is_safe_custom_header(key: &str, value: &str) -> bool {
39 let trimmed = key.trim();
40 if trimmed.is_empty() {
41 return false;
42 }
43 if trimmed.eq_ignore_ascii_case("accept") || trimmed.eq_ignore_ascii_case("content-type") {
44 return false;
45 }
46 !value.contains('\r') && !value.contains('\n')
47 }
48
49 pub(super) fn apply_safe_custom_headers(
50 mut request: reqwest::RequestBuilder,
51 headers: &HashMap<String, String>,
52 ) -> reqwest::RequestBuilder {
53 for (key, value) in headers {
54 if !is_safe_custom_header(key, value) {
55 tracing::warn!(
56 target: "mcp",
57 "skipping unsafe MCP header {:?} (empty/control-char/reserved)",
58 key
59 );
60 continue;
61 }
62 request = request.header(key.as_str(), value.as_str());
63 }
64 request
65 }
66
66 lines RUST