:基于 HTTP 内容协商的响应格式控制完全指南)
PostgREST 资源表示Resource Representation基于 HTTP 内容协商的响应格式控制完全指南【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrestPostgREST 将 Postgres 数据库暴露为 REST API其资源表示遵循 RFC 7231 的 HTTP 内容协商机制同一个 API 端点可以根据请求中Accept头的内容以 JSON、CSV 等不同格式返回数据。本文以 resource_representation.rst 为主体结合仓库源码MediaType.hs、Plan/Negotiate.hs与测试用例系统讲解响应格式协商、内置媒体类型处理器、单数/复数资源控制、null 剥离以及请求体媒体类型帮助读者掌握精确控制 PostgREST 输入输出格式的完整能力。核心机制HTTP 内容协商RFC 7231PostgREST 遵循 RFC 7231 第 5.3 节规定的 HTTP 内容协商机制来交付资源表示。这意味着同一个 API 端点可以响应不同格式如 JSON 或 CSV具体格式由客户端请求决定服务端无需为每种格式提供不同 URL。在源码层面内容协商由 Plan/Negotiate.hs 中的negotiateContent函数实现-- | Do content negotiation. i.e. choose a media type based on the -- intersection of accepted/produced media types. negotiateContent :: AppConfig - ApiRequest - QualifiedIdentifier - [MediaType] - MediaHandlerMap - Bool - Either ApiRequestError ResolvedHandler其核心逻辑是把请求头Accept中列出的媒体类型与目标资源能产出的媒体类型求交集选择第一个匹配项firstAcceptedPick listToMaybe $ mapMaybe matchMT accepts。如果交集为空Nothing则返回MediaTypeError对应 HTTP 415 状态码。客户端的Accept头在 ApiRequest.hs 中被解析iAcceptMediaType maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader accept注意两个细节未发送Accept头时默认值是[MTAny]即*/*一个Accept头可以包含多个媒体类型逗号分隔PostgREST 会按顺序逐一尝试匹配。响应格式Response Format通过 Accept 头指定使用Accept请求头来指定响应可接受的格式可以指定一种或多种curl http://localhost:3000/people \ -H Accept: application/json关于列顺序的说明响应的列顺序不保证与select子句中指定的顺序一致。例如带资源嵌入resource embedding的请求http://localhost:3000/films?selectdirectors(last_name,id),title返回结果可能是[ { title: title, directors: { id: 5, last_name: name } } ]这与 JSON Schema 规范2020-12 版对对象的定义一致——object: An unordered set of properties mapping a string to an instance即 JSON 对象是字符串到实例的无序映射。因此依赖响应中字段顺序的客户端代码是不可靠的应始终通过字段名访问属性。资源嵌入的完整用法参见 resource_embedding.rst。内置媒体类型处理器Builtin Media Type HandlersPostgREST 为常见标准媒体类型提供了内置处理器媒体类型适用端点说明text/csv与application/json所有 API 端点表/视图见 tables_views.rst函数见 functions.rstapplication/openapijson根端点/返回 OpenAPI 文档见 openapi.rstapplication/geojson相关端点GeoJSON 格式*/*所有端点对 API 端点解析为application/json对根端点解析为application/openapijson供应商媒体类型Vendor Media TypesPostgREST 还支持以下供应商vendor媒体类型处理器application/vnd.pgrst.plan返回执行计划EXPLAIN见 media_type_handlers.rst 中的计划相关章节application/vnd.pgrst.object与application/vnd.pgrst.array控制单数/复数响应与 null 剥离详见下文单数或复数与剥离空值两节。在源码 MediaType.hs 中这些媒体类型被建模为 Haskell 代数数据类型data MediaType MTApplicationJSON | MTGeoJSON | MTTextCSV | MTTextPlain | MTTextXML | MTOpenAPI | MTUrlEncoded | MTOctetStream | MTAny | MTOther Text -- vendored media types | MTVndArrayJSONStrip | MTVndSingularJSON Bool | MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]其中MTVndPlan携带格式PlanJSON/PlanText与选项analyze、verbose、settings、buffers、wal并可通过for...参数指定目标媒体类型。无法识别的媒体类型会报错任何无法识别的媒体类型都会导致错误。例如curl http://localhost:3000/people \ -H Accept: unknown/unknown返回HTTP/1.1 415 Unsupported Media Type {code:PGRST107,details:null,hint:null,message:None of these media types are available: unknown/unknown}该错误码在 Error.hs 中定义code MediaTypeError{} PGRST107对应 HTTP 415 状态。从negotiateContent的实现可以看到MediaTypeError中携带的是客户端 Accept 的完整媒体类型列表map MediaType.toMime accepts便于排错。如需扩展可接受的媒体类型可以使用自定义媒体类型处理器custom media types详见 media_type_handlers.rst。单数或复数响应Singular or Plural默认情况下PostgREST总是以数组形式返回 JSON 结果即使只有一条记录。例如请求/items?ideq.1返回[ { id: 1 } ]这对某些客户端代码可能不够方便。要返回不被数组包裹的单数对象第一条结果在Accept头中指定vnd.pgrst.objectcurl http://localhost:3000/items?ideq.1 \ -H Accept: application/vnd.pgrst.objectjson此时返回{ id: 1 }注意vnd.pgrst.object和vnd.pgrst.array属于供应商媒体类型具有特殊处理逻辑不能被自定义媒体类型处理器覆盖。这一点在 Plan/Negotiate.hs 中有明确注释all the vendored media types have special handling as they have media type parameters, they cannot be overridden对应测试见 CustomMediaSpec.hs。空结果时的行为差异当请求单数响应但未找到任何记录时服务端返回错误消息和 406 Not Acceptable 状态码而不是通常的空数组 200{ code: PGRST116, message: Cannot coerce the result to a single JSON object, details: The result contains 0 rows, hint: null }源码中的判定逻辑位于 MainTx.hsfailNotSingular :: MediaType - ResultSet - DbHandler () failNotSingular mediaType RSStandard{rsQueryTotalqueryTotal} when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] queryTotal / 1) $ do lift SQL.condemn throwError $ Error.ApiRequestErr . Error.SingularityError $ toInteger queryTotal即只要媒体类型是单数 JSON无论是否剥离 null且查询总行数queryTotal不等于 1就抛出SingularityError。该错误的 HTTP 状态码为 406见 Error.hs错误码为PGRST116details字段动态携带实际行数The result contains show n rows见 Error.hs。这也意味着查询返回多于 1 行时同样会触发该错误queryTotal / 1单数响应要求查询结果恰好为一行。为什么不用/stories/1这种嵌套 URL很多 API 用特殊的嵌套 URL 约定来区分单数和复数资源如/stories与/stories/1。PostgREST 使用/stories?ideq.1的原因在于单数资源对 PostgREST 而言是由主键确定的一行而主键可以是复合主键跨越多个列常见的嵌套 URL 只考虑了简单且绝大多数为数值型主键的特例这些所谓的人工键artificial keys通常由对象关系映射ORM库自动引入诚然PostgREST 可以检测到对所有构成主键的列都有等值条件从而自动转换为单数但这可能导致格式的意外变化——客户端仅仅因为多过滤了一个列响应格式就从对象变成数组从而破坏已有客户端代码因此PostgREST 选择让手动显式指定单数/复数把这一选择与 URL 格式解耦行为可预测。剥离空值Stripped Nulls默认情况下PostgREST 返回所有JSON null 值。例如请求/projects?idgt.10返回[ { id: 11, name: OSX, client_id: 1, another_col: val }, { id: 12, name: ProjectX, client_id: null, another_col: null }, { id: 13, name: Y, client_id: null, another_col: null } ]在大结果集上值为null的未使用键会白白浪费带宽。要移除它们把nullsstripped作为application/vnd.pgrst.array的参数curl http://localhost:3000/projects?idgt.10 \ -H Accept: application/vnd.pgrst.arrayjson;nullsstripped此时返回[ { id: 11, name: OSX, client_id: 1, another_col: val }, { id: 12, name: ProjectX }, { id: 13, name: Y } ]源码中的实现细节在 MediaType.hs 的decodeMediaType中nulls参数通过解析媒体类型参数获得(application, vnd.pgrst.objectjson, _) - MTVndSingularJSON strippedNulls (application, vnd.pgrst.object, _) - MTVndSingularJSON strippedNulls (application, vnd.pgrst.arrayjson, _) - checkArrayNullStrip (application, vnd.pgrst.array, _) - checkArrayNullStrip ... strippedNulls fromMaybe false (params !? nulls) stripped checkArrayNullStrip if strippedNulls then MTVndArrayJSONStrip else MTApplicationJSON关键点参数名不区分大小写按 RFC 7231 规范归一化为小写nulls参数只有精确等于stripped时才生效application/vnd.pgrst.arrayjson未带nullsstripped时等价于普通的application/jsoncheckArrayNullStrip返回MTApplicationJSONapplication/vnd.pgrst.objectjson;nullsstripped表示单数响应 剥离 null的组合MTVndSingularJSON Bool中的Bool正是用于记录是否剥离 null。对应响应时的Content-Type由 MediaType.hs 的toMime生成toMime MTVndArrayJSONStrip application/vnd.pgrst.arrayjson;nullsstripped toMime (MTVndSingularJSON True) application/vnd.pgrst.objectjson;nullsstripped toMime (MTVndSingularJSON False) application/vnd.pgrst.objectjson该功能的完整行为有专门测试覆盖见 NullsStripSpec.hs其中分别验证了application/vnd.pgrst.arrayjson;nullsstripped与application/vnd.pgrst.array;nullsstripped省略json后缀两种写法。补充供应商媒体类型对 OpenAPI 的影响在 Response/OpenAPI.hs 中可以看到PostgREST 生成的 OpenAPI 文档把produces与consumes声明为[MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]即文档明确告知客户端端点可产出/消费 JSON、单数 JSON含剥离 null 变体与 CSV。请求体Request Body服务端处理以下请求体媒体类型application/jsonapplication/x-www-form-urlencodedtext/csv对于表/视图见 tables_views.rst上述媒体类型适用于POST、PATCH和PUT方法对于函数见 functions.rst适用于POST方法。对于函数还有三种额外的请求体媒体类型application/octet-streamtext/plaintext/xml这些二进制/文本/XML 类型的请求体用于向函数传递单一未命名参数single unnamed argument详见 functions.rst 中的相关章节。源码中的请求体解析请求体的Content-Type头在 ApiRequest.hs 中被解析默认application/json实际解析逻辑位于 ApiRequest/Payload.hs(MTTextCSV, _) - do json - csvToJson $ first BS.pack (CSV.decodeByName reqBody) note All lines must have same number of fields $ payloadAttributes (JSON.encode json) json (MTUrlEncoded, True) - Right $ ProcessedUrlEncoded params (S.fromList $ fst $ params) ... (MTTextPlain, True) - Right $ RawPay reqBody (MTTextXML, True) - Right $ RawPay reqBody (MTOctetStream, True) - Right $ RawPay reqBody (ct, _) - Left $ Content-Type not acceptable: MediaType.toMime ct从中可以确认CSV 请求体会被解析为 JSON使用CSV.decodeByName要求所有行的字段数一致否则报错application/x-www-form-urlencoded被解析为参数映射text/plain、text/xml、application/octet-stream仅对函数调用True表示是过程调用以原始字节形式RawPay传递不支持的Content-Type返回Content-Type not acceptable: ...错误。小结PostgREST 的资源表示能力可归纳为一张速查表需求写法指定响应格式Accept: application/json、Accept: text/csv、Accept: application/geojson单数响应Accept: application/vnd.pgrst.objectjson剥离 null 的数组Accept: application/vnd.pgrst.arrayjson;nullsstripped单数 剥离 nullAccept: application/vnd.pgrst.objectjson;nullsstripped查看执行计划Accept: application/vnd.pgrst.planjson需启用 db-plan见 media_type_handlers.rst请求体格式Content-Type: application/json、application/x-www-form-urlencoded、text/csv函数另支持text/plain、text/xml、application/octet-stream需要记住的三个关键行为列顺序不保证不要依赖 JSON 响应中字段的排列顺序单数响应是显式行为vnd.pgrst.object在结果不为恰好一行时返回 406PGRST116不会静默降级供应商媒体类型不可被自定义处理器覆盖vnd.pgrst.*系列由 PostgREST 内置逻辑独占处理。通过合理组合Accept头与媒体类型参数你可以在不改动数据库与业务代码的前提下为不同客户端浏览器、移动端、数据管道提供最贴合其需求的资源表示。【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考