news 2026/9/7 14:56:32

Deno deno_fetch crate 深度解析:浏览器级 Fetch API 在 Rust 中的实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Deno deno_fetch crate 深度解析:浏览器级 Fetch API 在 Rust 中的实现

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()HeadersRequestResponseFormDataEventSourceDeno.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-utiltokiohickory-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.jsHeaders
    • 21_formdata.jsFormData
    • 22_body.js— body 处理(extractBody、InnerBody)
    • 22_http_client.jsDeno.createHttpClient()的 JS 侧
    • 23_request.js/23_response.jsRequest/Response
    • 26_fetch.jsfetch()主入口与网络错误/重定向状态机
    • 27_eventsource.jsEventSource
  • 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_webidldeno_webdeno_console(分别由同名的 crate 提供);从源码结构看,extension!宏的deps字段实际声明了deno_webidldeno_web,其中26_fetch.js还会loadExtScript加载ext:deno_webidl/00_webidl.jsext: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 侧只需在RuntimeOptionsextensions字段中提供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 中fetchHeadersRequestResponseFormDataEventSource全部通过懒加载 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_agentString全局默认 User-Agent,注入到每个请求
root_cert_store_providerOption<Arc<dyn RootCertStoreProvider>>延迟提供根证书库的回调,root_cert_store()时调用
proxyOption<Proxy>全局代理(Http/Tcp/Unix/Vsock)
client_builder_hookOption<fn(HyperClientBuilder) -> HyperClientBuilder>自定义 hyper client 构建;可被Deno.createHttpClient()的同名参数覆盖
request_builder_hookOption<fn(&mut http::Request<ReqBody>) -> Result<(), JsErrorBox>>每个请求发出前修改请求的钩子,出错映射为FetchError::RequestBuilderHook
unsafely_ignore_certificate_errorsOption<Vec<String>>对指定 SAN 忽略证书校验错误
client_cert_chain_and_keyTlsKeysmTLS 客户端证书链与私钥
file_fetch_handlerRc<dyn FetchHandler>file:协议处理器,默认DefaultFileFetchHandler
resolverdns::ResolverDNS 解析器

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: ByteStringurl: Stringheaders: Vec<(ByteString, ByteString)>client_rid: Option<u32>(自定义 client)、has_body: booldata: 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)>, }

两个值得注意的设计:

  1. body_decoded标记:当 body 被透明解压时,响应里的Content-Encoding/Content-Length/Transfer-Encoding描述的是"线上编码后的 body"而非response_rid后的解码 body,JS 侧据此修正。
  2. 错误镜像:传输失败(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 字节)
proxyNoneHttp(URL + basic auth)/Tcp/Unix/Vsock;按类型分别做check_net_urlcheck_netcheck_open + check_net_unix_socketcheck_net_vsock权限校验;Unix代理在 Windows 上不可用,Vsock仅 Android/Linux/macOS
pool_max_idle_per_hosthyper 默认每主机最大空闲连接
pool_idle_timeouthyper 默认true→用默认,false→永不超时,数字为毫秒
http1/http2均为true二者皆 false 时返回HttpVersionSelectionInvalid;仅 http2 时走builder.http2_only(true)
allow_hostfalsetrue时允许用户覆写Host
local_addressNone绑定的本地 IP
http2_max_header_list_sizeNoneHTTP/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 的共同构造函数,流程如下:

  1. TLS 配置:经deno_tls::create_client_config装配根证书库、CA 证书、mTLS 链、unsafely_ignore_certificate_errors;代理用的 TLS 配置清空 ALPN(与代理服务端建连不做协议协商)。
  2. ALPN:按http2/http1选项生成["h2", "http/1.1"]协议列表。
  3. 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显式参数
  4. 连接器:dns::PermissionedHttpConnector(DNS 解析后对实际 IP 做 net deny 校验,与Deno.connect行为一致)外包一层proxy::ProxyConnector。代理列表由proxy::from_env()读取环境变量,Options.proxycreateHttpClient传入的代理会以最高优先级prepend
  5. 连接池:pool_max_idle_per_hostpool_idle_timeout透传给 hyper builder。
  6. 服务栈组装(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-AuthorizationAccept: */*(lib.rs#L1528-L1558)。

6. 错误模型

FetchError 是面向 JS 的顶层错误枚举,覆盖了该 crate 可抛出的全部形态,节选:

变体JS 表现触发场景
NetworkErrorTypeError: NetworkError when attempting to fetch resourceDNS 层之前的网络类失败;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 损坏
BlobNotFoundBlob for the given URL not found.blob:URL 无法解析
SchemeNotSupported(String)Url scheme '...' not supported未知 scheme
RequestCanceledRequest was cancelledAbortSignal 触发
HttpHttpError(generic)hyper/http 栈错误
ClientCreateclient 创建失败(如HttpVersionSelectionInvalidInvalidProxyUrlUnixProxyNotSupportedOnWindowsInvalidUserAgent)createHttpClient参数非法
ClientSendop_fetch_send重映射为fetch failed+cause传输阶段失败
RequestBuilderHookhook 自定义错误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.js27_eventsource.js)负责标准接口的语义状态机,lib.rs负责权限、DNS、代理、TLS、连接池、解压与重试等系统侧能力,二者以 5 个 op(op_fetchop_fetch_sendop_utf8_to_byte_stringop_fetch_custom_clientop_fetch_promise_is_settled)为边界对接。理解FetchRequestResource/FetchResponseResource的资源化设计,以及"默认值 < hook < 显式参数"的配置优先级、"仅池化连接上安全重试一次"的保守重试策略,是读懂 Deno 网络栈的关键入口。

【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 14:56:30

技术选型新视角:从社区投票结果洞察用户真实需求

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 14:56:00

工具评估清单:验证处理能力的十个硬指标

工具评估清单&#xff1a;验证处理能力的十个硬指标 一份给采购党准备的犀利清单&#xff1a; 「被工具坑过三次之后&#xff0c;我学乖了&#xff1a;看演示没用&#xff0c;听销售吹没用&#xff0c;我只问十个具体问题。比如『验证处理的感知延迟是多少』『失败之后的降级逻…

作者头像 李华
网站建设 2026/9/7 14:55:39

从本地到上线:Python项目Docker化部署实战指南

带了几年项目&#xff0c;我越来越觉得“本地能跑”和“能上线”完全是两码事。Python 写业务逻辑确实快&#xff0c;但一旦要部署给别人用、放到服务器上长期跑&#xff0c;各种环境问题就会接踵而至&#xff1a;Python 版本对不上、系统少了个底层库、依赖装到一半报错、换台…

作者头像 李华
网站建设 2026/9/7 14:51:47

软考中级系统集成项目管理工程师300集精讲:零基础备考全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 14:50:13

免费开源公文排版工具:批量处理与AI内容自动重排

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华