跳转到正文
返回门户

LFHttp ​

全局变量:LFHttp · 启用方式:// @require http

经扩展后台代理的 HTTP 能力:普通请求、流式响应、WebSocket、本地文件系统,以及面向PluginX客户端/服务端的快捷路由。默认自动携带目标域名已有的浏览器 cookie(credentials: 'include',可传 'omit' 或 withCredentials: false 关闭);也可以用 headers.Cookie 显式指定 cookie,它会整体替换浏览器自动附带的 cookie(见下方 headers 说明)。

URL 安全策略:只拒绝本机 PluginX 客户端端口——回环主机上的 41720 端口(127.0.0.0/8、0.0.0.0、localhost、*.localhost、[::1]、[::],以及内嵌回环 IPv4 的 IPv6 写法,如 [::ffff:127.0.0.1]、[::127.0.0.1]、[64:ff9b::7f00:1]),访问本地客户端请改用 LFHttp.client.*;此外拒绝 chrome-extension://、file:// 与非 http(s) 目标(WebSocket 为非 ws(s) 目标)。localhost 的其他端口、局域网地址与 PluginX 服务器域名都可以访问。

扩展代理通路在重定向后也会按同一策略复核最终地址:跳到本地客户端端口时以 LFHttpUrlBlocked 拒绝(message 末尾带 after redirect),响应体不会交给脚本。注意被重定向的那次请求本身仍会由浏览器发出(fetch 自动跟随重定向),被拦下的只是结果。策略只校验字面主机名/IP,不做 DNS 解析。

方法 ​

ts
LFHttp.request(url, options?): Promise<Response>

发起请求,返回 Response 风格对象

ts
LFHttp.text(url, options?): Promise<string>

请求并取回文本

ts
LFHttp.json(url, options?): Promise<T>

请求并解析 JSON

ts
LFHttp.stream(url, options?): Promise<StreamResponse>

流式响应,for await (const chunk of stream.chunks())

ts
LFHttp.websocket(url, options?): Promise<WebSocketConnection>

建立 WebSocket 连接(页面内直连,或经扩展后台代理)

ts
LFHttp.page.download(url, options?): Promise<PageDownloadReceipt>

从页面 MAIN world 下载到 OPFS(默认)或 client.fs

ts
LFHttp.page.saveAs(url, options?): Promise<PageSaveAsReceipt>

将文件交付到浏览器下载管理器,自动选择页面或扩展代理通路;返回下载回执。

ts
LFHttp.page.fetchToOPFS(url, pathOrOptions?): Promise<PageDownloadReceipt>

下载到当前页面 origin 的 OPFS

ts
LFHttp.page.fetchToClientFile(url, pathOrOptions, options?): Promise<PageDownloadReceipt>

分片写入PluginX客户端文件系统

ts
LFHttp.client.request / text / json(route, options?)

调用PluginX本地客户端路由的快捷方式

ts
LFHttp.client.isAvailable(options?): Promise<boolean>

探测本地客户端是否可用

ts
LFHttp.client.open(): boolean

尝试唤起本地客户端

ts
LFHttp.client.ensure(options?): Promise<boolean>

确保客户端已启动(探测 → 唤起 → 轮询就绪)

ts
LFHttp.client.getDownloadUrl(): Promise<string>

获取本地客户端下载地址

ts
LFHttp.client.fs.* (readFile / writeFile / readdir / stat / find / requestAccess / ...)

本地文件系统读写(经本地客户端执行,受权限名单管控)

ts
LFHttp.server.request / text / json(route, options?)

调用PluginX服务端路由的快捷方式

请求选项 LFHttpRequestOptions ​

request / text / json / stream 接受下表全部字段;client.* / server.* 共用其中大部分,差异见表格后的说明。所有字段均可选,options 传 null 等同于不传:

字段类型默认值说明
methodstringGETHTTP 方法,自动转大写
headersRecord<string, string>{}原样转发。浏览器保留头(Cookie / Referer / User-Agent / Origin / Sec-* / DNT / Accept-Charset / Date / Access-Control-Request-*)fetch 本身改不动,扩展代理通路会用一次性 declarativeNetRequest 规则在网络层写入,结果见响应的 headerOverrides。传 Cookie 会整体替换浏览器自动附带的 cookie,而不是追加。该规则只匹配原始 URL,重定向后的请求不再带这些头
credentials'include' / 'omit' / 'same-origin''include'默认携带目标域名已有的浏览器 cookie;传 'omit'(或 withCredentials: false)发起无 cookie 请求。client.* 固定不带、server.* 固定带,二者忽略此项
bodystring / object—字符串原样发送;其它值一律 JSON.stringify 后发送,所以 FormData / URLSearchParams / Blob / ArrayBuffer / TypedArray 会变成 {} 之类的错误内容,不受支持。不会自动设置 Content-Type(浏览器按字符串 body 默认 text/plain;charset=UTF-8):发 JSON 请显式加 'content-type': 'application/json',发表单请自行 new URLSearchParams(...).toString() 并设置 application/x-www-form-urlencoded。GET/HEAD 的 body 会被丢弃
throwOnHTTPErrorbooleantrue设为 false 时非 2xx 也正常 resolve,便于读取 4xx/5xx 的业务错误体;优先级高于 validateStatus。stream() 忽略此项
validateStatus(status, response) => boolean—自定义成功判定,只影响是否抛 HTTPError,不改变返回值的 ok 字段。stream() 忽略此项
maxBodyBytesnumber67108864(64MB)缓冲读取上限,超限 reject(不会静默截断);传 0 或 Infinity 关闭上限。对 stream() 只约束 text() / json() / arrayBuffer() / bytes(),chunks() 不设限
onProgress({ loaded, total, ratio }) => void—下载进度回调;响应无 Content-Length 时 total/ratio 为 null。对 request / text / json 以及 stream() 的缓冲读取有效(chunks() 不回调);client.* / server.* 忽略
streambooleanfalse仅 request / text / json:传 true 强制走流式传输,读完后仍返回同样的 Response 风格对象。响应声明的体积较大时运行时会自动切换,一般无需手动传
signalAbortSignal—request / text / json / stream 可用。传了 signal 的 request / text / json 自动走流式传输,abort 后请求真正取消,Promise 以 name: 'AbortError'、code: 'ABORTED'、cause: signal.reason 的错误 reject;signal 已经 abort 时立即 reject、不发请求。client.* / server.* 忽略

不支持的选项:timeout、mode、redirect、cache、referrer、keepalive 等其它 fetch 选项不会转发,重定向始终自动跟随。要超时可传 signal: AbortSignal.timeout(ms);注意标签页隐藏(切到别的标签、窗口被遮挡或最小化)时浏览器会节流这个计时器,实际触发可能晚数秒乃至一分钟,只适合当上限保护,不要依赖它做精确计时。client.* / server.* 发出后无法取消。

client.* / server.* 的差异:cookie 策略固定(见 credentials),onProgress、stream 与 signal 被忽略(始终一次性传输),其余字段相同。

响应对象 LFHttpResponseLike ​

字段类型说明
okboolean200-299 时为 true,始终由真实状态码决定
status / statusTextnumber / stringHTTP 状态码 / 原因短语。网络层失败(DNS 失败、连接被拒、离线等)不会返回响应对象,而是抛 NetworkError
headersRecord<string, string>小写键名的响应头对象,多值头以逗号连接
urlstring重定向后的最终 URL
bodyType'text' / 'base64'文本类内容(JSON/HTML/XML 等)为 text,其余二进制为 base64。响应没有 Content-Type 时按 text 处理,此时 arrayBuffer() 拿到的是 UTF-8 重新编码后的字节,二进制内容可能有损
headerOverrides{ applied: string[], dropped: string[] }仅当请求带了浏览器保留头时出现:applied 是已在网络层写入的头,dropped 是没能写入的头(非空即表示这些头没发出去)。一次性传输与流式传输(stream()、onProgress / signal / stream: true)的响应都提供
text() / json() / arrayBuffer()() => Promise从内存读取 body;二进制响应的 text()/json() 会 reject(改用 arrayBuffer());空体或非 JSON 调 json() 抛 SyntaxError

LFHttp.stream(url, options?) ​

响应头一到达就 resolve(body 尚未下载),可先检查 ok / status / total(Content-Length 字节数,未知时为 null)再决定消费方式。chunks() 返回增量异步迭代器(for await (const bytes of res.chunks()),每个分片是 Uint8Array,文本请用 TextDecoder 解码,无缓冲上限),与 text() / json() / arrayBuffer() / bytes() 互斥——body 只能消费一次。注意 stream() 不抛 HTTPError(throwOnHTTPError / validateStatus 无效),需自行检查 ok。

两种取消方式:

  • options.signal:响应头到达前 abort,stream() 本身以 code: 'ABORTED' reject;读到一半 abort,chunks() 迭代(以及 text() 等缓冲读取)抛 name: 'AbortError'、code: 'ABORTED' 的错误,不会把截断的 body 当作正常结束。
  • res.abort():取消下载,之后 res.aborted 为 true;正在进行的 chunks() 迭代会直接结束而不抛错,需要区分时检查 res.aborted。

决定不消费 body 时务必调用 res.abort():后台没有背压,未消费的数据会继续下载并堆积在页面内存里。

LFHttp.page.download(url, options?) ​

在目标标签页的 MAIN world 发起原生 fetch,因此沿用页面自身的 Cookie、Referer、Origin 和 CORS 判定;不会借扩展后台绕过 CORS,也不会把完整文件塞进 runInMainWorld 返回值。响应流先落到页面 origin 的 OPFS(默认目标);target:'client.fs' 时再以有界分片写入已授权的本地客户端绝对路径。别名:page.fetchToOPFS(url, pathOrOptions?)、page.fetchToClientFile(url, pathOrOptions, options?)。

选项类型默认值说明
target'opfs' / 'client.fs''opfs'OPFS 路径是当前页面 origin 内的相对路径;client.fs 必须传已获授权的绝对 path
path / namestring从 URL 推导文件名目标名称/路径;OPFS 拒绝 ..,client.fs 复用应用文件权限
signalAbortSignal—取消页面 fetch 与后续 client.fs 分片;失败时尽力清理临时/部分文件
onProgress({loaded,total,ratio,stage})=>void—stage 为 fetch 或 client.fs;Content-Length 未知时 total/ratio 为 null
maxBytesnumber不限超限抛 MAX_BYTES_EXCEEDED,不保留部分下载;须为正整数,0 / Infinity 表示不限
throwOnHTTPErrorbooleantrue非 2xx 时抛 HTTP_ERROR;设为 false 则照常落盘,状态码见 receipt 的 status
method / headers / body / credentials / redirect / referrer 等同页面 fetchcredentials: 'include'原样交给页面里的原生 fetch(也可以集中放进 fetch: { ... } 子对象)。这是页面自己的请求,浏览器保留头(Cookie/Referer/Origin 等)不能改写;body 只接受字符串;mode: 'no-cors' 会抛 INVALID_OPTIONS(不透明响应读不到 body)

返回小型 receipt(不含文件正文):{ ok, target, path, name, bytesWritten, status, statusText, contentType, url, nonWorkspaceSideEffect, advisory }。nonWorkspaceSideEffect.rollbackSupported 恒为 false,可供 Agent 回退预览提示:OPFS/client.fs 都在编辑器 workspace 之外,不会随会话回退自动删除或恢复。

page.download 不会产生浏览器下载记录。 它只有 OPFS 和 client.fs 两个落点,用户在浏览器「下载内容」里看不到任何条目;写 target:'browser' 会直接抛 INVALID_TARGET。需要让用户拿到文件请用下面的 page.saveAs。

LFHttp.page.saveAs(url, options?) ​

唯一会进浏览器下载管理器的 API,也是唯一能投递「页面自己 fetch 不到」的文件的 API。它不会比手写 stream + Blob 慢,不要为了速度去手写。

字节从不跨世界搬运,而且默认根本不落盘。 代理通路的字节直接进插件所在的 world;只要响应装得进 64 MB 内存预算,它就完全按 stream + Blob 的方式交付——收在内存里、包成 object URL、点一个合成的 a[download],一次磁盘写都没有,一次跨世界调用也没有。只有超出预算(或你用 path 要求保留 OPFS 副本)才转为落盘:按大分片攒着写、且边下边写,几 GB 的文件既不会撑爆内存,也不用为每个分片付一次磁盘往返;这种情况下才会把暂存文件读回来做成 object URL,并跨一次世界交付(只传路径和文件名)。页面通路的字节则由浏览器在页面 MAIN world 里 fetch 并落盘。

暂存有两条传输通路,transport:'auto'(默认)自动选:先按 page.download 走页面身份 fetch;如果页面读不到最终响应(PAGE_CORS_OR_NETWORK,典型是同源签名 URL 302 到没有 Access-Control-Allow-Origin 的 CDN),就自动改走扩展代理——代理不受页面 CORS 约束。页面那次尝试是在响应头阶段失败的,没有下载过 body,所以自动改道不浪费流量。也就是说调用 saveAs 之前不需要预判 CORS 行为,更不需要因为「CDN 跳转可能不透明」就退回手写整文件 Blob。

选项类型默认值说明
fileNamestring从 URL 推导交给浏览器的下载文件名(即 a[download]),也接受 name 作为别名。同名时浏览器仍可能自动加序号;因为字节来自 object URL,Content-Disposition 不生效
transport'auto' / 'page' / 'proxy''auto'auto 先试页面 fetch,被 CORS 挡则自动改走扩展代理;page 保留页面 Cookie/Origin/Referer 并直接抛出 CORS 错误(请求只有带页面身份才合法时用它);proxy 跳过页面尝试(URL 自带签名且已知落在 CDN 时用它)。其它值抛 INVALID_OPTIONS
pathstring不落盘额外保留一份 OPFS 副本的路径。传了就强制走落盘且不自动删——别为了「保险」随手传,那会放弃更快的纯内存交付。不传则装得进内存预算的响应完全不碰磁盘,超出的才暂存到 .lf-http-page-saveas/*.part 并在延迟后删除
cleanupDelayMsnumber120000点击后 object URL(以及未指定 path 时的临时文件)的存活时长。浏览器是惰性读取 blob 的,回收太早会让下载截断或失败;大文件/慢盘请调大
其余选项同 page.download—signal / maxBytes / onProgress / throwOnHTTPError / method / headers / body 透传给实际生效的暂存通路;signal 在两条通路上都生效,取消时以 code: 'ABORTED' reject。代理通路只认这几项,credentials / referrer 等页面 fetch 选项对它无效;它的 headers 与 LFHttp.stream() 一样,浏览器保留头会在网络层写入。onProgress 的 stage 为 fetch(页面通路)或 proxy(代理通路);target / destination 被忽略

receipt 里的 transport 字段告诉你实际走的是哪条通路('page' / 'proxy'),汇报时请照实说,不要假定页面 fetch 成功了。

多候选 URL 时优先选同源自签名的那条。 代理通路会带上浏览器里该目标域已有的 cookie,但不带页面身份(页面的 Origin/Referer 不会自动附带,只有你显式传的 header),所以有防盗链的 CDN 直链(签名/页面身份绑定)即使带 Referer 也可能 403。当一个资源给出多个候选 URL——比如媒体的 url_list 里同时有同源自签名端点(/aweme/v1/play/?... 这类)和第三方 CDN 直链(*vod*/*.cdn 域名)——优先用同源自签名 URL:它无论走 page 还是 proxy 通路都能下。直接取 url_list[0] 往往命中 CDN 直链而 403。只有防盗链 CDN URL 时,传上所需 Referer/header;仍 403 就如实报告是身份要求,别反复换 URL 写法或通路。

返回 page.download 的 receipt 再加上 { target:'browser', transport:'page'|'proxy', fileName, dispatched:true, stagedPath }。投递不等于完成:receipt 只证明浏览器接受了这次下载请求,不证明文件已落盘——浏览器仍可能弹「保留/丢弃」、拦截,或因同源多次自动下载而要求授权。需要有一个真实页面(run --tab、liveDebug 或已发布插件),没有标签页的纯 run 脚本用不了;页面通路还需要 LFRuntime.runInMainWorld 可用,缺它时 transport:'page' 会在下载任何字节之前失败,transport:'auto' 则直接改走代理通路。

LFHttp.websocket(url, options?) ​

选项类型默认值说明
mode'auto' / 'direct' / 'background''auto'direct 用浏览器原生 WebSocket(无法自定义握手头);background 在扩展后台建连,可注入握手头;auto 在 headers 非空、或执行环境不是 page 时自动选 background。其它值直接抛普通 Error
headersRecord<string, string>—自定义握手头(如 Authorization),需要 background 模式;显式 mode: 'direct' 时传 headers 会抛普通 Error
protocolsstring / string[]—Sec-WebSocket-Protocol 值
binaryType'arraybuffer' / 'blob'—二进制消息类型,仅 direct 模式生效;background 模式的二进制消息恒为 ArrayBuffer
timeoutnumber不超时连接超时毫秒数,0 表示不超时;超时 reject 普通 Error(不是 LFHttpWebSocketError)

返回连接对象:

  • 方法:send(data)(string / ArrayBuffer / TypedArray;background 模式不支持 Blob)、sendJson(value)、waitMessage({ timeout?, json?, predicate? })、close(code?, reason?)、on(type, cb) / off(type, cb)(别名 addEventListener / removeEventListener;type 为 'open' / 'message' / 'error' / 'close',on() 返回取消订阅函数)。没有 onMessage 之类的方法,收消息请用 socket.on('message', cb)。
  • 只读属性:readyState(0 连接中 / 1 已打开 / 3 已关闭;direct 模式为原生 socket 的值)、mode(实际选用的 'direct' / 'background')、url、lastError、rawSocket(仅 direct 模式为原生 WebSocket,否则为 null)。

握手失败时 reject LFHttpWebSocketError(字段见下方错误表):先收到 close 时 phase: 'open'(带服务器的 close code),先收到 error 时 phase: 'error'、code: 0——direct 模式下浏览器通常先触发 error,所以多见 'error'。

message 事件的形状是统一的:无论走哪条通路,回调都拿到 { data, bodyType? },读 event.data 即可;waitMessage() 则直接解析出消息本身。通路的传输字段(中继信封等)不会出现在事件对象里。

waitMessage() 从消息队列里取消息。 每条到达的消息都会进入内部队列,waitMessage() 取走队列里最早一条尚未被 waitMessage 取走、且满足 predicate(data) 的消息——包括调用之前就已到达的消息,以及已经派发给 on('message') 的消息。json: true 时对 data 做 JSON.parse。超时 reject 普通 Error;连接已关闭且没有匹配消息时 reject phase: 'close' 的 LFHttpWebSocketError;连接出过错时 reject 那次错误。队列最多保留最近 1000 条消息,超出时丢弃最旧的,所以消息量大时 waitMessage() 可能取不到很早以前的消息;实时处理每条消息请用 on('message', cb)。

direct 只在 page 环境可用。 后台应用沙箱和不带 --tab 的 run 编辑器沙箱都是 opaque origin,CSP 会拦掉原生 WebSocket——连自己写 new WebSocket(...) 也连不上。这些环境下保持默认 mode: 'auto' 即可(会自动选 background);若显式指定 direct,报错的 reason 会点明是哪个执行环境限制并提示改用 background。

close() 之后 close 事件不一定立刻到。 direct 模式(以及 run --tab 里的 background 模式)下,close(code, reason) 只是发出关闭请求,close 事件要等真正的关闭握手完成,对远端服务器可能是数秒;应用里(liveDebug / 已安装 / 后台应用)的 background 模式则在 close() 后立即触发 close 事件(code/reason 为你传入的值)。无论哪种,事件触发时 readyState 都会变为 3。别用固定 sleep 去断言,等事件(或等 waitMessage() 以 phase: 'close' reject)。

client / server 专属通路 ​

方法说明
client.request/text/json(route, options?)固定打本地PluginX客户端 http://127.0.0.1:41720,route 只填路径(如 /api/xxx,可带 query),不接受绝对 URL(传了直接抛 LFHttpUrlBlocked);后台自动注入 clientid 请求头;固定不带 cookie
client.isAvailable({ route?, method?, timeout? })探测客户端是否在运行(默认超时 3000ms):收到任意 HTTP 响应(含 4xx/5xx)即视为运行中
client.open()通过自定义协议唤起本地客户端(隐藏 iframe 触发,不导航当前页),返回是否成功触发
client.ensure({ timeout?, interval?, route?, probeTimeout? })先探测,未运行则唤起并按 interval(默认 500ms)轮询直到 timeout(默认 15000ms);返回最终是否就绪
client.getDownloadUrl()获取客户端安装包下载地址,用于引导未安装的用户
server.request/text/json(route, options?)后台自动拼接PluginX服务器基址,route 只填路径;固定携带该域名 cookie(credentials 无效),不注入 clientid

client.request/text/json 与 server.request/text/json 的每个请求都自动携带应用身份头:x-lf-app-id(应用 id)与 x-lf-app-status(liveDebug / dev / published),由运行时强制注入,options.headers 传入的同名头会被覆盖;background 还会按消息来源做浏览器背书的身份归因并追加 x-lf-app-attested(1=已认证 / 0=无法归因)。本地客户端与服务器可据此按应用与状态拒绝危险请求。固定路由的辅助方法不携带身份头:isAvailable / ensure 的根路径探测(GET/HEAD 打 / 且无请求体)、open(协议唤起)、getDownloadUrl 与 fs.*(独立后台通路),以及 LFUser 全部方法。

本地文件系统 LFHttp.client.fs.* ​

由本地PluginX客户端经 WebSocket 通道执行磁盘 I/O。路径必须是绝对路径,且须通过该应用的白/黑名单权限校验——首次使用先调用 requestAccess() 向用户申请。

安全边界:fs 请求按浏览器认证的应用身份归属,扩展后台只按调用方真实所属的应用查白/黑名单,脚本自行声明其它应用的 bundleId 无法借用别人的授权。client.fs 是插件应用唯一可用的本地文件通道;编辑器内部使用的本地文件访问、本地命令执行(localexec)与 CLI 注册表请求只接受 PluginX 编辑器页面发起,插件代码直接发这些消息会被拒绝(EACCES_PERMISSION)——访问编辑器工作区请用 LFRuntime.cli.register({ workspace: [...] }) + ctx.workspace.*。

方法说明
readFile(path, { offset?, length?, encoding?, maxBytes? })读文件,返回 { data, bytesRead, eof, size };整文件上限 16MB(超限抛 EMSGSIZE);encoding 默认 utf8,读二进制务必传 'binary'(data 为 Uint8Array)
writeFile(path, data, { offset?, append?, encoding?, chunkSize? })写文件,默认整文件覆盖,append:true 追加;返回 { bytesWritten },数据过大自动分片时返回 { bytesWritten, chunks };另有 appendFile() 等价于 append:true
readdir(path) / stat(path) / exists(path)列目录({ name, path, isDir, size, mtimeMs }[])/ 读元数据({ size, isDir, mtimeMs, mode },mode 是权限字符串如 -rw-rw-rw-)/ 判断存在(不抛错)
mkdir / remove / rename / copy / find建目录(默认递归)/ 删除(非空目录需 recursive:true)/ 重命名移动 / 复制(目标存在需 overwrite:true)/ 按 glob 查找文件名(如 find(dir, { pattern: '*.json', maxResults?, maxDepth? }))
open / read / write / seek / close句柄式顺序流,用于超大文件;open(path, flags) 返回句柄,务必在 finally 中 close(handle);read(handle, { length?, encoding? }) 默认 encoding: 'binary'、每次 2MB
readFileChunked / writeFileChunked大文件分片读写封装(默认 2MB/片,大于 16MB 必须走分片);读二进制务必 encoding:'binary' 或用 onChunk 回调,utf8 聚合模式对二进制字节有损
requestAccess(paths, { reason? })向用户申请目录访问权限,返回 { granted, mode, grantedPaths };拒绝/关窗/超时(120s)返回 granted:false,不抛错
getPermissions()读取当前应用的 { mode, whitelist, blacklist } 权限配置

fs 失败统一抛 LFHttpFsError(用 instanceof LFHttp.FsError 判断),按 error.code 分支:EACCES_PERMISSION(名单拒绝)、CLIENT_OFFLINE(客户端未连接,先 ensure();ensure() 只探测客户端的 HTTP 服务,返回 true 后 fs 通道可能仍在连接,遇到 CLIENT_OFFLINE 可短暂重试)、CONNECTION_LOST(通道中途断开)、TIMEOUT(单请求 30s 超时)、EMSGSIZE(超过大小上限,改用分片)、ENOENT / EEXIST / EACCES_OS 等系统错误、EBADF(无效/跨应用句柄)、USER_DENIED(授权被拒)。

错误类型 ​

错误字段说明
HTTPErrorstatus / statusText / headers / url / response非 2xx 时默认抛出;await error.response.text() 仍可读取错误体;用 instanceof LFHttp.HTTPError 判断(error.name === 'HTTPError')
LFHttpUrlBlockedreason / host / protocolURL 被安全策略拒绝。这是普通 Error,没有对应的导出类,请按 error.name === 'LFHttpUrlBlocked' 判断。reason 为稳定枚举:invalid url / unsupported protocol / missing host / local client port。只有页面侧前置校验抛出的错误保证带 reason / host;client.* / server.* 传绝对 URL、以及重定向复核等由扩展后台拦下的情况只保证 name 与 message(重定向复核的 message 末尾带 after redirect)
LFHttpWebSocketErrorcode / reason / wasClean / phase / urlphase 为 'open'(握手阶段先收到 close)、'error'(连接错误,code 为 0)或 'close'(waitMessage() 等待时连接已关闭);用 instanceof LFHttp.WebSocketError 判断
LFHttpPageDownloadErrorcode / target / pathpage.download / page.saveAs 失败;用 instanceof LFHttp.PageDownloadError 判断。取消时 name === 'AbortError'、code === 'ABORTED'。code:ABORTED、PAGE_CORS_OR_NETWORK、HTTP_ERROR、MAX_BYTES_EXCEEDED、QUOTA_EXCEEDED、INVALID_TARGET、INVALID_OPTIONS、OPFS_UNAVAILABLE、OPFS_READ_FAILED、CLIENT_OFFLINE、TAB_UNAVAILABLE、TAB_NAVIGATED、TAB_OR_BACKGROUND_UNAVAILABLE、MAIN_WORLD_UNAVAILABLE、MAIN_WORLD_EXEC_FAILED、DISPATCH_FAILED,兜底为 PAGE_DOWNLOAD_FAILED
LFHttpFsErrorcode / path / opclient.fs.* 失败;用 instanceof LFHttp.FsError 判断,code 见上一节
NetworkError—DNS 失败 / 连接被拒 / 离线等传输层失败。普通 Error,按 error.name === 'NetworkError' 判断;不会以 status: 0 的响应形式返回
AbortErrorcode / cause通过 options.signal 取消 request / text / json / stream:name === 'AbortError'、code === 'ABORTED',cause 为 signal.reason

其它失败(响应超过 maxBodyBytes、stream body 重复消费、WebSocket 连接超时或 waitMessage 超时等)抛普通 Error,message 说明原因。

示例

js
// @require http

const config = await LFHttp.json("https://example.com/config.json");
const page = await LFHttp.text("https://example.com/page.html");

// 发送 JSON:对象 body 会被 JSON.stringify,但 Content-Type 要自己写
const created = await LFHttp.json("https://example.com/api/items", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: { name: "demo" },
});

// 10 秒超时:abort 后以 code === "ABORTED" reject
try {
  const report = await LFHttp.json("https://example.com/api/report", {
    signal: AbortSignal.timeout(10000),
  });
  LFRuntime.print(report);
} catch (error) {
  if (error.code === "ABORTED") LFRuntime.print("请求超时已取消");
  else throw error;
}

// 流式响应:每个 chunk 是 Uint8Array
const stream = await LFHttp.stream("https://example.com/events");
if (!stream.ok) {
  stream.abort(); // 不消费 body 时务必 abort
} else {
  const decoder = new TextDecoder();
  for await (const chunk of stream.chunks()) {
    LFRuntime.print(decoder.decode(chunk, { stream: true }));
  }
}

// WebSocket
const socket = await LFHttp.websocket("wss://example.com/ws");
const off = socket.on("message", (event) => LFRuntime.print(event.data));
socket.send("ping");
const firstReply = await socket.waitMessage({ timeout: 5000 });
LFRuntime.print(firstReply);
off();
socket.close();