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 错误码与限流超时排障闭环

imgproxyimgproxy图片处理2026/10/26

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.jpg1920×1080,104248 字节主测试图(JPEG,含 Exif)
alpha-800x600.png800×600,4759 字节透明通道 PNG
exif-photo-1200x800.jpg1200×800,55715 字节含 Exif 元数据的 JPEG
watermark-300x120.png300×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 /healthGET健康检查
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 /health200健康检查,2.1 节
GET /200说明页
GET /info/...403Pro 功能,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 /." 落到操作上是四步:

  1. 取"签名之后的路径":处理 URL 为 /<处理选项>/<源URL>.<扩展名>(或 /<处理选项>/plain/<明文URL>@<扩展名>),注意包含前导斜杠、包含扩展名;
  2. 拼接盐:把十六进制解码后的 SALT 字节串放在路径字节前面;
  3. 计算 HMAC-SHA256:密钥为十六进制解码后的 KEY;
  4. 编码: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 的三种形态对比

形态拼写适用场景备注
Base64aHR0cDovL...(URL-safe Base64、去填充)URL 含特殊字符、希望整体路径干净最通用;local:///path 也走此形态
plainplain/http://origin/a.jpg@jpg调试直观、URL 规范可预测@ 后为输出扩展名;URL 必须转义特殊字符
encenc/<加密串>不想暴露源地址需服务端配 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-TypeContent-Length
/rs:fill:600:600:1/<b64>.webpimage/webp7918
/rs:fill:600:600:1/<b64>.avifimage/avif7357
/rs:fill:600:600:1/<b64>.jpgimage/jpeg17860
/rs:fill:600:600:1/<b64>.pngimage/png49675
/rs:fill:600:600:1/f:webp/<b64>.jpgimage/jpeg17860
/rs:fill:600:600:1/f:webp/<b64>(无扩展名)image/webp7918

代表性命令与回显(.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:1600×33812875
rs:fill:600:600:1600×60017860
rs:auto:600:600:1600×60017860
rs:force:600:400:1600×400(拉伸变形)14077
rs:fit:300:600:1300×1695835
rs:auto:300:600:1300×1695835
s:400:300:1400×2257871
rt:fill + s:600:600:1600×60017860

代表性命令与回显(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>.jpg18614
/rs:fill:600:600:1/bg:255:0:0/<alpha_b64>.jpg17251

另一组 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.png19960水印生效
未配置任何水印源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 行为准,逐项说明:

响应头实测值(示例)说明
Serverimgproxy固定标识,便于探活识别
Content-Security-Policydefault-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox说明页防 XSS 的 CSP,图片响应同样携带
X-Request-Idc7ymJnPe58ZzVyRb8sr_C每请求唯一 ID,与访问日志 request_id 对应,排障时拿它对账
Cache-Controlmax-age=86400, public由 IMGPROXY_TTL=86400 决定,允许 CDN 缓存
Etag"FS_7xMa7AW6Xs0WLY5Edu2aMdhH2h8UNwFcMFQ3vwq8"条件请求用,见 7.3
Last-ModifiedSun, 25 Oct 2026 14:02:44 GMT源文件修改时间,与 ETag 双轨协商
Content-Dispositioninline; filename="photo-1920x1080.jpg"默认继承源文件名,fn: 可覆盖(6.10)
Content-Type / Content-Lengthimage/jpeg / 13768输出格式与体积

7.2 调试响应头

IMGPROXY_ENABLE_DEBUG_HEADERS=true 时,imgproxy 追加五个调试头(本文全部 200 实测均携带):

响应头实测示例含义
X-Origin-Width / X-Origin-Height1920 / 1080源图尺寸
X-Origin-Content-Length104248源图字节数
X-Result-Width / X-Result-Height640 / 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:1404Invalid URL(11 字节)
非法尺寸数值 rs:fit:abc:abc:1404Invalid URL
非法重力 g:zz404Invalid URL
未知预设 pr:nosuch404Invalid URL
exp 已过期404Invalid URL
白名单拒绝的源 URL404Invalid source URL(18 字节)
源文件不存在(nope.jpg)404Source is unreachable(21 字节)
源不是图片(fake.jpg)422Invalid source image(20 字节)
缺签名 / 坏签名 / unsafe403Forbidden(9 字节)
IMGPROXY_MAX_SRC_RESOLUTION 超限的大图404Invalid URL(配置篇实测)
处理队列打满429Too Many Requests(17 字节)
源站超时504Source is unreachable
路径含畸形百分号编码(%%zz)400400 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)。

十、参考链接