URL-to-PNG 2.1.4 API 使用详解

URL-to-PNG v2.1.4 接口实测手册:端点定位、全参数逐项回显、输出尺寸行为矩阵、错误码汇总,并沉淀 /route 404、首图 5 秒、forceReload 缓存四个排障实录。

URL-to-PNGURL-to-PNGAPIREST APIPlaywright2026/10/31

URL-to-PNG 2.1.4 API 使用详解

目标:给出 URL-to-PNG v2.1.4 截图服务的完整接口实测手册——端点定位、全部查询参数的逐项实测回显、尺寸行为矩阵、错误码与缓存机制,并沉淀"接口路径 404""首图 5 秒"两个典型排障实录。全文命令与回显均在 Ubuntu 22.04 + URL-to-PNG v2.1.4 环境实测通过(服务监听 8136 端口),截图结果均以 file 命令验证实际像素尺寸。

一、环境信息

  • 操作系统:Ubuntu 22.04 LTS(x86_64)
  • 服务:URL-to-PNG v2.1.4(官方最新稳定版)
  • 服务地址:http://localhost:8136(服务内默认端口 3089)
  • 存储后端:filesystem(缓存目录持久化,重启不丢)
  • 客户端工具:curl(7.81.0)、file 命令(验证 PNG 实际尺寸)
  • 部署方式:Docker Compose(部署与运维操作见《URL-to-PNG Docker Compose 部署指南》)

二、前置条件

  1. 服务就绪:部署完成且健康检查通过(部署步骤见《URL-to-PNG Docker Compose 部署指南》):
curl -s http://localhost:8136/ping
# "pong"
  1. 认证方式:接口无内置认证——只要能访问端口即可调用。生产环境必须置于反向代理/网关之后做认证与限流。若需防止截图地址被第三方套用,可配置 CRYPTO_KEY 启用加密请求(见 5.5 节)。

  2. 客户端工具:curl 即可(本文全部示例);浏览器 <img> 标签同样可直接把接口当图片源。

三、API 总览

URL-to-PNG 是单端点截图服务,全部 API 如下(v2.1.4 实测确认,无版本前缀、无 REST 资源层):

端点方法功能成功响应
/GET截图主接口(全部功能都在这里)200 + image/png 二进制
/pingGET健康检查200 + "pong"
/metricsGET浏览器连接池指标(METRICS=true 时)200 + JSON

请求约定:

  • 协议/格式:纯 HTTP GET + 查询字符串,无请求体;响应为 PNG 二进制流(错误时为 JSON);
  • 参数传递:全部经查询字符串,布尔值只认字符串 true(forceReload=1 等同 false,实测见 5.1 节);
  • 缓存约定:同参数请求幂等返回同一份缓存图,响应带 Cache-Control: public, max-age=86400, immutable(实测响应头见 5.3 节);
  • 限流:服务端无限流;并发由浏览器池承载(默认常驻 2、上限 10,/metrics 可观测),队列超出 POOL_MAX_WAITING_CLIENTS(默认 50)后请求等待。

四、首次调用

最小可用调用(默认参数,输出 250x250):

curl -s 'http://localhost:8136/?url=https://example.com' -o example.png
file example.png
# example.png: PNG image data, 250 x 250, 8-bit/color RGB, non-interlaced

缺 url 参数时的 400 报错(实测):

curl -s 'http://localhost:8136/'
# {"message":"Invalid query parameters:  (url - Required)"}

五、各接口域详解

5.1 截图接口 GET /:参数全集与逐项实测

参数总表(zod 校验语义 + 官方 usage 文档 + 本文逐项实测):

参数类型/默认说明
url必填目标网页地址,强制 http/https 协议
width数字,默认 250输出图宽(超过 1920 被钳制)
height数字,默认 250输出图高(生效条件特殊,见 5.2 行为矩阵)
viewportWidth / viewportHeight数字,默认 1080渲染视口尺寸
viewPortWidth / viewPortHeight数字大写 P 的旧拼写,仍生效但日志告警
forceReload布尔,默认 false强制重新渲染
isMobile布尔,默认 false移动端 UA 模拟
isFullPage布尔,默认 false整页截图
isDarkMode布尔,默认 false深色配色
deviceScaleFactor数字,默认 1设备像素比
hash字符串加密请求载体(需 CRYPTO_KEY)

逐项实测:

① 布尔参数只认字符串 true。forceReload=1 不报错也不强刷(按 false 处理,返回缓存图):

curl -s 'http://localhost:8136/?url=https://example.com&width=250&forceReload=1' -o /dev/null -w 'http=%{http_code} time=%{time_total}s size=%{size_download}\n'
# http=200 time=0.989585s size=13812

② 缓存命中与强制刷新的耗时对照(同参数连发两次,首次为真实渲染):

curl -s 'http://localhost:8136/?url=https://example.com&width=600' -o /dev/null -w 'first  time=%{time_total}s size=%{size_download}\n'
# first  time=0.977933s size=61832
curl -s 'http://localhost:8136/?url=https://example.com&width=600' -o /dev/null -w 'second time=%{time_total}s size=%{size_download}\n'
# second time=0.001507s size=61832

第二次 1.5ms 返回且字节一致——缓存幂等是接口的核心契约。

③ 尺寸钳制:width=3000 实际输出 1920x1920(MAX_DIMENSION=1920 静默钳制):

curl -s 'http://localhost:8136/?url=https://example.com&width=3000&forceReload=true' -o clamp.png
file clamp.png
# clamp.png: PNG image data, 1920 x 1920, 8-bit/color RGB, non-interlaced

④ deviceScaleFactor 不改变输出像素尺寸(只提升清晰度,文件体积增大):

curl -s 'http://localhost:8136/?url=https://example.com&width=400&height=300&deviceScaleFactor=2&forceReload=true' -o dpr2.png
file dpr2.png
# dpr2.png: PNG image data, 400 x 400, 8-bit/color RGB, non-interlaced

注意上例另一个要点:height=300 并未生效(输出仍是 400x400 而非 800x600 或 400x300)——height 的真实行为见 5.2。

⑤ 旧拼写 viewPortWidth(大写 P)仍生效,服务日志输出 warn(实测日志行,JSON 格式):

{"level":40,"time":1793408177889,"pid":1,"hostname":"...","name":"url-to-png","msg":"'viewPortWidth' is deprecated, please use 'viewportWidth'"}

⑥ 协议校验:file:// 与 ftp:// 一律 400(v2.0.3 安全修复的实测验证):

curl -s 'http://localhost:8136/?url=file:///etc/passwd'
# {"message":"Invalid query parameters:  (url - must start with http or https)"}

curl -s 'http://localhost:8136/?url=ftp://example.com'
# {"message":"Invalid query parameters:  (url - must start with http or https)"}

⑦ 不可解析域名:参数合法但渲染失败,返回 500 + Playwright 错误原文:

curl -s 'http://localhost:8136/?url=https://nonexistent.invalid'
# {"message":"page.goto: net::ERR_NAME_NOT_RESOLVED at https://nonexistent.invalid/\nCall log:\n  - navigating to \"https://nonexistent.invalid/\", waiting until \"domcontentloaded\"\n"}

5.2 输出尺寸行为矩阵(height 的真实语义)

官方文档称 height 是"输出截图的高度,默认 250",但 v2.1.4 实测其只在特定组合下生效。以下矩阵全部实测(file 命令回显真实像素):

请求参数组合实际输出结论
(无宽高)250 x 250默认宽高注入且两者都参与
width=600600 x 600只有 width → 视口 1080x1080 按宽等比缩放
width=400&height=300400 x 400height 被忽略,按视口比例等比
width=400&height=800400 x 400同上,height 完全不参与
width=400&viewportWidth=800&viewportHeight=600400 x 300视口比例决定输出比例,width 控制大小
height=300(单独)1080 x 1080只有 height → 返回视口原图,height 不缩放
height=300&viewportWidth=800&viewportHeight=600800 x 600同上,输出=视口原始尺寸
isFullPage=true(无宽高)250 x 250全页模式注入默认 width=250、按宽等比
isFullPage=true&height=400(无 width)400 x 400全页且无 width 时 height 才生效(按高等比)
width=600&height=400600 x 600width 与 height 同时给 → width 优先

机制解读(源码核对):截图后统一走 sharp.resize() 缩放——非全页模式只把 width 传给缩放(输出比例由视口决定);height 仅在「isFullPage=true 且未给 width」时作为缩放目标。想要精确的宽高比,正确姿势是同时调 viewportWidth/viewportHeight 控制比例、width 控制大小,而不是依赖 height。

5.3 响应头与缓存语义

成功响应的完整响应头(实测,已略去日期行):

HTTP/1.1 200 OK
cache-control: public, max-age=86400, immutable
content-type: image/png
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: cross-origin
origin-agent-cluster: ?1
referrer-policy: no-referrer
strict-transport-security: max-age=15552000; includeSubDomains
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-xss-protection: 0
content-length: 13812
Connection: keep-alive
Keep-Alive: timeout=5

要点:cache-control 可经 CACHE_CONTROL 环境变量调整;immutable 意味着浏览器/CDN 24 小时内不回源,改图后调用方需换参数(或服务端换 URL)才能拿到新图。

5.4 排障实录一:接口路径 404("/route" 误区)

现象:社区里常有人把截图端点写作 /route(把"截图路由"当成了路径)。实测:

curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:8136/route?url=https://example.com'
# 404
curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:8136/api/v1/route?url=https://example.com'
# 404

定位:官方 getting-started 与 usage 文档中所有示例均为 http://localhost:3089?url=... 形式——截图端点就是根路径 /,没有任何路径前缀,"route"只是对"截图功能"的口语化指代。服务仅注册 /、/ping、/metrics 三个路由,其余一律 404。

解决:直接请求根路径即可:

curl -s 'http://localhost:8136/?url=https://example.com&forceReload=true' -o ok.png -w '%{http_code}\n'
# 200
file ok.png
# ok.png: PNG image data, 250 x 250, 8-bit/color RGB, non-interlaced

5.5 排障实录二:加密请求(hash)的开启与禁用边界

未配置加密密钥时传 hash(实测 400):

curl -s 'http://localhost:8136/?url=https://example.com&hash=abc123'
# {"message":"This server is not configured for encryption"}

配置 CRYPTO_KEY 后边界反转:服务只接受 hash=str-enc:... 形式的加密参数(AES-GCM 256,密钥为浏览器 console 生成的 JWK JSON 串,明文参数一律 400 This server must use encryption)。加密串由官方 @jmondi/string-encrypt-decrypt 库对参数 JSON 加密生成。适合"截图地址暴露给不可信方"的场景;普通内网调用无需开启。

5.6 健康检查与指标接口

curl -s http://localhost:8136/ping
# "pong"

curl -s http://localhost:8136/metrics
# {"poolMetrics":{"spareResourceCapacity":8,"size":2,"available":2,"borrowed":0,"pending":0,"max":10,"min":2}}

/metrics 字段速查:size 池中实例数、available 空闲数、borrowed 被占用数、pending 排队请求数、min/max 池上下限(对应 POOL_MIN/POOL_MAX 环境变量)。容量排障看 pending:持续大于 0 说明渲染吞吐饱和,需调大 POOL_MAX 或横向扩实例。

5.7 排障实录三:"首图 5 秒"定位与结论

现象:对含慢资源页面的首次截图请求耗时约 5 秒,而服务日志显示页面事件早已完成,怀疑浏览器冷启动或参数问题。

分层实测(三层假设逐一排除,数据均为实测回显):

  1. 假设"浏览器冷启动"——池冷(服务刚重启)与池热同参数强刷对照:
# 池冷(重启后首次 forceReload 渲染)
time=5.108944s size=70122
# 池热(连续第 N 次同参数 forceReload)
time=5.094541s size=70122

两者几乎无差 → 排除冷启动。

  1. 假设"页面加载等待语义"——在服务运行环境用 Playwright 原语直测:chromium.launch 33ms、newPage 21ms、goto(slowpage, domcontentloaded) 仅 10ms 即返回 → 排除 goto 阻塞。

  2. 定位真凶——page.screenshot() 隐式等待:goto 按 DCL 返回后,screenshot 调用单独耗时 5058ms,且恰等于页面内 5 秒慢图(服务端延迟 5 秒返回的 PNG)的加载耗时;对照组无资源页 screenshot 仅 51ms。

结论:Playwright 1.55.1 的 page.screenshot() 会隐式等待页面全部网络资源加载完成——首图耗时 ≈ 页面最慢资源耗时。验证闭环:慢图页 5.095.11s、纯静态页 0.0810.127s、带 setTimeout 注入 JS 的页面 0.127s(JS 注入不构成等待项)、带固定尺寸属性的慢图页同样 5.10s(与布局无关)。工程对策:缓存命中则毫秒级返回(1.8~14ms),因此预热缓存(部署后先对慢页面主动请求一轮)比调 BROWSER_WAIT_UNTIL 更有效;forceReload 每次都会重新支付这一成本,慢页慎用强刷。

5.8 排障实录四:forceReload 与 imageId 的版本行为

v2.1.4 实测:forceReload 参与缓存键(imageId)计算——?url=https://example.com&width=600 与 ?url=https://example.com&width=600&forceReload=true 生成两个不同的缓存文件并长期并存(缓存目录实测样例):

10-30-2026.httpsexamplecom_width-600_height-400.png
10-30-2026.httpsexamplecom_width-600_height-400_forcereload-true.png

即每次强刷都会落一个新文件且无自动清理机制——强刷不会复用也不会删除旧缓存。另实测 imageId 的日期前缀为美式 10-30-2026(M-D-YYYY)格式,参数按出现顺序拼接、不排序。对比仓库 main 分支源码(jsdelivr 拉取核对):main 已把 url/hash/forceReload 列入 NON_CACHE_PARAMS(强刷不再产生独立缓存文件)、参数排序拼接、日期前缀改为 ISO 格式——升级到 main 之后版本可获得更干净的缓存行为,v2.1.4 用户需知晓强刷的磁盘代价(应对:定期按 8.2 节清理缓存目录)。

六、通用约定(分页/过滤/排序)

本服务为单端点截图 API,无列表型资源,分页/过滤/排序约定不适用;等效的"过滤"能力即上文查询参数(尺寸、视口、模式开关等),每次请求只产出一图,无翻页语义。

七、错误码汇总

全部错误码实测汇总(响应体均为 JSON,Content-Type: application/json):

HTTP 码触发条件实测报文
400缺 url / 协议非法{"message":"Invalid query parameters: (url - Required)"}
400参数非法(类型/取值){"message":"Invalid query parameters: (url - must start with http or https)"}
400传 hash 但服务未配置加密{"message":"This server is not configured for encryption"}
400服务配置加密后仍用明文参数{"message":"This server must use encryption"}
403命中 ALLOW_LIST/BLOCK_LIST 名单{"message":"Access to this URL is forbidden"}
404路径不存在(如 /route)空 body
500渲染超时 / 域名不可达等{"message":"page.goto: Timeout 2000ms exceeded.\nCall log:\n - navigating to \"https://example.com/\", waiting until \"domcontentloaded\"\n"}

八、常见问题

Q1:截图是旧版页面,怎么强制更新?

带 forceReload=true 重新渲染。注意三点:布尔只认字符串 true;v2.1.4 强刷会产生新的缓存文件(见 5.8);响应头带 immutable 的话浏览器侧也要换 URL 才能绕过本地缓存。

Q2:想要 16:9 的 1280x720 输出,参数怎么给?

width=1280&viewportWidth=1280&viewportHeight=720——用视口控制比例、width 控制大小(原理见 5.2 矩阵);单独 height=720 不会生效。

Q3:慢页面首图要 5 秒以上,能加速吗?

首次渲染耗时 ≈ 页面最慢资源耗时(5.7 节实测结论)。加速路径:预热缓存(部署后主动请求一轮)、避免对慢页频繁 forceReload、目标页允许时用 BROWSER_WAIT_UNTIL=domcontentloaded(本就默认)而非 networkidle。

Q4:请求 403 了,但域名明明在名单里?

检查是否用了子域名:名单按 host 精确匹配,example.com 不匹配 www.example.com(实测两者回显一致)。把实际 host 逐个补进名单。

Q5:/metrics 返回 404?

METRICS 未开启(默认 false)。在部署配置中设 METRICS: "true" 后重建生效(操作见《URL-to-PNG Docker Compose 部署指南》)。

九、参考链接