
PostgREST 为img标签提供图片基于自定义媒体类型与响应头 GUC 的文件下载端点实战【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest导读本文基于 PostgREST 官方 how-todocs/how-tos/providing-images-for-img.rst讲解如何在不引入任何客户端 JavaScript 的前提下为 HTMLimg标签提供一个能直接返回二进制图片的 REST 端点。核心思路是利用 PostgREST 的**自定义媒体类型Custom Media Type Handler**能力把 PostgreSQL 的domain当作 MIME 类型声明把返回该 domain 的数据库函数当作媒体处理器从而让浏览器默认发出的Accept: image/webp请求也能命中端点并返回原始字节流。读完本文你将掌握最小可用的二进制图片端点、为响应设置Content-Type/Content-Disposition/Cache-Control等响应头的 GUC 机制response.headers、以及结合pg_byteamagic扩展与*/*万能处理器的完整生产级方案。注意把二进制文件直接存在数据库里通常不是最佳实践多数场景下独立的对象存储服务如 S3、OSS更合适。本方案适用于小文件、演示项目或数据强关联如与业务行同事务存储的场景。PostgreSQL 官方 Wiki 对此有专门讨论Storing Binary files in the Database。背景为什么img不能直接访问/rpc/filePostgREST 默认只内置 JSON、CSV 等少量媒体类型。对于函数返回的bytea二进制直接通过普通 RPC 端点访问时PostgREST 会尝试把结果序列化为 JSONimg标签拿到的不是图片字节流自然无法渲染。问题的关键还在于 HTTP 内容协商content negotiation浏览器请求图片时不会发送Accept: application/octet-stream而是发送形如Accept: image/webp,image/apng,*/*;q0.8的默认头。PostgREST 的媒体类型处理器机制正是为此设计的——仓库测试 test/spec/Feature/Query/CustomMediaSpec.hs 中验证了请求application/octet-stream而端点不支持时返回PGRST107406 Not Acceptable的行为。PostgREST 的自定义媒体类型规则详见 docs/references/api/media_type_handlers.rst媒体类型通过PostgreSQL domain声明domain 名称必须符合 RFC 6838 的媒体类型命名规范即形如type/subtype返回该 domain 类型的函数即成为该媒体类型的handlerdomain 名称长度受 PostgreSQL 标识符长度限制63 字节超长的媒体类型如application/vnd.openxmlformats-officedocument.wordprocessingml.document无法用 domain 表达需要退而使用*/*万能处理器PostgREST 自身的 vendor 媒体类型application/vnd.pgrst.plan、application/vnd.pgrst.object、application/vnd.pgrst.array不能被覆盖。最小示例用application/octet-stream返回图片1. 建表存放二进制文件create table files( id int primary key , blob bytea );2. 声明媒体类型 domain 并创建返回函数假设表中id 42的行存放了一张小猫图片。为了能通过 API 以二进制形式取回它先创建一个名称恰好为application/octet-stream的 domain再让函数返回该 domaincreate domain application/octet-stream as bytea; create or replace function file(id int) returns application/octet-stream as $$ select blob from files where id file.id; $$ language sql;函数体中的file.id是对函数自身参数id的限定引用SQL 语言函数在解析列名时优先匹配表列这里显式限定避免歧义。3. 用 curl 验证curl localhost:3000/rpc/file?id42 -H Accept: application/octet-stream此时 PostgREST 会把函数返回值以原始字节流输出类似 docs/references/api/media_type_handlers.rst 中 TWKB 示例的# binary output并自动把Content-Type设为application/octet-stream。4. 问题浏览器不发送该 Accept 头直接把上面的 URL 塞进img标签无法显示因为浏览器默认发的是Accept: image/webpChrome、Firefox 等主流浏览器都会带上image/webp。PostgREST 没有匹配的处理器会返回 406。解决办法很简单把媒体类型从application/octet-stream改成浏览器默认接受的image/webpcreate domain image/webp as bytea; create or replace function file(id int) returns image/webp as $$ select blob from files where id file.id; $$ language sql;这样即使图片本身不是 WebP 格式也能正常返回PostgREST 不做格式转换只负责字节透传img就能显示了img srchttp://localhost:3000/file?id42 altCute Kittens/注意此时路径是/file?id42而不是/rpc/file?id42——PostgREST 允许通过?select...之外的直接函数调用形式两种 URL 均可用区别在于路径风格详见 docs/references/api/functions.rst。改进版Content-Type、文件名与缓存最小示例有三个明显短板Content-Type写死为image/webp如果库里存的是 PNG/JPEG这个头是错的浏览器可能按 WebP 解码失败或 MIME 嗅探异常下载文件名不友好直接请求/files?selectblobideq.42时浏览器另存为默认文件名是表名files用户体验差响应无缓存头每次img加载都会穿透到数据库图片不常变化时造成不必要的负载。改进思路把媒体类型、文件名也存进数据库在函数内部动态生成响应头。1. 扩展表结构alter table files add column type text generated always as (byteamagic_mime(substr(blob, 0, 4100))) stored, add column name text;这里借助 pg_byteamagic 扩展提供的byteamagic_mime()函数根据文件开头的魔数magic bytes自动推断 MIME 类型并生成存储列。只读取文件前 4100 字节来猜测类型比读整个 blob 高效得多对绝大多数常见格式判断依据都集中在文件头部。2. 升级函数*/*万能处理器 动态响应头create domain */* as bytea; create function file(id int) returns */* as $$ declare headers text; declare blob bytea; begin select format( [{Content-Type: %s}, {Content-Disposition: inline; filename\%s\}, {Cache-Control: max-age259200}] , files.type, files.name) from files where files.id file.id into headers; perform set_config(response.headers, headers, true); select files.blob from files where files.id file.id into blob; if FOUND -- special var, see https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS then return(blob); else raise sqlstate PT404 using message NOT FOUND, detail File not found, hint format(%s seems to be an invalid file id, file.id); end if; end $$ language plpgsql;使用方式不变img srchttp://localhost:3000/rpc/file?id42 altCute Kittens/3. 原理拆解*/*domain 是万能处理器。根据 docs/references/api/media_type_handlers.rst 中的any_handler定义它对所有媒体类型都响应甚至包括不带Accept头的请求默认把Content-Type设为application/octet-stream但可以通过response.headersGUC 覆盖它覆盖所有其他处理器内置或自定义所以应尽量只在独立的函数/视图上使用避免影响其他端点。本函数返回*/*意味着无论浏览器发什么Acceptimage/webp、image/png、*/*或干脆不带请求都能命中。response.headersGUC 是动态响应头的关键。其底层实现在 src/library/PostgREST/Response/GucHeader.hsPostgREST 解析SET LOCAL response.headers [{Set-Cookie: ..}]这样的 JSON 字符串每个对象必须是单键单值FromJSON实例只接受恰好一个键值对然后作为 HTTP 响应头注入。完整规则见 docs/references/transactions.rstresponse.headers必须是一个单键对象组成的 JSON 数组而不是单个多键对象——因为Cache-Control、Set-Cookie这类头需要重复出现才能表达多个值多键对象无法表达重复键PostgREST 提供的Content-Type、Location等头可以被覆盖但除非使用了自定义媒体类型否则响应体仍会被序列化为 JSON——这正是本方案用*/*返回原始字节的前提如果 JSON 格式非法例如非数组、对象多键、值非字符串PostgREST 返回PGRST111HTTP 500对应源码 src/library/PostgREST/Error.hs 的错误消息response.headers guc must be a JSON array composed of objects with a single key and a string value。本例通过set_config(response.headers, headers, true)设置三个头头值作用Content-Typefiles.type如image/png按文件真实格式返回浏览器正确解码Content-Dispositioninline; filenamefiles.nameinline表示页内展示而非强制下载附件同时提供另存为时的文件名Cache-Controlmax-age259200客户端缓存 3 天259200 秒减少对数据库的重复请求注意第三个参数true表示事务级SET LOCAL语义请求结束自动失效不会污染其他请求。PT404是 PostgREST 预定义的 SQLSTATE 错误码。raise sqlstate PT404会让 PostgREST 返回 HTTP 404message/detail/hint会映射到 JSON 错误体的同名字段。PostgREST 预留了PTxyz系列错误码PT4xx与 HTTP 状态码一一对应如PT402→ 402相关机制在 docs/references/errors.rst 中有完整说明包括用PGRSTSQLSTATE 配合 JSON 化的 message/detail 精确控制状态码与响应头。4. 生产环境补充函数内设置的Cache-Control只是客户端缓存。对于生产环境建议在反向代理层如 Nginx再叠加缓存与限流策略——PostgREST 官方文档 docs/explanations/nginx.rst 给出了完整 Nginx 代理配置范例包括upstream定义、location /api/代理转发以及针对/rpc/login/之类热点的limit_req_zone限流配置。深入从源码与测试看媒体处理器的行为边界Content-Type 的自动设置与覆盖PostgREST 对 handler 函数返回的 domain 媒体类型会自动在响应中设置对应的Content-Type见 media_type_handlers 文档中 TWKB 示例的响应Content-Type: application/vnd.twkb。而*/*处理器默认落到application/octet-stream需要时再通过response.headers覆盖——这正是本 how-to 改进版的做法。测试用例的印证仓库测试 test/spec/Feature/Query/CustomMediaSpec.hs 系统验证了这些行为请求不支持的媒体类型返回PGRST107406与提示消息如None of these media types are available: application/octet-stream*/*处理器接受任意媒体类型并默认返回application/octet-stream可显式覆盖*/*的Content-Type测试夹具 test/spec/fixtures/schema.sql 中定义了create domain application/octet-stream as bytea;作为内置演示。此外test/spec/Feature/Query/RpcSpec.hs 中还有针对response.headers非法 JSON 触发PGRST111的断言{message:response.headers guc must be a JSON array composed of objects with a single key and a string value,code:PGRST111,...}佐证了响应头 GUC 的格式约束。内容协商细节浏览器对img发出的默认请求头image/webp,image/apng,*/*;q0.8等在 test/spec/Feature/Query/RawOutputTypesSpec.hs 中被定义为chromeAcceptHdrs并用于测试说明 PostgREST 官方测试同样关注浏览器默认 Accept 场景——这也解释了为什么 how-to 选择image/webp作为最小示例的媒体类型。总结方案适用场景要点最小示例image/webpdomain快速验证、格式固定的单类型图片定义 domain → 函数返回 domain →img直接引用改进版*/*response.headers生产级、多格式文件动态Content-Type、Content-Disposition文件名、客户端缓存、404 处理核心要点回顾媒体类型即 domainPostgREST 用 PostgreSQL domain 名称表达 MIME 类型函数返回该 domain 即成为媒体类型处理器*/*是万能处理器响应一切Accept默认application/octet-stream可用response.headers覆盖Content-Typeresponse.headersGUC 提供动态响应头必须是单键对象数组事务级生效格式错误会触发PGRST111错误处理用PTxyzSQLSTATE如PT404直接映射 HTTP 404缓存要分层函数内设置客户端缓存生产环境再叠加反向代理层缓存与限流。本方案同样适用于任意二进制文件PDF、视频片段、导出文件等的分发只需在a标签的href中指向对应 RPC 端点即可原理完全相同。【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考