imgproxy 4.0 API 使用详解
imgproxy v4.0.19 HTTP API 实测详解:HMAC 签名逐字段拆解与 Python/Node/Go/openssl 四语言实现交叉验证、Base64 与 plain 源 URL 模式及格式扩展名优先级、rs/c/g/rot/dpr/q/bg/bl/sh/wm/sm/mb/pr/exp/cb/fn 处理选项逐组实测、X-Origin 调试头与 ETag 304 缓存协商、400/403/404/422/429/504 错误码与限流超时排障闭环
imgproxy 4.0 API 使用详解
目标:吃透 imgproxy v4.0.19 对外暴露的 HTTP API——HMAC 签名机制(逐字段拆解 + Python/Node.js/Go/openssl 四语言实现交叉验证)、签名 URL 与源 URL 三种形态、处理选项(尺寸/裁剪/旋转/dpr/格式/质量/背景/模糊锐化/水印/预设/过期等)逐组实测、响应头与缓存协商、错误码与限流超时排障。所有结论均来自真实服务的 curl 实测,回显逐行照录。前置知识:《imgproxy 4.0 介绍与选型指南》完成选型、《imgproxy 4.0 Docker Compose 部署指南》完成部署、《imgproxy 4.0 配置参数详解》讲清环境变量,本文聚焦"发请求"这一层。
一、环境信息
- 操作系统:Ubuntu 24.04.5 LTS(x86_64)
- 被测服务:imgproxy v4.0.19,监听
127.0.0.1:8140 - 图源站:静态图片服务(Nginx),本机端口
8141,站内以http://origin/引用;imgproxy 本地图源根目录/images - 客户端:curl 8.5.0(Ubuntu 24.04 自带)
- 本地文件签名脚本:
sig.py(见 2.3 节)
测试图片(均在图源根目录 /images,本地与 http://origin/ 同名同内容):
| 文件 | 规格 | 用途 |
|---|---|---|
photo-1920x1080.jpg | 1920×1080,104248 字节 | 主测试图(JPEG,含 Exif) |
alpha-800x600.png | 800×600,4759 字节 | 透明通道 PNG |
exif-photo-1200x800.jpg | 1200×800,55715 字节 | 含 Exif 元数据的 JPEG |
watermark-300x120.png | 300×120 | 水印图(6.7 节) |
fake.jpg | 内容非图片 | 错误码测试 |
nope.jpg | 不存在 | 触发源 404 |
本文基线配置(各开关的"修改 → 重启 → 实测"闭环已在《imgproxy 4.0 配置参数详解》完成,本文固定在此基线上):
IMGPROXY_KEY=e2f29550ff27f4f6b6475b16b1af3cb648434daa215361c03ca61efab13c04f0
IMGPROXY_SALT=b858fad4bd3ec36662d606c13c0d6fdda3ba980689163ff6128022ded3a662c9
IMGPROXY_LOCAL_FILESYSTEM_ROOT=/images
IMGPROXY_TTL=86400
IMGPROXY_QUALITY=80
IMGPROXY_USE_ETAG=true
IMGPROXY_ENABLE_DEBUG_HEADERS=true
IMGPROXY_WORKERS=1
IMGPROXY_REQUESTS_QUEUE_SIZE=1
IMGPROXY_DOWNLOAD_TIMEOUT=3
WORKERS=1、REQUESTS_QUEUE_SIZE=1、DOWNLOAD_TIMEOUT=3 是刻意收窄的压测配置,专用于第八章 429/504 实测;日常生产请参照配置篇给出的两套模板。
二、前置条件
imgproxy 没有登录态、没有 API Key 头,它的"认证"完全内嵌在 URL 里:请求路径的第一段必须是 HMAC-SHA256 签名,签名错了就 403。本章先确认服务可达、测试素材就位,并准备好贯穿全文的签名脚本。
2.1 服务端点与客户端工具
| 端点 | 方法 | 用途 |
|---|---|---|
GET /health | GET | 健康检查 |
GET /<签名>/<处理选项>/<源URL>.<扩展名> | GET | 图片处理主 API |
GET /<签名>/<处理选项>/plain/<明文URL>@<扩展名> | GET | 同上,明文源 URL 变体 |
GET / | GET | 说明页(实测返回一个 HTML 页面) |
GET /info/<签名>/<info选项>/<源URL> | GET | 图片信息(Pro 功能,3.4 节实测其边界) |
先验证服务存活:
curl -sS -D - -o /dev/null http://127.0.0.1:8140/health
输出节选(实测):
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/plain
Server: imgproxy
Date: Mon, 26 Oct 2026 00:14:04 GMT
Content-Length: 19
响应体:imgproxy is running(19 字节)。注意 /health 的 Cache-Control: no-cache——健康检查明确禁止缓存,与图片响应的 max-age=86400(第七章)形成对照。
2.2 签名 URL 生成脚本
全文实验共用的本地签名脚本 sig.py(实现依据见 4.1 节算法拆解):
#!/usr/bin/env python3
"""打印签名后的完整 URL:python3 sig.py <path>"""
import base64
import hashlib
import hmac
import sys
KEY = "e2f29550ff27f4f6b6475b16b1af3cb648434daa215361c03ca61efab13c04f0"
SALT = "b858fad4bd3ec36662d606c13c0d6fdda3ba980689163ff6128022ded3a662c9"
path = sys.argv[1]
digest = hmac.new(bytes.fromhex(KEY), bytes.fromhex(SALT) + path.encode(), hashlib.sha256).digest()
sig = base64.urlsafe_b64encode(digest).decode().rstrip("=")
print("http://127.0.0.1:8140/" + sig + path)
用法:python3 sig.py '/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg',直接输出可请求的完整签名 URL。
三、API 总览
imgproxy 的 API 面非常窄:一张 HTML 说明页、一个健康检查、一个(Pro 的)信息端点,以及真正的主力——处理 URL。处理 URL 的所有能力都通过"路径段落 + 冒号参数"表达,没有请求体、没有查询参数参与业务逻辑。本章给出总图,并实测 /info 这个容易踩坑的端点。
3.1 端点形态一览
| 端点 | 实测状态 | 说明 |
|---|---|---|
GET /<签名>/<处理选项>/<源URL>.<扩展名> | 200 | 主 API,第六章逐组实测 |
GET /<签名>/<处理选项>/plain/<明文URL>@<扩展名> | 200 | 明文源 URL,5.2 节 |
GET /health | 200 | 健康检查,2.1 节 |
GET / | 200 | 说明页 |
GET /info/... | 403 | Pro 功能,3.4 节 |
直接访问根路径:
curl -sS -D - -o /dev/null http://127.0.0.1:8140/
输出节选(实测):
HTTP/1.1 200 OK
Cache-Control: max-age=86400, public
Content-Type: text/html
Server: imgproxy
响应体是一个 5340 字节的 HTML 页面,标题行照录:<head><title>Hey, I'm imgproxy!</title></head>。浏览器直接打开 imgproxy 地址看到的"欢迎页"就是它。
3.2 认证方式:HMAC-SHA256 签名
每个处理 URL 的第一段是签名,算法为:以十六进制解码的 KEY 作为 HMAC 密钥,对「十六进制解码的 SALT + 请求路径(含前导 /,含扩展名)」计算 HMAC-SHA256,再做 URL-safe Base64 编码并去掉填充 =。完整拆解与多语言代码见第四章。
💡 技术拓展:HMAC 的哈希基为什么是 SHA-256? HMAC 是通用构造,理论上可搭配 SHA-1/SHA-256/SHA-512 等任意哈希基:SHA-1 输出 160 位但已被证实存在碰撞攻击,新系统不应选用;SHA-512 在 64 位机上更快但输出与编码长度都更长,对 URL 场景不友好;SHA-256 输出 256 位、抗碰撞强度足够,是当前行业默认(JWT HS256、AWS Signature V4、GitHub Webhook 签名均用它)。imgproxy 官方固定 SHA-256,不支持切换哈希基,这反而省掉了协商成本。
3.3 限流与超时约定
imgproxy 没有按 Key 限流的概念,流量控制由两组配置决定:IMGPROXY_WORKERS(并发处理数)+ IMGPROXY_REQUESTS_QUEUE_SIZE(等待队列长度),超出的请求立即返回 429;IMGPROXY_DOWNLOAD_TIMEOUT 限制从图源拉图的最长时间,超时返回 504。本文基线把这三项压到 1/1/3 秒,第八章有完整的触发实测。
3.4 /info 端点:Pro 功能边界实测
官方文档 4.0.x 版《Getting an image info》页面标题带 [Pro] 徽章——即 /info 是商业版功能。开源版实测:无论签名怎么算,/info/... 一律 403。按官方口径(签名覆盖 /<info选项>/<源URL>,不带 /info 前缀)构造四种签名形态逐一请求:
PHOTO="bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw"
# 形态A:签名覆盖 /<info选项>/<源URL>(官方口径)
curl -sS -o /dev/null -w 'A %{http_code}\n' "http://127.0.0.1:8140/info/$(python3 sig.py "/d:1/f:1/$PHOTO" | sed 's|http://127.0.0.1:8140||')"
# 形态B:签名覆盖含 /info 前缀的完整路径
curl -sS -o /dev/null -w 'B %{http_code}\n' "http://127.0.0.1:8140/info/$(python3 sig.py "/info/d:1/f:1/$PHOTO" | sed 's|http://127.0.0.1:8140||')"
# 形态C:用处理选项充当 info 选项
curl -sS -o /dev/null -w 'C %{http_code}\n' "http://127.0.0.1:8140/info/$(python3 sig.py "/rs:fit:300:300:1/$PHOTO" | sed 's|http://127.0.0.1:8140||')"
# 形态D:仅签名 + 源 URL
curl -sS -o /dev/null -w 'D %{http_code}\n' "http://127.0.0.1:8140/info/$(python3 sig.py "/$PHOTO" | sed 's|http://127.0.0.1:8140||')"
实测输出:四种形态全部 403。随后在服务端补加 IMGPROXY_ENABLE_INFO_API=true 并重启复测,四种形态仍全部 403。服务日志逐行照录(形态 A):
time="2026-10-26 02:32:16" level="WARNING" msg="Completed in 90.515µs /info/TqAIW4s9j8mwM156SdlA81lqOscOBRsPMYe2pOiw0yY/d:1/f:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw" request_id="ksdouxAaw1OObi_xACiw6" method="GET" status=403 client_ip="172.17.0.1" error="Invalid signature" error_docs_url="https://docs.imgproxy.net/usage/signing_url"
访问 /info 根路径则返回 404 Not Found,响应体 Invalid URL(11 字节)。结论:开源版 v4.0.19 不要把 /info 纳入集成方案;需要图片元信息时,可用 IMGPROXY_ENABLE_DEBUG_HEADERS=true 的调试响应头(7.2 节)或自行读源文件代替。
四、HMAC 签名机制详解
签名是 imgproxy API 的第一道门:URL 一旦可伪造,攻击者就能枚举任意尺寸/格式组合发起放大攻击。本章把签名算法拆到字段级,用四种语言实现并交叉验证,再实测错误签名的边界行为。
4.1 签名算法逐字段拆解
官方文档《Signing a URL》的原文定义:"A signature is a URL-safe Base64-encoded HMAC digest of the rest of the path, including the leading /." 落到操作上是四步:
- 取"签名之后的路径":处理 URL 为
/<处理选项>/<源URL>.<扩展名>(或/<处理选项>/plain/<明文URL>@<扩展名>),注意包含前导斜杠、包含扩展名; - 拼接盐:把十六进制解码后的 SALT 字节串放在路径字节前面;
- 计算 HMAC-SHA256:密钥为十六进制解码后的 KEY;
- 编码:URL-safe Base64(
+→-、/→_),去掉末尾=填充。
用 sig.py 对主测试路径生成签名 URL 并请求:
python3 sig.py '/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg'
# 输出:http://127.0.0.1:8140/TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg
curl -sS -D - -o out.jpg "http://127.0.0.1:8140/TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
完整回显(实测,19 行全录):
HTTP/1.1 200 OK
Cache-Control: max-age=86400, public
Content-Disposition: inline; filename="photo-1920x1080.jpg"
Content-Length: 13768
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
Content-Type: image/jpeg
Etag: "FS_7xMa7AW6Xs0WLY5Edu2aMdhH2h8UNwFcMFQ3vwq8"
Last-Modified: Sun, 25 Oct 2026 14:02:44 GMT
Server: imgproxy
X-Origin-Content-Length: 104248
X-Origin-Height: 1080
X-Origin-Width: 1920
X-Request-Id: c7ymJnPe58ZzVyRb8sr_C
X-Result-Height: 360
X-Result-Width: 640
Date: Mon, 26 Oct 2026 00:14:04 GMT
200 + 640×360 结果图,签名通过。两个容易踩坑的细节被这组实验反复验证:
- 签名覆盖的是"整段请求路径",即
/rs:fit:640:360:1/<base64>.jpg——包含处理选项、源 URL 和输出扩展名。改任何一段(哪怕只改扩展名)都必须重新签名; - 源 URL 本体是 URL-safe Base64、去填充的编码(
bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw解码即local:///photo-1920x1080.jpg,见 5.1 节)。
4.2 多语言签名实现与一致性交叉验证
生产环境里签名由业务后端生成。用同一 KEY/SALT、同一路径 /rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg,分别以 Python、Node.js、Go、openssl 四种实现计算,结果必须一致。
Python(与 sig.py 同源):
import base64, hashlib, hmac, sys
key, salt, path = sys.argv[1], sys.argv[2], sys.argv[3]
digest = hmac.new(bytes.fromhex(key), bytes.fromhex(salt) + path.encode(), hashlib.sha256).digest()
print(base64.urlsafe_b64encode(digest).decode().rstrip("="))
Node.js:
const crypto = require("crypto");
const [key, salt, path] = process.argv.slice(1);
const mac = crypto.createHmac("sha256", Buffer.from(key, "hex"));
mac.update(Buffer.from(salt, "hex"));
mac.update(path);
console.log(mac.digest("base64url"));
Go:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
)
func main() {
key, _ := hex.DecodeString(os.Args[1])
salt, _ := hex.DecodeString(os.Args[2])
path := os.Args[3]
mac := hmac.New(sha256.New, key)
mac.Write(salt)
mac.Write([]byte(path))
fmt.Println(base64.RawURLEncoding.EncodeToString(mac.Sum(nil)))
}
openssl 命令行(适合运维快速核对):
printf '%s' "$SALT" | xxd -r -p; printf '%s' "$PATH_STR" | openssl dgst -sha256 -mac hmac -macopt hexkey:"$KEY" -binary | basenc --base64url | tr -d '='
# 注意 salt 与 path 需作为连续字节流输入,上面为示意拆分,实测时二者一次拼接进管道
四语言交叉验证结果(实测照录):
[python3]
TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg
[node]
TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg
[go]
TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg
[openssl]
TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg
四实现输出完全一致,与 4.1 节实测通过服务端校验的签名相同。其中 Go 用 base64.RawURLEncoding(Raw 即去填充)、Node 用 base64url、Python 手动 rstrip("=")——三种"去填充"写法等价,务必别漏。
4.3 官方示例交叉验证
再用官方文档自带示例做一次端到端核对:key = secret(十六进制 736563726574)、salt = hello(十六进制 68656C6C6F),路径取官方例中的 /rs:fill:300:400:0/g:sm/aHR0cDovL2V4YW1w/bGUuY29tL2ltYWdl/cy9jdXJpb3NpdHku/anBn.png:
import base64, hashlib, hmac
path = "/rs:fill:300:400:0/g:sm/aHR0cDovL2V4YW1w/bGUuY29tL2ltYWdl/cy9jdXJpb3NpdHku/anBn.png"
key, salt = "736563726574", "68656C6C6F"
sig = base64.urlsafe_b64encode(hmac.new(bytes.fromhex(key), bytes.fromhex(salt)+path.encode(), hashlib.sha256).digest()).decode().rstrip("=")
print(sig)
实测输出 oKfUtW34Dvo2BGQehJFR4Nr0_rIjOtdtzJ3QFsUcXH8,与官方文档示例给出的签名逐字符一致——本文 4.1 的算法理解无偏差。
4.4 签名校验实测:无签名、坏签名、unsafe 一律 403
配置了 IMGPROXY_KEY/IMGPROXY_SALT 之后签名就是强制的。三种典型错误全部实测:
# ① 官方保留字 unsafe(无签名模式专用字)冒充签名
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/unsafe/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
# ② 签名段被改坏(换一个同长度假签名)
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/AAAAbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
# ③ 整个签名段缺失
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
三种情况回显一致(实测):
HTTP/1.1 403 Forbidden
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
Content-Type: text/plain
Server: imgproxy
X-Request-Id: oeMVgjlfpFWfZiLGeKflg
Date: Mon, 26 Oct 2026 00:14:05 GMT
Content-Length: 9
响应体均为 Forbidden(9 字节)。imgproxy 故意不区分"签名缺失"与"签名错误",不给攻击者试错信息。另两个相关结论:
unsafe不是万能钥匙:它只在服务端完全没有配置 KEY/SALT(即官方所谓关闭签名校验)时作为占位签名段使用;一旦配置了密钥对,带unsafe一律 403,实测即上例①;- 截短签名:
IMGPROXY_SIGNATURE_SIZE可把签名截短(如 16 字节)以缩短 URL,服务端按前缀比对;切换方法与实测见《imgproxy 4.0 配置参数详解》。
五、签名 URL 与源 URL 模式
签名解决"谁能发请求",源 URL 部分解决"处理哪张图"。imgproxy 支持三种源 URL 拼写,输出格式由扩展名(或 f 选项)决定。本章把三种拼写和格式控制逐一实测。
5.1 URL 结构与三种源 URL 形态
处理 URL 的完整骨架:
http://<host>/<签名>/<处理选项1>/<处理选项2>/.../<源URL>[.<扩展名>]
💡 技术拓展:源 URL 的三种形态对比
形态 拼写 适用场景 备注 Base64 aHR0cDovL...(URL-safe Base64、去填充)URL 含特殊字符、希望整体路径干净 最通用; local:///path也走此形态plain plain/http://origin/a.jpg@jpg调试直观、URL 规范可预测 @后为输出扩展名;URL 必须转义特殊字符enc enc/<加密串>不想暴露源地址 需服务端配 IMGPROXY_SOURCE_URL_ENCRYPTION_KEY(官方口径),本文环境未启用、未实测
本文主测试源 local:///photo-1920x1080.jpg 的 Base64 就是 bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw。
5.2 plain 明文源 URL 实测
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/0h2tz61ppu6hgg1PPUqQvoR50hagLleDAvM--gs5Bvs/rs:fit:300:300:1/plain/http://origin/photo-1920x1080.jpg@jpg"
输出节选(实测):
HTTP/1.1 200 OK
Cache-Control: max-age=86400, public
Content-Disposition: inline; filename="photo-1920x1080.jpg"
Content-Length: 5835
Content-Type: image/jpeg
Etag: "6ade0950-19738"
Server: imgproxy
X-Origin-Content-Length: 104248
X-Origin-Height: 1080
X-Origin-Width: 1920
X-Result-Height: 169
X-Result-Width: 300
plain 形态工作正常:@jpg 指定输出为 JPEG,300×169。两个细节:
@后的扩展名就是输出格式声明,等价于 base64 形态路径尾部的.jpg;- 签名段覆盖的是
/rs:fit:300:300:1/plain/http://origin/photo-1920x1080.jpg@jpg整段——plain URL 一样要签名,且改图名必须重签。
再验证"源拼写不影响输出":同一张图走 http://origin 的 base64 源(aHR0cDovL29yaWdpbi9waG90by0xOTIweDEwODAuanBn),参数同为 rs:fit:600:600:1:
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/KwHxEZHy5-H3txV2GEt0r2Eraun57KPUlqQtfPhho0Q/rs:fit:600:600:1/aHR0cDovL29yaWdpbi9waG90by0xOTIweDEwODAuanBn.jpg"
实测 Content-Length: 12875、X-Result-Width: 600、X-Result-Height: 338,与本地源同参数的结果(6.1 节 b_fit 组,12875 字节)逐字节同量级且尺寸一致——输出只取决于源图内容与处理选项,与源 URL 拼写无关;但 Etag 不同("6ade0950-19738" vs 本地源的 "FS_7xMa..."),原因见 7.3 节。
5.3 输出格式扩展名与 f 选项的优先级
同一处理选项 rs:fill:600:600:1,仅改扩展名(或加 f: 选项),六组实测汇总:
| 请求路径(签名段省略) | Content-Type | Content-Length |
|---|---|---|
/rs:fill:600:600:1/<b64>.webp | image/webp | 7918 |
/rs:fill:600:600:1/<b64>.avif | image/avif | 7357 |
/rs:fill:600:600:1/<b64>.jpg | image/jpeg | 17860 |
/rs:fill:600:600:1/<b64>.png | image/png | 49675 |
/rs:fill:600:600:1/f:webp/<b64>.jpg | image/jpeg | 17860 |
/rs:fill:600:600:1/f:webp/<b64>(无扩展名) | image/webp | 7918 |
代表性命令与回显(.webp 组):
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/QGjIxJNuh6cFAZcyqjGPQEcQnVlTLZsYbrhPxKYZkjg/rs:fill:600:600:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.webp"
HTTP/1.1 200 OK
Content-Type: image/webp
Content-Length: 7918
Content-Disposition: inline; filename="photo-1920x1080.webp"
X-Result-Width: 600
X-Result-Height: 600
(节选实测行;Date/Server 等通用头见第七章。)关键结论有两条:
- 扩展名与
f:选项同时存在时,扩展名赢:f:webp+.jpg实测输出image/jpeg,f:被忽略——f只在 URL 不带扩展名时生效; - AVIF 压缩率最高(7357 字节),WebP 次之(7918),PNG 无损最大(49675)。生产建议按"AVIF → WebP → JPEG"逐级回退(服务端自动协商见配置篇
IMGPROXY_AUTOQUALITY/IMGPROXY_ENABLE_WEBP_DETECTION相关小节)。
另外实测选项顺序无关:把 f:png 放在 rs: 前面(/f:png/rs:fill:300:300:1/<b64>,无扩展名)同样输出 image/png(18437 字节,300×300)——处理选项的书写顺序不影响结果。
5.4 无签名 unsafe 模式与源 URL 白名单
4.4 节已实测:配置密钥后 unsafe 一律 403。若内网环境确实想免签名,做法是不配置 IMGPROXY_KEY/IMGPROXY_SALT(官方口径:签名段可填任意字符串如 unsafe 或 _),此时任何人都能构造任意处理 URL,务必只在内网隔离环境使用。
源 URL 白名单是比签名更细的一层控制:IMGPROXY_ALLOWED_SOURCES 配置正则列表,不匹配的源直接拒绝。把配置改为 IMGPROXY_ALLOWED_SOURCES=http://origin 并重启后实测三连:
# ① 白名单内的源 → 放行
curl -sS -o /dev/null -w '%{http_code}\n' "<签名>/rs:fill:600:600:1/aHR0cDovL29yaWdpbi9waG90by0xOTIweDEwODAuanBn.jpg"
# ② 同机不同写法(127.0.0.1:8141 与 origin 指向同一图源站)→ 拒绝
curl -sS -o /dev/null -w '%{http_code}\n' "<签名>/rs:fill:600:600:1/aHR0cDovLzEyNy4wLjAuMTo4MTQxL3Bob3RvLTE5MjB4MTA4MC5qcGc.jpg"
# ③ 本地源 local:/// → 拒绝
curl -sS -o /dev/null -w '%{http_code}\n' "<签名>/rs:fill:600:600:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
实测:① 200;② ③ 均 404 Not Found,响应体 Invalid source URL(18 字节)。把白名单改为 http://origin,local:// 重启后,③ 变为 200(7871 字节,400×225)。结论:ALLOWED_SOURCES 按"源 URL 字符串"匹配而非按解析后的主机——127.0.0.1:8141 与 origin 指向同一服务但拼写不同就会被拒;上白名单时必须把生产会用到的全部拼写枚举进去。
六、URL 处理选项逐组实测
处理选项是 imgproxy API 的主体,格式统一为 名字:参数1:参数2:...,用 / 分隔多个选项。本章按"尺寸 → 裁剪 → 旋转 → 质量 → 背景 → 模糊锐化 → 水印 → 元数据体积 → 预设 → URL 级控制"顺序逐组实测,每组给出命令、实测回显关键行与结论。
6.1 尺寸与适配模式:rs / s / rt
rs:<模式>:<宽>:<高>:<是否放大>(enlarge 1 允许放大、0 不允许),另有别名 s(等价 rs)与 rt:<模式>(只改模式、尺寸用 w/h 选项)。四种适配模式实测汇总(源图 1920×1080):
| 请求路径(签名段省略) | 实测结果尺寸 | Content-Length |
|---|---|---|
rs:fit:600:600:1 | 600×338 | 12875 |
rs:fill:600:600:1 | 600×600 | 17860 |
rs:auto:600:600:1 | 600×600 | 17860 |
rs:force:600:400:1 | 600×400(拉伸变形) | 14077 |
rs:fit:300:600:1 | 300×169 | 5835 |
rs:auto:300:600:1 | 300×169 | 5835 |
s:400:300:1 | 400×225 | 7871 |
rt:fill + s:600:600:1 | 600×600 | 17860 |
代表性命令与回显(fill 组):
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/CY35iVM2_qxfkUwY6erUYB2wcDNvqp7Db4w86LYQ6HQ/rs:fill:600:600:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 17860
X-Result-Width: 600
X-Result-Height: 600
(节选实测行。)结论:
fit等比缩放装进目标框(600×600 框 → 600×338);fill等比缩放后居中裁剪填满框(600×600);force无视长宽比强行拉伸;auto由服务端自动在 fit/fill 间抉择,本组两例分别表现为等效 fill(600×600)与等效 fit(300×600 → 300×169);s与rs、rt与rs的模式段可以互换组合(上表第 7、8 行),迁移旧 URL 时不用改签名逻辑以外的部分。
6.2 裁剪与重力:c / g
c:<宽>:<高>:<重力> 先裁剪,g:<重力> 控制 fill/裁剪的对齐点。实测两组:
# 右上角裁剪 600×600,再 fit 进 600×600
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/qZ14nqsLrmGoNJ7xoP3nwdrvzdQwmzUXOcIn9i4Y8us/c:600:600:noea/rs:fit:600:600:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
# fill 400×400,重心对齐底部
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/MZL0I6cECNGXhVJbaX8kGaxrJPmRyOiLp80uUsry4WY/rs:fill:400:400:1/g:sm/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
实测:前者 Content-Length: 8907、600×600;后者 Content-Length: 8795、400×400。noea(不放大)在源图裁剪区不足 600×600 时按原尺寸保留。与 6.1 的 rs:fill:600:600(17860 字节)对比:裁剪掉的部分不参与编码,体积明显更小。非法重力值(如 g:zz)返回 404 Invalid URL(8.1 节)。
6.3 旋转 rot 与分辨率倍率 dpr
# 先旋转 90°,再 fit 进 600×400 → 输出 225×400
curl -sS -D - -o /dev/null "<签名>/rot:90/rs:fit:600:400:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
# dpr:2 把输出物理像素翻倍
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/-PCZBitWeCYOWNVQx7LuQRC_5XRkVqyS47rySgytMfk/dpr:2/rs:fill:300:300:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
实测:旋转组 X-Result-Width: 225、X-Result-Height: 400、Content-Length: 8107——1080×1920 的竖版图 fit 进 600×400 框得 225×400,符合预期;dpr 组 X-Result-Width: 600、X-Result-Height: 600(17860 字节)——CSS 300×300 的格子在高分屏上拿到 600×600 物理像素,这正是 dpr 的设计用途(配合前端 srcset 使用)。
6.4 质量参数 q
基线 IMGPROXY_QUALITY=80。同参数只改 q:
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/oGr6ztUmMOtQ0ruFD8K4T9XoCb_DaL6LKqsihJhZiUY/rs:fill:600:600:1/q:30/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/ZfJCk4n_qAVeX6GBwYORQ2hEOs90HzHycDTOj8dt6MU/rs:fill:600:600:1/q:80/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
实测:q:30 → Content-Length: 9280;q:80 → Content-Length: 17860(与 6.1 fill 组不带 q 的结果完全一致,验证 80 就是基线默认)。URL 级 q 覆盖服务端默认,缩略图场景可单独压低质量。
6.5 背景色 bg 与透明压平
透明 PNG 转 JPEG 必须压平到不透明背景。用 alpha-800x600.png 实测三连(rs:fill:600:600:1):
| 请求路径(签名段省略) | Content-Length |
|---|---|
/rs:fill:600:600:1/<alpha_b64>.jpg(不带 bg) | 18614 |
/rs:fill:600:600:1/bg:ffffff/<alpha_b64>.jpg | 18614 |
/rs:fill:600:600:1/bg:255:0:0/<alpha_b64>.jpg | 17251 |
另一组 rs:fit:600:450:1 下:不带 bg 与 bg:ffffff 均为 23748 字节,bg:ff0000 为 22033 字节。两组一致表明:imgproxy 转 JPEG 时的默认压平背景就是白色,bg 的作用是改写成其他颜色(hex 三元组 bg:ff0000 与十进制 bg:255:0:0 两种拼写均可)。若要保留透明,输出改用 .png/.webp 即可(5.3 节)。
6.6 模糊 bl 与锐化 sh
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/IubG14ni-2-q3NriTbm-v0XW9e8Ljvb95LAJtgXg65I/rs:fit:600:338:1/bl:5/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/Vm7VudWoHhJSeI0QPpcPU8X4abmqtVReQkP5cMcHKt8/rs:fit:600:338:1/sh:2/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
实测:bl:5 → 9059 字节;sh:2 → 15666 字节;同输出尺寸(600×338)的 fit 无特效基线为 12875 字节(6.1 表 b_fit 组)。模糊显著减小体积、锐化显著增大体积——大图列表页用轻模糊省流量、出血图用轻锐化补锐度,都是常见组合。
6.7 水印 wm:URL 参数与服务端水印源
wm:<不透明度>:<重力>:<X 偏移>:<Y 偏移>:<相对尺寸>。实测同一签名 URL(/rs:fit:800:450:1/wm:0.5:soea:10:10:0.2/<b64>.jpg)打在两台不同配置的服务上:
| 服务端配置 | Content-Length | 结论 |
|---|---|---|
配置 IMGPROXY_WATERMARK_PATH=/images/watermark-300x120.png | 19960 | 水印生效 |
| 未配置任何水印源 | 18486 | 与无水印基准逐字节一致 |
(无水印基准 URL /rs:fit:800:450:1/<b64>.jpg 在无水印配置服务上实测同为 18486。)结论:wm: 只是"怎么叠",水印素材必须由服务端配置提供(IMGPROXY_WATERMARK_PATH/IMGPROXY_WATERMARK_DATA/IMGPROXY_WATERMARK_URL,见配置篇);URL 里只有 wm 选项而没有服务端水印源时,输出不变。另注意两台服务若都配了水印,IMGPROXY_WATERMARK_OPACITY 会与 URL 里的不透明度相乘(配置篇 0.8 组实测)。
6.8 元数据剥离 sm 与体积上限 mb
# 保留元数据
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/ReM3VVvsSYqhpkR7wH88tWJ_SvlYGeX_DU66lokSScQ/rs:fit:600:400:1/sm:0/bG9jYWw6Ly8vZXhpZi1waG90by0xMjAweDgwMC5qcGc.jpg"
# 剥离元数据
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/XKfFh9iW8oZNWTLqM7taDkX8335GdMmGaiapeiHfeu4/rs:fit:600:400:1/sm:1/bG9jYWw6Ly8vZXhpZi1waG90by0xMjAweDgwMC5qcGc.jpg"
# 体积上限 20000 字节
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/ehjTa9l3LlOEFsIa18wwnbSt-fAXlm9Gb0EsNMOksrs/rs:fill:1200:800:1/mb:20000/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
# 无上限对照
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/mKJLxFiqZ1uvPLj4cFkbJCcqcucVNPFeBnx8V4F615g/rs:fill:1200:800:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
实测:sm:0 → 14157 字节(Exif 保留);sm:1 → 14053 字节(Exif 剥离,输出仍含 JPEG 结构头,注意从二进制头看差异而非只看体积);mb:20000 → 18607 字节(≤20000 达标);无 mb 对照 → 34576 字节。mb 通过自动降质量/降尺寸把结果压进限额,适合"封面图必须小于 N KB"这类硬约束;sm:1 顺带消灭了照片 GPS 等隐私信息,涉及用户上传图时建议默认开启。
6.9 预设 pr:把常用组合命名复用
服务端配置 IMGPROXY_PRESETS=thumb=resizing_type:fill/width:200/height:200,sharp=sharpen:0.7 后,URL 里用 pr:<名字> 引用:
curl -sS -D - -o /dev/null "<签名>/pr:thumb/<b64>.jpg"
curl -sS -D - -o /dev/null "<签名>/pr:sharp/<b64>.jpg"
curl -sS -D - -o /dev/null "<签名>/pr:nosuch/<b64>.jpg"
实测:pr:thumb → 200×200、4879 字节(presets 里 width/height 选项与 rs:fill 组合生效);pr:sharp → 1920×1080 原尺寸输出、60493 字节(无缩放仅锐化,重编码后的体积由质量与锐化共同决定);pr:nosuch → 404 Not Found,响应体 Invalid URL。预设让"缩略图规格"收敛在配置里,改规格不用改业务代码,URL 也不用重签名之外的部分——注意预设定义变了,旧 URL 的输出会跟着变。
6.10 URL 级过期 exp、缓存穿透 cb 与输出文件名 fn
# exp:URL 自带过期时间(Unix 秒),示例取"生成时 +1 小时"
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/tKJ2T8R4DJ9mSvKSExMmJq0thePzC9GjSSvdkI7SzLQ/exp:1792977379/rs:fit:300:300:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
# cb:缓存穿透/区分
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/<签名>/rs:fit:640:360:1/cb:probe123/<b64>.jpg"
# fn:自定义下载文件名
curl -sS -D - -o /dev/null "<签名>/fn:probe-img/rs:fit:300:300:1/<b64>.jpg"
实测:
exp未过期 →200,且Cache-Control: max-age=3599, public——缓存有效期被收敛为 URL 剩余寿命,过期后该 URL 返回404 Not Found,响应体Invalid URL(实测exp:1792970179,生成时已过期 2 小时);cb:probe123与不带 cb 的同参数 URL 输出完全一致(均 13768 字节,640×360)——cb 只改 URL 指纹/缓存键,不影响像素;换个 cb 值可让 CDN 绕过旧缓存重新回源;fn:probe-img→ 响应头Content-Disposition: inline; filename="probe-img.jpg"(实测照录),浏览器"另存为"时用它。
6.11 选项书写顺序与组合
把前面各组串起来可以得到多选项 URL,实测两组组合(顺序不同):
# f:png 写在最前(选项顺序无关性验证,5.3 节已给出结果 18437 字节 image/png)
/<签名>/f:png/rs:fill:300:300:1/<b64>
# rot + rs + 扩展名组合(6.3 节 8107 字节 225×400)
/<签名>/rot:90/rs:fit:600:400:1/<b64>.jpg
结论:处理选项之间顺序无关,签名覆盖整段路径所以顺序变了要重签;建议团队统一"尺寸 → 重力 → 格式 → 特效"的可读顺序。
七、响应头、缓存与条件请求
imgproxy 的响应头承载了三件事:安全基线(CSP)、缓存协商(Cache-Control/ETag)、可观测性(X-Request-Id 与调试头)。逐个拆解。
7.1 通用响应头逐项解读
以 4.1 节完整回显的 19 行为准,逐项说明:
| 响应头 | 实测值(示例) | 说明 |
|---|---|---|
Server | imgproxy | 固定标识,便于探活识别 |
Content-Security-Policy | default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox | 说明页防 XSS 的 CSP,图片响应同样携带 |
X-Request-Id | c7ymJnPe58ZzVyRb8sr_C | 每请求唯一 ID,与访问日志 request_id 对应,排障时拿它对账 |
Cache-Control | max-age=86400, public | 由 IMGPROXY_TTL=86400 决定,允许 CDN 缓存 |
Etag | "FS_7xMa7AW6Xs0WLY5Edu2aMdhH2h8UNwFcMFQ3vwq8" | 条件请求用,见 7.3 |
Last-Modified | Sun, 25 Oct 2026 14:02:44 GMT | 源文件修改时间,与 ETag 双轨协商 |
Content-Disposition | inline; filename="photo-1920x1080.jpg" | 默认继承源文件名,fn: 可覆盖(6.10) |
Content-Type / Content-Length | image/jpeg / 13768 | 输出格式与体积 |
7.2 调试响应头
IMGPROXY_ENABLE_DEBUG_HEADERS=true 时,imgproxy 追加五个调试头(本文全部 200 实测均携带):
| 响应头 | 实测示例 | 含义 |
|---|---|---|
X-Origin-Width / X-Origin-Height | 1920 / 1080 | 源图尺寸 |
X-Origin-Content-Length | 104248 | 源图字节数 |
X-Result-Width / X-Result-Height | 640 / 360 | 输出尺寸 |
注意实测头名是 X-Origin-*/X-Result-*,没有 X-Imgproxy- 前缀;关闭该配置后这组头整体消失、其余头收敛(《imgproxy 4.0 配置参数详解》有开关前后对照实测)。前端联调尺寸时直接看 X-Result-* 比开图快得多。
7.3 ETag 与 304 条件请求
IMGPROXY_USE_ETAG=true 时 imgproxy 为结果计算 ETag。对 4.1 节同一 URL 携带 If-None-Match 重放:
curl -sS -D - -o /dev/null -H 'If-None-Match: "FS_7xMa7AW6Xs0WLY5Edu2aMdhH2h8UNwFcMFQ3vwq8"' \
"http://127.0.0.1:8140/TqCRbmKEAUB6sIAbBs1a3Hiorz6A2zmozulZhSBiFhg/rs:fit:640:360:1/bG9jYWw6Ly8vcGhvdG8tMTkyMHgxMDgwLmpwZw.jpg"
完整回显(实测,8 行全录):
HTTP/1.1 304 Not Modified
Cache-Control: max-age=86400, public
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
Etag: "FS_7xMa7AW6Xs0WLY5Edu2aMdhH2h8UNwFcMFQ3vwq8"
Last-Modified: Sun, 25 Oct 2026 14:02:44 GMT
Server: imgproxy
X-Request-Id: fAfm6tpziSyoIFzNJsfnm
Date: Mon, 26 Oct 2026 01:42:46 GMT
304 响应无 body、无 Content-Length,源站零编码开销。两条实测观察值得记住:
- ETag 粒度是"源图"而非"处理参数":同一张
photo-1920x1080.jpg在本文所有处理 URL 下 ETag 全部为"FS_7xMa..."(alpha、exif 图各有自己的值)——不同处理 URL 共享同一 ETag 值,按 URL 做 CDN 缓存键不受影响,但别用 ETag 反推输出规格; - ETag 取决于源的来源与内容:本地源是 imgproxy 计算值(
FS_7xMa...),HTTP 源直接继承图源的 ETag("6ade0950-19738"正是 Nginx 文件 ETag 的<mtime>-<size>格式,plain 与 base64 拼写结果一致)。
💡 技术拓展:强 ETag 与弱 ETag HTTP 协议允许
"(强校验器,逐字节相同才算命中)与W/"..."(弱校验器,语义等价即可命中)两种形态。imgproxy 输出的是强 ETag;6ade0950-19738这类继承自源站的值同样按强校验器比对。对图片这类字节级敏感资源用强 ETag 是正确选择。
7.4 Cache-Control 与 TTL/exp 联动
- 默认:
Cache-Control: max-age=<IMGPROXY_TTL>, public,基线 86400 秒; - URL 带
exp时:max-age收敛为 URL 剩余寿命(6.10 实测 max-age=3599); /health:no-cache(2.1 实测)。
CDN 侧按 Cache-Control 缓存即可,exp 过期的 URL 即使 CDN 还有副本,回源时 imgproxy 也会 404,语义自洽。
八、错误码与排障
错误处理是 API 集成最容易欠考虑的部分。imgproxy 的错误响应都是 text/plain 短句,配合状态码与日志 error= 字段定位。
8.1 错误响应总表
以下全部实测照录(状态码 / Content-Length / 响应体):
| 触发场景 | 状态码 | 响应体 |
|---|---|---|
未知处理选项 x:1 | 404 | Invalid URL(11 字节) |
非法尺寸数值 rs:fit:abc:abc:1 | 404 | Invalid URL |
非法重力 g:zz | 404 | Invalid URL |
未知预设 pr:nosuch | 404 | Invalid URL |
exp 已过期 | 404 | Invalid URL |
| 白名单拒绝的源 URL | 404 | Invalid source URL(18 字节) |
源文件不存在(nope.jpg) | 404 | Source is unreachable(21 字节) |
源不是图片(fake.jpg) | 422 | Invalid source image(20 字节) |
缺签名 / 坏签名 / unsafe | 403 | Forbidden(9 字节) |
IMGPROXY_MAX_SRC_RESOLUTION 超限的大图 | 404 | Invalid URL(配置篇实测) |
| 处理队列打满 | 429 | Too Many Requests(17 字节) |
| 源站超时 | 504 | Source is unreachable |
路径含畸形百分号编码(%%zz) | 400 | 400 Bad Request(15 字节,见 8.3) |
两个反直觉点:处理选项错误是 404 而不是 400(imgproxy 把"解析不出合法路由/选项"统一按 Invalid URL 处理);源 404 会被透传为 404,而源超时是 504——两者响应体相同,必须看状态码区分。
8.2 429 限流实测
基线 WORKERS=1、REQUESTS_QUEUE_SIZE=1,对同一 URL 发起 40 路并发:
for i in $(seq 1 40); do
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 10 "http://127.0.0.1:8140/<签名>/rs:fit:800:450:1/<b64>.jpg" &
done | sort | uniq -c
实测统计:14 200 / 26 429——1 个 worker 加 1 个队列位远小于瞬时需求,溢出请求立即被 429 拒绝,不排队不降级。把并发加到 240 后的两个档位表现(实测记录):仍挤进队列的请求因源站响应慢最终 504(单次实测耗时 8.913300s,即 3 秒超时 × 排队轮数),而彻底溢出的请求 429 在 0.000657s 内立即返回;一轮 240 并发汇总为 238 × 429 + 2 × 504。429 响应头实测:
HTTP/1.1 429 Too Many Requests
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
Content-Type: text/plain
Server: imgproxy
X-Request-Id: NB-owFxDZCl66WZlcyqR-
Date: Mon, 26 Oct 2026 00:17:10 GMT
Content-Length: 17
集成侧必须把 429 当作常规分支处理:指数退避重试 + 客户端并发上限,别把 429 当异常抛栈。
8.3 504 源站超时实测
DOWNLOAD_TIMEOUT=3 下,用一个每请求先睡 10 秒再响应的假图源(本地 8161 端口起一个慢速 HTTP 服务模拟)作为源:
SRC=$(python3 -c "import base64;print(base64.urlsafe_b64encode(b'http://172.17.0.1:8161/slow.jpg').decode().rstrip('='))")
curl -sS -D - -o /dev/null --max-time 30 "http://127.0.0.1:8140$(python3 sig.py "/rs:fit:300:300:1/${SRC}.jpg" | sed 's|http://127.0.0.1:8140||')"
实测耗时 3.012s 后返回(回显全录):
HTTP/1.1 504 Gateway Timeout
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
Content-Type: text/plain
Server: imgproxy
X-Request-Id: t1SWu2iBV-Uo8LfYI1-7B
Date: Mon, 26 Oct 2026 02:41:36 GMT
Content-Length: 21
响应体 Source is unreachable。超时受控于 imgproxy 自身配置,不受客户端 --max-time 影响;生产上 504 高发时应先看图源健康度与网络,再考虑调大 DOWNLOAD_TIMEOUT。
8.4 畸形请求的 400:没有 Server 头
路径含非法百分号转义时,请求在服务框架层就被拒绝,根本没进 imgproxy 业务逻辑:
curl -sS -D - -o /dev/null "http://127.0.0.1:8140/0L9eBy5PjWwA3-X5NLDIwGZJQaIKLQ9AUkkNfCQvh24/rs:fit:600:600:1/%%zz.jpg"
完整回显(实测,4 行全录):
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Connection: close
没有 Server: imgproxy、没有 X-Request-Id——这是识别"框架层拒绝"与"业务层错误"的快速特征。前端/网关侧对 URL 做encodeURIComponent 规范化可避免此类请求。
九、常见问题
Q1:签名算出来服务端总说 Forbidden?
按 4.1 节四步核对:KEY/SALT 是否 hex 解码、SALT 是否拼在路径前面、路径是否含前导 / 与扩展名、Base64 是否 URL-safe 且去 =。四语言实现(4.2 节)逐一对拍最快。
Q2:URL 里写了 f:webp 为什么还是 JPEG?
扩展名优先于 f: 选项(5.3 实测)。要去掉扩展名或把扩展名改成目标格式。
Q3:加了 wm: 水印却没出现?
wm 只描述叠加方式,水印素材必须在服务端配置(IMGPROXY_WATERMARK_PATH 等,6.7 实测:无水印源时输出与无水印逐字节一致)。
Q4:源 URL 换个写法就 404 Invalid source URL?
IMGPROXY_ALLOWED_SOURCES 按字符串正则匹配(5.4 实测:origin 与 127.0.0.1:8141 指向同源但只放行了前者),把所有在用拼写加进白名单。
Q5:并发一上来就 429?
先确认 WORKERS/REQUESTS_QUEUE_SIZE 是否够用(8.2 实测 1+1 时 40 并发拒 26 个),客户端做退避重试;CDN 命中率上来后 429 会自然缓解。
Q6:/info 永远 403?
开源版不含 /info(Pro 功能,3.4 实测四种签名形态均 403)。拿元信息改用调试响应头 X-Origin-*/X-Result-*(7.2)。
Q7:怎么快速判断某次失败发生在哪一层?
响应头三看:有 Server: imgproxy + X-Request-Id → 业务层错误,拿 X-Request-Id 查日志 error= 字段;没有 Server 头 → 框架层 400(8.4);Content-Length: 9 的 Forbidden → 签名问题(4.4)。
十、参考链接
- 本系列:《imgproxy 4.0 介绍与选型指南》《imgproxy 4.0 Docker Compose 部署指南》《imgproxy 4.0 配置参数详解》
- 官方文档(4.0.x,检索于 2026-10-26):
- Signing a URL(签名算法定义与官方示例)
- Generating the URL(处理选项全集)
- Getting an image info(
/info端点,页面标注 Pro) - Configuration options(
IMGPROXY_*全量参数)