PythonIDE Docs
中文
简体中文

http_server

本地 HTTP 文件服务器。

http_server 是按 Python/MiniApp 运行会话隔离的原生静态文件服务器。它只绑定 127.0.0.1,以恒定内存分块提供授权目录中的普通文件,支持 GETHEAD 和单个 HTTP Byte Range。每次启动都会生成新的高熵访问令牌;没有令牌的本机请求也只能得到 404

边界:这是前台、会话内的 loopback 文件服务,不是局域网或公网 Web Server,也不是后台传输任务。MiniApp 停止、会话过期或调用 stop() 时,listener 和全部连接都会被取消。

声明式 MiniApp 同时需要网络与文件能力:

json
{
  "capabilities": ["network", "file_system"]
}

network 允许创建 loopback listener,file_system 允许读取服务根目录;两者不是二选一。

#模块概览

说明
导入import http_server
通用地址http://127.0.0.1:<port>/<token>/;URLSession、WebKit 与普通 HTTP 客户端均可使用
浏览器地址http://<token>.localhost:<port>/;用于 AppUI WebView/Safari,并支持 /style.css 这类根相对资源
方法GETHEAD
Range单个 bytes= 范围;不支持 multipart ranges
根目录MiniApp 默认当前工作区;普通脚本保留 Documents 默认值
文件只提供普通文件;不列目录,不跟随符号链接
流式64 KiB 文件块,发送完成后才读取下一块,内存不随文件大小增长
生命周期每个运行 owner 一台 server;重复 start() 串行停止旧 server 后启动新 server

#快速开始

推荐使用 port=0 让系统选择空闲端口:

python
import http_server

try:
    url = http_server.start(port=0)  # 默认 url_mode="universal"
    print("服务地址:", url)
    print(http_server.status())
finally:
    http_server.stop()

若返回 http://127.0.0.1:49152/4f8c.../,文件 report.pdf 的地址就是:

text
http://127.0.0.1:49152/4f8c.../report.pdf

请相对返回值追加文件名,不要自行去掉令牌或用以 / 开头的路径覆盖它。返回地址的逻辑根 GET/HEAD 是健康检查,不会列出目录内容。


#AppUI 示例

启动、停止和状态查询放在用户回调中,不要放进 body()

python
import appui
import http_server

state = appui.State(
    running=False,
    url="—",
    requests="0",
    status="点击启动本地服务",
)


def refresh_status():
    try:
        info = http_server.status() or {}
        state.batch_update(
            running=bool(info.get("running")),
            url=info.get("url") or "—",
            requests=str(info.get("requests_served", 0)),
        )
    except http_server.HttpServerError as exc:
        state.status = f"查询失败: {exc} ({exc.code})"


def start_server():
    try:
        url = http_server.start(port=0)
        refresh_status()
        state.status = f"已启动: {url}"
    except http_server.HttpServerError as exc:
        state.status = f"启动失败: {exc} ({exc.code})"


def stop_server():
    try:
        http_server.stop()
        refresh_status()
        state.status = "已停止"
    except http_server.HttpServerError as exc:
        state.status = f"停止失败: {exc} ({exc.code})"


def body():
    return appui.NavigationStack(
        appui.Form([
            appui.Section("服务状态", [
                appui.LabeledContent("运行中", value="是" if state.running else "否"),
                appui.LabeledContent("地址", value=state.url),
                appui.LabeledContent("已完成请求", value=state.requests),
            ]),
            appui.Section("操作", [
                appui.Button(
                    "停止服务" if state.running else "启动服务",
                    action=stop_server if state.running else start_server,
                ).button_style("bordered_prominent"),
                appui.Button("刷新状态", action=refresh_status),
                appui.Text(state.status).foreground_color("secondaryLabel"),
            ], footer="仅绑定 127.0.0.1;MiniApp 停止时自动清理。"),
        ]).navigation_title("HTTP 服务")
    )


appui.run(body, state=state)

若要让本地 HTML、CSS、JavaScript、媒体使用常规 Web 根路径,显式请求浏览器地址:

python
browser_url = http_server.start(port=0, url_mode="browser")
preview = appui.WebView(url=browser_url + "index.html")

返回的 browser_url 是服务基地址;它的根路径保留为 JSON 健康检查,所以预览网页时必须追加真实入口文件(例如 index.html)。宿主只为当前运行会话注册两段相互绑定的精确访问范围:浏览器地址的 scheme + token host + port,以及通用地址的 127.0.0.1 + port + /token/ path prefix。两种地址都可交给 AppUI WebView,无需把动态令牌写进 allowedNetworkHosts,也不会放开普通 http://localhost 或该端口的其他路径;stop()、重新启动或会话结束后授权立即失效。含 /style.css 这类根相对资源的站点仍应选择 browser


#API 参考

#start(port=8080, root_dir=None, *, url_mode="universal")

启动当前运行会话的 server,并按 url_mode 返回一种经过令牌保护的 URL。

python
url = http_server.start(port=0)
url = http_server.start(port=18080, root_dir="/path/to/folder")
browser_url = http_server.start(port=0, url_mode="browser")
# 网页入口:browser_url + "index.html"
参数说明
port0...655350 由系统选择可用端口,默认保留为 8080 以兼容旧脚本
root_dir已存在且已授权的目录;支持字符串和 os.PathLike
url_mode仅可为 "universal""browser";keyword-only,默认 "universal"

对同一运行再次调用 start() 时,只影响该运行拥有的旧 server,不会停止其他 MiniApp/脚本会话的 server。替换请求从进入 Python facade 起即采用 fail-closed 语义:端口、路径对象或 url_mode 的 Python 预校验失败,也会停止并撤销旧 server;通过预校验后,原生端仍会在自己的 owner 串行门内先停止旧 server,再校验根目录并启动新 listener。新 listener 必须成功进入 ready 状态后才返回真实端口。任何阶段失败时,该运行都保持停止状态,不会恢复已经撤销的旧 server。

#stop()

幂等停止当前运行的 server,并中断 listener、活动连接、流读取和会话资源注册。重复调用安全。

#status()

保留旧字段并增加有界诊断:

字段说明
running当前运行是否拥有 ready server
port实际绑定端口;停止时为 0
urlloopback URL;停止时为 None
browser_url当前 server 的精确 localhost 浏览器 origin;仅运行时提供
root_dir已授权、已解析的根目录
server_id当前 server 实例 ID;运行时提供
active_connections当前活动连接数
requests_served已完整发送的请求数
bytes_sent已确认发送的 HTTP 字节数
started_atISO 8601 启动时间;运行时提供
bind_host固定为 127.0.0.1
range_policy固定为 single

#HttpServerError

原生启动、权限或协议桥错误会抛出 HttpServerError。请按 exc.code 分支,不要匹配本地化消息。


#HTTP 行为

请求结果
对返回 URL 执行 GET200 JSON 健康响应
对返回 URL 执行 HEAD与健康响应相同 headers,不发送 body
返回 URL + path/file.bin200,流式发送普通文件
对文件 URL 执行 HEAD200 与准确 Content-Length,无 body
GET + Range: bytes=100-199206 + Content-Range,流式发送一个范围
无效、越界或多个 Range416 + Content-Range: bytes */size
其他方法405 + Allow: GET, HEAD
文件缺失404 JSON 错误

支持 bytes=start-endbytes=start-bytes=-suffixLength。只实现一个 range 是通用的确定性资源策略,不是面向某类 MiniApp 的补丁;视频预览、断点读取、PDF 和大文件客户端都能复用。

成功与错误的 HEAD 都不会发送 body。每个连接只处理一个请求并明确 Connection: close;最后一个响应块使用 Network.framework 的 final context 发送 TCP FIN,并在最多 2 秒的有界收尾窗口后强制回收异常客户端。常见扩展名会返回原生 MIME;未知类型为 application/octet-stream,同时发送 X-Content-Type-Options: nosniff

#安全与资源边界

  • listener 的必需本地端点固定为 127.0.0.1;不是 0.0.0.0,也不会公布 LAN 地址。
  • 通用 URL 以系统安全随机数生成的 32 位十六进制临时令牌作为首个 path segment;浏览器 URL 以同一令牌作为精确 .localhost 子域。未授权 origin/path 返回 404,不会暴露目录是否存在。
  • 两种 URL 的 AppUI WebView 放行都由当前 native lease 精确注册,并由 Python→IR 前置策略和 Swift/WebKit 最终策略分别校验;它不能泛化为任意 localhost、其他 token path 或端口,停止、替换 server 或会话撤销时同步撤销。
  • root_dir 必须是当前运行的工作区或授权只读根;声明式 MiniApp 必须同时具有 networkfile_system
  • URL 百分号解码后再验证;拒绝空组件、...、反斜杠、NUL 和越界路径。路径最多 128 个组件,每个组件最多 255 个 UTF-8 字节。
  • 声明式 MiniApp 的工作区/临时只读根会在 Python 启动前由宿主打开并随 native lease 持有目录 descriptor;start() 只从该可信 descriptor 按 lexical 相对组件继续 openat(..., O_NOFOLLOW),授权与打开不是两个可被竞态插入的步骤。iOS 的系统 /var//tmp alias 只在宿主建 lease 时规范化,MiniApp 可控的子路径不会先做 realpath。
  • 每一级请求路径继续使用目录 descriptor 与 openat(..., O_NOFOLLOW);最终对象必须是单链接普通文件。目录、FIFO、设备、socket、symlink 和 hard link 均不会被提供。
  • 请求 headers 上限 64 KiB,header 数量有界;完整 header 还必须在 10 秒绝对期限内到达。
  • 每台 server 最多 32 个活动连接;连接空闲/发送停滞约 30 秒取消。
  • 完整响应以 TCP FIN 优雅关闭写端;对端未及时结束时,2 秒后释放连接槽,不会无限等待。
  • 文件按 64 KiB 分块,并等待 Network.framework 的发送完成回调后读取下一块。
  • 已发送 200/206 headers 后若文件被截断或读失败,会关闭连接,不会再写第二套 500 headers 污染响应。
  • MiniApp 会话过期会阻止后续读取并关闭 server;旧运行无法借新运行继续访问文件。
  • URL 中的令牌属于当前会话的临时访问凭据;不要持久化、公开分享或写入长期日志。

#常见错误码

HttpServerError.code含义
invalid_input端口或 bridge 请求无效
invalid_path请求中的路径参数无效
invalid_rootroot_dir 不存在、不是目录或无法安全打开
symlink_not_allowed根目录或请求路径包含不允许的符号链接
file_access_denied根目录不在当前运行授权范围
network_not_allowed当前会话未获网络/server 权限
port_in_use指定端口已占用
server_limit达到宿主的并发 server 上限
startup_timeout / listener_unavailable / listener_cancelledlistener 未能进入 ready 或启动期间已被撤销
miniapp_session_expired启动期间或运行期间会话已过期
unavailable底层 Network.framework 启动失败
bridge_unavailable / empty_response / invalid_native_responsePython 与原生 bridge 不可用或返回不符合契约

HTTP 请求级错误以 HTTP status 和 JSON code 返回给客户端,不会作为 Python HttpServerError 抛出。

#常见错误

错误写法后果修正
body()start()SwiftUI/AppUI 重建时重复启动放进按钮回调
固定一个热门端口容易出现 port_in_use优先 port=0
只声明 network没有读取根目录的文件能力同时声明 networkfile_system
把它当 LAN 服务器127.0.0.1 不接受远端连接远程传输使用 SSH 或经审核的网络方案
用它代替后台下载/上传App/会话停止后 server 被清理使用 background_download 等持久任务 API
自己拼接未编码路径空格、Unicode 或 # 产生错误 URL对每个 URL path segment 做百分号编码
"/file" 覆盖通用 URL 的 path丢失访问令牌并收到 404使用相对 segment,或为网页选择 url_mode="browser"
browser_url 保存到下次运行token 与 server 生命周期已结束每次运行重新调用 start()

#相关文档

文档用途
networkHTTP 客户端、上传下载和流式响应
background_download可恢复的后台文件下载
archive下载或分享 ZIP/IPA 后原生处理
MiniApp 原生能力network + file_system 能力与会话规则

#预期效果

旧的 start() / stop() / status() 写法保持可用;新代码可用 port=0、HEAD、Range 和诊断字段。无论文件多大,正常传输内存都保持有界;路径逃逸、symlink、hard link、非普通文件、过量 headers、连接过多和旧会话都会被原生内核拒绝或清理。