Deno deno_fetch crate 深度解析:浏览器级 Fetch API 在 Rust 中的实现
【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno
本文以 Deno 仓库中的ext/fetch/README.md为核心,结合 ext/fetch 下的 Rust 实现与主运行时的 JS 引导代码,完整讲解deno_fetchcrate 的组件构成、JS/Rust 两侧接入方式、5 个公开 op 的语义,以及请求调度、代理/压缩/重试等底层机制。读完后你将理解fetch()、Headers、Request、Response、FormData、EventSource与Deno.createHttpClient()在 Deno 内部的真实实现链路。
1. deno_fetch 是什么
ext/fetch/README.md 开篇即定义了本 crate 的定位:
This crate implements the Fetch API.(本 crate 实现 Fetch API)
即按 WHATWG Fetch 规范为 Deno 运行时提供fetch()及相关 Web 标准全局对象。Cargo.toml 中 crate 名为deno_fetch(当前版本 0.282.0),描述为 "Fetch API implementation for Deno"。从 lib.rs 的依赖看,底层构建在hyper/hyper-util、tokio、hickory-resolver(DNS)、deno_tls/rustls(TLS)、tower(中间件与重试)之上,并借助async-compression提供透明 gzip/Brotli 解压。
整个 crate 由两类文件组成:
- JS 层(浏览器标准侧实现):通过
deno_core::extension!宏的lazy_loaded_js字段注册,见 lib.rs#L159-L184:20_headers.js—Headers21_formdata.js—FormData22_body.js— body 处理(extractBody、InnerBody)22_http_client.js—Deno.createHttpClient()的 JS 侧23_request.js/23_response.js—Request/Response26_fetch.js—fetch()主入口与网络错误/重定向状态机27_eventsource.js—EventSource
- Rust 层(系统侧):lib.rs(核心逻辑)、dns.rs(带权限校验的 DNS 解析与连接)、proxy.rs(代理连接器与 basic auth)、fs_fetch_handler.rs(
file:协议处理)。
extension!宏同时声明了依赖与状态注入:
deno_core::extension!(deno_fetch, deps = [ deno_webidl, deno_web ], ops = [ op_fetch, op_fetch_send, op_utf8_to_byte_string, op_fetch_custom_client, op_fetch_promise_is_settled, ], lazy_loaded_js = [ /* 上述 8 个 JS 文件 */ ], options = { options: Options }, state = |state, options| { state.put::<Options>(options.options); }, );README 的 Dependencies 一节列出的运行时依赖为deno_webidl、deno_web、deno_console(分别由同名的 crate 提供);从源码结构看,extension!宏的deps字段实际声明了deno_webidl与deno_web,其中26_fetch.js还会loadExtScript加载ext:deno_webidl/00_webidl.js与ext:deno_web/06_streams.js等模块(ext/fetch/26_fetch.js#L37-L50)。
2. 接入方式:JS 侧全局注册 + Rust 侧 init
README 给出了将本扩展装配进运行时的完整范式。JS 侧:加载扩展脚本并把各构造函数挂到全局作用域:
import { core } from "ext:core/mod.js"; const headers = core.loadExtScript("ext:deno_fetch/20_headers.js"); const formData = core.loadExtScript("ext:deno_fetch/21_formdata.js"); const request = core.loadExtScript("ext:deno_fetch/23_request.js"); const response = core.loadExtScript("ext:deno_fetch/23_response.js"); const fetch = core.loadExtScript("ext:deno_fetch/26_fetch.js"); const eventSource = core.loadExtScript("ext:deno_fetch/27_eventsource.js"); // Set up the callback for Wasm streaming ops core.setWasmStreamingCallback(fetch.handleWasmStreaming); Object.defineProperty(globalThis, "fetch", { value: fetch.fetch, enumerable: true, configurable: true, writable: true, }); Object.defineProperty(globalThis, "Request", { value: request.Request, enumerable: false, configurable: true, writable: true, }); Object.defineProperty(globalThis, "Response", { value: response.Response, enumerable: false, configurable: true, writable: true, }); Object.defineProperty(globalThis, "Headers", { value: headers.Headers, enumerable: false, configurable: true, writable: true, }); Object.defineProperty(globalThis, "FormData", { value: formData.FormData, enumerable: false, configurable: true, writable: true, });注意两处细节:fetch被声明为enumerable: true(与浏览器一致,Object.keys(globalThis)可见),其余四个构造器不可枚举;core.setWasmStreamingCallback(fetch.handleWasmStreaming)把WebAssembly.instantiateStreaming(fetch(...))所需的流式回调接入。
Rust 侧只需在RuntimeOptions的extensions字段中提供deno_fetch::deno_fetch::init(Default::default()),其中Options实现Default。
在主运行时中,这一装配已做懒加载优化:fetch模块整体约 208 KB(经22_body.js引入 streams),启动路径上只有 WASM streaming 回调注册是必需的,因此 runtime/js/99_main.js#L101-L113 用一个lazyFetchMod()包装core.loadExtScript("ext:deno_fetch/26_fetch.js"),首次真正用到时才加载;而 runtime/js/98_global_scope_shared.js 中fetch、Headers、Request、Response、FormData、EventSource全部通过懒加载 getter 挂载到windowOrWorkerGlobalScope上。Deno.createHttpClient则经由 runtime/js/90_deno_ns.js#L12 加载22_http_client.js接入Deno命名空间。
3. Options:运行时注入的可调参数
lib.rs#L110-L157 定义了Options结构体,Default值基本为"空配置"(空 User-Agent、无 CA store provider、无代理、无 hook、默认DefaultFileFetchHandler、默认 DNS 解析器):
| 字段 | 类型 | 说明 |
|---|---|---|
user_agent | String | 全局默认 User-Agent,注入到每个请求 |
root_cert_store_provider | Option<Arc<dyn RootCertStoreProvider>> | 延迟提供根证书库的回调,root_cert_store()时调用 |
proxy | Option<Proxy> | 全局代理(Http/Tcp/Unix/Vsock) |
client_builder_hook | Option<fn(HyperClientBuilder) -> HyperClientBuilder> | 自定义 hyper client 构建;可被Deno.createHttpClient()的同名参数覆盖 |
request_builder_hook | Option<fn(&mut http::Request<ReqBody>) -> Result<(), JsErrorBox>> | 每个请求发出前修改请求的钩子,出错映射为FetchError::RequestBuilderHook |
unsafely_ignore_certificate_errors | Option<Vec<String>> | 对指定 SAN 忽略证书校验错误 |
client_cert_chain_and_key | TlsKeys | mTLS 客户端证书链与私钥 |
file_fetch_handler | Rc<dyn FetchHandler> | file:协议处理器,默认DefaultFileFetchHandler |
resolver | dns::Resolver | DNS 解析器 |
FetchHandlertrait(lib.rs#L284-L293)只有一个fetch_file方法,返回可取消的响应 future;默认实现DefaultFileFetchHandler对任何请求都返回NetworkError,宿主(如 Deno CLI)可替换为真实文件读取实现(fs_fetch_handler.rs 提供FsFetchHandler)。
4. 五个公开 op 逐一解析
README 的 "Provided ops" 一节列出了可通过Deno.coreops 访问的 5 个 op,与extension!宏声明完全一致:
4.1 op_fetch —— 请求提交(同步,返回资源句柄)
op_fetch 是同步 op,参数为method: ByteString、url: String、headers: Vec<(ByteString, ByteString)>、client_rid: Option<u32>(自定义 client)、has_body: bool、data: Option<Uint8Array>、resource: Option<ResourceId>(流式 body 的资源),返回FetchReturn { request_rid, cancel_handle_rid }——请求被封装为资源表中的FetchRequestResource,真正的网络 I/O 在op_fetch_send中异步等待。
按 URL scheme 分流(lib.rs#L458-L596):
file::仅允许GET,否则返回FetchError::FsNotGet(method);交由Options.file_fetch_handler处理。http:/https::先经PermissionsContainer::check_net_url(&url, "fetch()")做网络权限检查;随后:extract_authority从 URL 中剥离user:password并转为Authorization: Basic ...头(lib.rs#L1663-L1692,源自 reqwest 的同名实现);- body 有三种形态,
ReqBody枚举(lib.rs#L1599-L1605):Full(一次性字节,直接设置Content-Length)、Streaming(包一个ResourceToBodyAdapter,按 64 KiB 从资源读取,若size_hint精确则设置Content-Length)、Empty(无 body 时POST/PUT按规范强制Content-Length: 0); - 逐条追加用户头时跳过
Content-Length,且默认 client 会跳过用户设置的Host头(自定义 client 以allow_host: true放行); - 调用
request_builder_hook(如有); - 创建
CancelHandle,把client.send(request)包裹成可取消 future 存入资源表,AbortSignal 的取消最终通过关闭FetchCancelHandle资源触发。
data::DataUrl::process解析后直接构造 200 响应,Content-Type取 data URL 的 MIME 类型。blob::blob URL 在 JS 侧解析;若走到这里说明不是 object URL,返回FetchError::BlobNotFound。- 其他 scheme:
FetchError::SchemeNotSupported(scheme)。
4.2 op_fetch_send —— 异步取回响应
op_fetch_send 从资源表取出FetchRequestResource并 await 其 future,返回FetchResponse(lib.rs#L604-L622):
#[derive(Default, ToV8)] pub struct FetchResponse { pub status: u16, pub status_text: String, pub headers: Vec<(ByteString, ByteString)>, pub url: String, pub response_rid: ResourceId, // 响应体资源,按块 read pub content_length: Option<u64>, pub body_decoded: bool, // 是否被透明解压过 pub error: Option<(String, String)>, }两个值得注意的设计:
body_decoded标记:当 body 被透明解压时,响应里的Content-Encoding/Content-Length/Transfer-Encoding描述的是"线上编码后的 body"而非response_rid后的解码 body,JS 侧据此修正。- 错误镜像:传输失败(
ClientSend,如连接被拒、DNS 失败、响应中途断连)不再直接把原始底层错误抛给 JS,而是返回FetchResponse { error: Some((detail, cause)), .. },由 JS 侧重建为TypeError: "fetch failed"且.cause携带底层细节——与 Node/undici 的 fetch 错误形态对齐(lib.rs#L638-L667 中的注释与实现)。被取消的请求则返回FetchError::RequestCanceled。
响应体资源FetchResponseResource(lib.rs#L738-L831)实现Resource::read,把 hyper 的Incomingbody 转成BytesStream,支持peek分块、size_hint、取消,并提供upgrade()以支持协议升级(如 WebSocket 握手所需的 101 连接接管)。
4.3 op_utf8_to_byte_string
极小的辅助 op(lib.rs#L1213-L1216):把 JS 字符串按 UTF-8 转成ByteString(用于 header 名的规范化比较等)。
4.4 op_fetch_custom_client —— Deno.createHttpClient() 的后端
op_fetch_custom_client 创建独立于全局的 HTTP client,参数结构 CreateHttpClientArgs:
| 参数 | 默认 | 说明 |
|---|---|---|
ca_certs | [] | 额外追加的 CA 证书(PEM 字节) |
proxy | None | Http(URL + basic auth)/Tcp/Unix/Vsock;按类型分别做check_net_url、check_net、check_open + check_net_unix_socket、check_net_vsock权限校验;Unix代理在 Windows 上不可用,Vsock仅 Android/Linux/macOS |
pool_max_idle_per_host | hyper 默认 | 每主机最大空闲连接 |
pool_idle_timeout | hyper 默认 | true→用默认,false→永不超时,数字为毫秒 |
http1/http2 | 均为true | 二者皆 false 时返回HttpVersionSelectionInvalid;仅 http2 时走builder.http2_only(true) |
allow_host | false | true时允许用户覆写Host头 |
local_address | None | 绑定的本地 IP |
http2_max_header_list_size | None | HTTP/2SETTINGS_MAX_HEADER_LIST_SIZE |
创建的 client 连同allow_host存入HttpClientResource(资源名httpClient);之后op_fetch传入client_rid即用该 client 发请求。
4.5 op_fetch_promise_is_settled
#[op2(fast)]快速 op(lib.rs#L1694-L1697),直接读 V8 Promise 状态判断是否已 settle,供 JS 侧Responsebody 读取路径做同步状态检查,避免一次 op 往返。
5. HTTP 客户端底层:create_http_client 全解
create_http_client 是全局 client 与自定义 client 的共同构造函数,流程如下:
- TLS 配置:经
deno_tls::create_client_config装配根证书库、CA 证书、mTLS 链、unsafely_ignore_certificate_errors;代理用的 TLS 配置清空 ALPN(与代理服务端建连不做协议协商)。 - ALPN:按
http2/http1选项生成["h2", "http/1.1"]协议列表。 - HTTP/2 头列表上限:hyper 默认 16 KB,会把带大
content-security-policy或大量set-cookie的响应以PROTOCOL_ERROR拒掉;这里把默认值提到 256 KB(DEFAULT_HTTP2_MAX_HEADER_LIST_SIZE,与浏览器行为一致,lib.rs#L1037-L1041)。优先级为:默认值 <client_builder_hook<http2MaxHeaderListSize显式参数。 - 连接器:
dns::PermissionedHttpConnector(DNS 解析后对实际 IP 做 net deny 校验,与Deno.connect行为一致)外包一层proxy::ProxyConnector。代理列表由proxy::from_env()读取环境变量,Options.proxy或createHttpClient传入的代理会以最高优先级prepend。 - 连接池:
pool_max_idle_per_host、pool_idle_timeout透传给 hyper builder。 - 服务栈组装(lib.rs#L1201-L1210):
DecompressionService └─ retry::Retry<FetchRetry> └─ ConnectionTracker └─ hyper Client<TrackingConnector<Connector>>透明解压(DecompressionService::call):默认自动补Accept-Encoding: gzip,br;带Range请求或显式Accept-Encoding: identity时跳过解压(Range 响应可能是压缩字节流的一部分,无法整体解码)。解压时保留原始Content-Encoding/Content-Length头(符合 fetch 规范"仅解码 body 不改头"的要求),并插入BodyDecoded标记;Content-Length: 0的空 body 直接透传,避免解析零字节"压缩流"。
传输重试策略 FetchRetry(lib.rs#L1972-L2114):只在安全条件下重试一次(Retried标记防重入),且clone_request仅能克隆Full/Emptybody——流式 body 永不重试。可重试的错误判定(is_error_retryable):
- HTTP/2 服务端优雅
GOAWAY(NO_ERROR)、远端REFUSED_STREAM(RFC 9113 §8.7.3.2)——任何连接上均可重试; - 其余情形(HTTP/1.1 消息不完整、
ECONNRESET/ECONNABORTED)仅当失败发生在连接池里取出的复用连接上才可重试——新建立连接上的失败意味着服务端可能已收到并处理请求,重发会造成非幂等请求重复。 - 连接来源判定由
TrackingConnector/ConnectionTracker/CheckoutTracker实现:每个连接携带ConnectionUsage原子计数,每次 checkout 后对计数采样,区分"池中的陈旧连接"与"本次新建连接";代码注释还明确记录了 HTTP/2 多路复用下该计数的已知局限(计数按连接而非按 stream)。
发送前Client::send还会注入User-Agent(若请求未设置)、代理Proxy-Authorization与Accept: */*(lib.rs#L1528-L1558)。
6. 错误模型
FetchError 是面向 JS 的顶层错误枚举,覆盖了该 crate 可抛出的全部形态,节选:
| 变体 | JS 表现 | 触发场景 |
|---|---|---|
NetworkError | TypeError: NetworkError when attempting to fetch resource | DNS 层之前的网络类失败;file:下 IO/NotSupported 错误也会被折叠为此 |
FsNotGet(Method) | Fetching files only supports the GET method: received ... | file:URL 用了非 GET |
Permission | 权限检查错误 | check_net_url失败 |
DataUrl/Base64 | 解析/解码错误 | data:URL 非法或 base64 损坏 |
BlobNotFound | Blob for the given URL not found. | blob:URL 无法解析 |
SchemeNotSupported(String) | Url scheme '...' not supported | 未知 scheme |
RequestCanceled | Request was cancelled | AbortSignal 触发 |
Http | HttpError(generic) | hyper/http 栈错误 |
ClientCreate | client 创建失败(如HttpVersionSelectionInvalid、InvalidProxyUrl、UnixProxyNotSupportedOnWindows、InvalidUserAgent) | createHttpClient参数非法 |
ClientSend | 经op_fetch_send重映射为fetch failed+cause | 传输阶段失败 |
RequestBuilderHook | hook 自定义错误 | request_builder_hook返回 Err |
7. 测试与基准
该 crate 自带三层验证资产:
- tests.rs:针对
extract_authority、代理 URL 解析等纯函数的单元测试; - benches/body.rs:body 流式读取路径的基准;
- benches/headers_methods.rs:
Headers/方法处理的基准。
更上层的集成验证散落在tests/specs/与tests/unit/(如 fetch 相关的权限、file:协议、createHttpClient用例),可作为行为规格的补充阅读。
8. 小结
deno_fetch是 Deno 中 Fetch API 的"双层实现":JS 文件(20_headers.js…27_eventsource.js)负责标准接口的语义状态机,lib.rs负责权限、DNS、代理、TLS、连接池、解压与重试等系统侧能力,二者以 5 个 op(op_fetch、op_fetch_send、op_utf8_to_byte_string、op_fetch_custom_client、op_fetch_promise_is_settled)为边界对接。理解FetchRequestResource/FetchResponseResource的资源化设计,以及"默认值 < hook < 显式参数"的配置优先级、"仅池化连接上安全重试一次"的保守重试策略,是读懂 Deno 网络栈的关键入口。
【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考