Directus 12.5 API 使用详解

Directus 12.5.0 REST/GraphQL API 全流程 curl 实测:登录/refresh 轮换/静态令牌/登出四种凭证、Collections 与 Items CRUD、filter/sort/limit/offset/search/aggregate 查询体系、文件上传与资产下载、用户角色权限边界(v12 Policy 模型)、GraphQL 权限裁剪与类型差异对比,含 INVALID_CREDENTIALS/TOKEN_EXPIRED/FORBIDDEN 等错误码实录,回显逐字照抄(2026-10-19)。

DirectusDirectusCMS2026/10/19

Directus 12.5 API 使用详解

目标:用 curl 把 Directus 12.5.0 的对外 API 完整跑通一遍——认证(登录/刷新/静态令牌/登出)、Collections 与 Items CRUD、查询参数体系(fields/filter/sort/limit/offset/search/aggregate/meta)、文件上传与资产下载、用户角色权限边界、GraphQL 与 REST 对比,全部 JSON 回显逐字照抄自 2026-10-19 在本机 8103 实例上的实测。实例部署见 《Directus Docker Compose 部署指南》,参数含义见 《Directus 12.5 配置参数详解》。

一、环境信息

项目值
服务器 IP192.168.146.128(网卡 ens33)
操作系统Ubuntu 22.04.4 LTS(内核 6.8.0-138-generic)
Directus 实例directus/directus:12.5.0,API 基址 http://192.168.146.128:8103(应用内监听 8055)
数据库 / 缓存PostgreSQL 17 / Redis 7
管理员admin@example.com / q1w2e3r4,user id 7103b67b-b376-4ffb-ac0d-14c5ccfd17db
实测普通账号viewer@example.com / q1w2e3r4(仅 articles 只读权限),角色 c1-viewer
实测业务集合articles(id/title/status,实测时含 id=1、2 两条)
实测静态令牌svc-ops-token-2026(挂在管理员账号上)
请求工具curl 7.81.0
敏感值处理access_token 为 15 分钟有效的 JWT,发布时均已过期,长 token 标注处截断

实测约定:命令中 $TOKEN 为管理员登录返回的 access_token(取值方法见第四章);HTTP 状态码与 JSON 回显逐字照抄,长响应标注「节选」。

二、前置条件

2.1 端点与协议

  • REST 基址:http://192.168.146.128:8103,全部走 HTTP/1.1 + JSON;
  • GraphQL 端点:同端口 /graphql(POST),订阅走 WebSocket /websocket(启动日志实测 GraphQL Subscriptions started at 0.0.0.0:8055/graphql);
  • 探活:GET /server/ping 免鉴权。

2.2 客户端工具

系统自带 curl 7.81.0 即可完成全部实测;JSON 美化用 python3 -m json.tool。无额外依赖。

2.3 实测业务数据准备

articles 集合在《配置参数详解》实测时已创建(id/title/status 三个字段),本章在其上做 CRUD;id=1 标题为「缓存实测第四版」(draft),id=2 标题为「API 实测文章」(published)。

三、API 总览

3.1 端点族

端点族前缀鉴权本章章节
认证/auth/login、/auth/refresh、/auth/logout邮箱口令 / refresh_token四
服务器/server/ping、/server/infoping 免鉴权,info 需 token四
集合定义/collectionsBearer token五
条目数据/items/<collection>Bearer token + 权限六
文件/files、/assets/<id>Bearer token + 权限七
用户与权限/users、/roles、/policiesBearer token(管理权限)八
GraphQL/graphqlBearer token + 权限九

3.2 三种凭证怎么选

凭证形态有效期适用
access_token(登录换得)JWT,Authorization: Bearer 头15 分钟(ACCESS_TOKEN_TTL 默认,回显 expires=900000ms 佐证)浏览器/交互场景,配合 refresh 轮换
refresh_token不透明随机串7 天(REFRESH_TOKEN_TTL 默认)免登出期间长期续期;一次性轮换
静态 token(用户 token 字段)自定义字符串,Authorization: Bearer 头长期有效直至撤销服务端集成、CI 脚本

3.3 统一错误结构

所有错误返回统一 JSON 结构:{"errors":[{"message":"...","extensions":{"code":"错误码"}}]},常用错误码实测收集:INVALID_CREDENTIALS(口令错/refresh 失效,401)、TOKEN_EXPIRED(JWT 过期,401)、INVALID_TOKEN(签名校验失败,403)、FORBIDDEN(无权限,403)、GRAPHQL_VALIDATION(GraphQL 校验,400)、INTERNAL_SERVER_ERROR(服务端异常,500)。

四、认证与首次调用

4.1 登录换令牌

curl -s -X POST http://192.168.146.128:8103/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"q1w2e3r4"}' \
  -w "\nHTTP %{http_code}\n"

实测回显(access_token 为 15 分钟有效登录态,发布时已过期,照抄):

{"data":{"expires":900000,"refresh_token":"FgXS8KwcPNPc28sSiS0uAfELMkau7j3jlO99U-KuHhxvaWGRSnwYMYEggMbQVGIb","access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjcxMDNiNjdiLWIzNzYtNGZmYi1hYzBkLTE0YzVjY2ZkMTdkYiIsInJvbGUiOiIzMzdkZmNkYy1jNTMyLTQ5NGItOTk2Yy04NjY5ZmJjMGUzNjgiLCJhcHBfYWNjZXNzIjp0cnVlLCJhZG1pbl9hY2Nlc3MiOnRydWUsImlhdCI6MTc5MjM3MjExNCwiZXhwIjoxNzkyMzczMDE0LCJpc3MiOiJkaXJlY3R1cyJ9.1bQS5xilW0YXFTfbjMup46F59sxa5VJcEwSu7dJsSNg"}}
HTTP 200

expires: 900000(毫秒)即 15 分钟,与 ACCESS_TOKEN_TTL 默认值一致。

4.2 错误口令(错误码实测)

curl -s -X POST http://192.168.146.128:8103/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"wrong-pass"}' \
  -w "\nHTTP %{http_code}\n"

实测回显:

{"errors":[{"message":"Invalid user credentials.","extensions":{"code":"INVALID_CREDENTIALS"}}]}
HTTP 401

4.3 首次调用 /users/me

TOKEN=<登录回显中的 access_token>
curl -s http://192.168.146.128:8103/users/me -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

实测回显(节选):

{
    "data": {
        "id": "7103b67b-b376-4ffb-ac0d-14c5ccfd17db",
        "first_name": "Admin",
        "email": "admin@example.com",
        "status": "active",
        "role": "337dfcdc-c532-494b-996c-8669fbc0e368",
        "provider": "default",
        "policies": []
    }
}

4.4 refresh 轮换(一次性)

RT=<登录回显中的 refresh_token>
curl -s -X POST http://192.168.146.128:8103/auth/refresh \
  -H 'Content-Type: application/json' \
  -d "{\"refresh_token\":\"$RT\",\"mode\":\"json\"}" \
  -o /tmp/refresh.json -w 'HTTP %{http_code}\n'
python3 -c "import json; d=json.load(open('/tmp/refresh.json'))['data']; print('expires:', d['expires']); print('access_token:', d['access_token'][:60]+'...(截断)')"

实测回显:

HTTP 200
expires: 900000
access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjcxMDNiNjdiLWI...(截断)

refresh_token 是一次性轮换的:用旧值再刷一次,实测立即失效:

{"errors":[{"message":"Invalid user credentials.","extensions":{"code":"INVALID_CREDENTIALS"}}]}

HTTP 401

客户端逻辑必须保存每次 refresh 返回的新 refresh_token。

4.5 静态 token(服务端集成推荐)

给用户设置 token 字段即签发静态令牌(长期有效,撤销 = 置空):

curl -s -X PATCH http://192.168.146.128:8103/users/7103b67b-b376-4ffb-ac0d-14c5ccfd17db \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"token":"svc-ops-token-2026"}' -o /dev/null -w 'PATCH HTTP %{http_code}\n'

实测回显:

PATCH HTTP 200

用静态令牌调用(无需登录,头形态与 JWT 相同):

curl -s http://192.168.146.128:8103/users/me -H "Authorization: Bearer svc-ops-token-2026" \
  | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print('email:',d['email'],'id:',d['id'])"

实测回显:

email: admin@example.com id: 7103b67b-b376-4ffb-ac0d-14c5ccfd17db

4.6 登出

curl -s -X POST http://192.168.146.128:8103/auth/logout \
  -H 'Content-Type: application/json' \
  -d "{\"refresh_token\":\"$RT\"}" -o /dev/null -w 'HTTP %{http_code}\n'

实测回显:

HTTP 204

登出后再用该 refresh_token 续期,实测被拒(INVALID_CREDENTIALS,401)——会话已销毁。

4.7 探活与服务器信息

curl -s http://192.168.146.128:8103/server/ping -w " HTTP %{http_code}\n"
curl -s http://192.168.146.128:8103/server/info -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

实测回显(info 节选):

pong HTTP 200
{
    "data": {
        "project": {
            "project_name": "Directus",
            "project_color": "#6644FF",
            "default_language": "en-US"
        }
    }
}

五、集合(Collections)API

集合定义即数据模型:一个集合对应库中一张表。列出全部集合:

curl -s "http://192.168.146.128:8103/collections?limit=3" -H "Authorization: Bearer $TOKEN" \
  | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(len(d),'个集合'); print([c['collection'] for c in d[:4]])"

实测回显:

35 个集合
['articles', 'directus_access', 'directus_activity', 'directus_collections']

⚠️ 注意实测发现:请求带了 limit=3,实际返回了全部 35 个集合——/collections 端点在 12.5 对 limit 参数未生效(其余 /items 端点 limit 正常,见第六章)。

返回里 directus_* 为系统集合(用户/角色/文件/权限/流程等),业务集合与系统集合同库同 API。查单个集合定义:

curl -s "http://192.168.146.128:8103/collections/articles" -H "Authorization: Bearer $TOKEN" \
  | python3 -c "import json,sys; c=json.load(sys.stdin)['data']; print('collection:',c['collection'],'| schema:',c['schema']['name'],'| icon:',c['meta']['icon'])"

实测回显:

collection: articles | schema: articles | icon: article

六、条目(Items)CRUD

6.1 创建(POST)

curl -s -X POST http://192.168.146.128:8103/items/articles \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"API 实测文章","status":"published"}' \
  -w "\nHTTP %{http_code}\n"

实测回显:

{"data":{"id":2,"title":"API 实测文章","status":"published"}}
HTTP 200

6.2 查询参数体系(filter/sort/limit/offset/fields/search)

filter 是 JSON 对象经 URL 编码传入,配合 sort/limit/offset 使用:

curl -s "http://192.168.146.128:8103/items/articles?filter=%7B%22status%22%3A%7B%22_eq%22%3A%22published%22%7D%7D&sort=-id&fields=id,title&limit=1" \
  -H "Authorization: Bearer $TOKEN" -w "\nHTTP %{http_code}\n"

实测回显(filter 为 {"status":{"_eq":"published"}},按 id 倒序取第一条):

{"data":[{"id":3,"title":"API 实测第二篇"}]}
HTTP 200

offset 翻页(跳过 1 条取下一条):

{"data":[{"id":2,"title":"API 实测文章"}]}

search 全文关键词(等效于各字段模糊匹配):

curl -s "http://192.168.146.128:8103/items/articles?search=%E7%AC%AC%E4%BA%8C%E7%AF%87&fields=id,title" -H "Authorization: Bearer $TOKEN"

实测回显:

{"data":[{"id":3,"title":"API 实测第二篇"}]}

6.3 计数与元信息(aggregate / meta)

计数用 aggregate(注意必须用对象写法):

curl -s "http://192.168.146.128:8103/items/articles?aggregate=%7B%22count%22%3A%22*%22%7D" -H "Authorization: Bearer $TOKEN"

实测回显:

{"data":[{"count":"3"}]}

⚠️ 实测发现:官方旧示例的简写 ?aggregate=count:* 在 12.5 上返回 500({"errors":[{"message":"Cannot assign to read only property '0' of string 'count:*'","extensions":{"code":"INTERNAL_SERVER_ERROR"}}]}),必须用对象式 {"count":"*"}。

总条数元信息用 meta 参数:

curl -s "http://192.168.146.128:8103/items/articles?meta=filter_count&fields=id&limit=2" -H "Authorization: Bearer $TOKEN"

实测回显:

{"meta":{"filter_count":3},"data":[{"id":1},{"id":2}]}

💡 实测观察:响应头里并未输出 Content-Range(尽管 CORS 默认 Expose-Headers 含 Content-Range),分页总数以 meta=filter_count 为准。

6.4 单条读取 / 更新 / 删除

curl -s "http://192.168.146.128:8103/items/articles/2?fields=id,title,status" -H "Authorization: Bearer $TOKEN" -w "\nHTTP %{http_code}\n"

实测回显:

{"data":{"id":2,"title":"API 实测文章","status":"published"}}
HTTP 200

更新(PATCH,只传变更字段):

curl -s -X PATCH http://192.168.146.128:8103/items/articles/2 \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"title":"API 实测文章(已修订)"}' -w "\nHTTP %{http_code}\n"

实测回显:

{"data":{"id":2,"title":"API 实测文章(已修订)","status":"published"}}
HTTP 200

删除(DELETE,无响应体):

curl -s -X DELETE http://192.168.146.128:8103/items/articles/3 -H "Authorization: Bearer $TOKEN" -o /dev/null -w 'HTTP %{http_code}\n'

实测回显:

HTTP 204

删除后再查同一条,实测返回 403 而非 404:

{"errors":[{"message":"You don't have permission to access this.","extensions":{"code":"FORBIDDEN"}}]}

HTTP 403

访问不存在的集合同样是 403(Directus 把「不存在」并入「无权限」,避免探测):

{"errors":[{"message":"You don't have permission to access collection \"nonexistent\" or it does not exist. Queried in root.","extensions":{"reason":"You don't have permission to access collection \"nonexistent\" or it does not exist. Queried in root.","code":"FORBIDDEN"}}]}

HTTP 403

七、文件 API

7.1 上传(multipart/form-data)

printf 'Directus D5 api test file 2026-10-19\n' > /tmp/d5-file.txt
curl -s -X POST http://192.168.146.128:8103/files \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/tmp/d5-file.txt;type=text/plain" \
  -w "\nHTTP %{http_code}\n" \
  | python3 -c "
import sys, json
lines = sys.stdin.read().rstrip('\n').split('\n')
d = json.loads(lines[0])['data']
print(json.dumps({k: d[k] for k in ('id','storage','filename_download','type','filesize','uploaded_on')}, ensure_ascii=False))
print(lines[-1])"

实测回显:

{"id": "1017d537-07c9-4227-a078-34a0642bb44b", "storage": "local", "filename_download": "d5-file.txt", "type": "text/plain", "filesize": "37", "uploaded_on": "2026-10-19T01:10:39.217Z"}
HTTP 200

storage: local 表示落在本地存储位置(见《配置参数详解》第十章的落盘核对)。

7.2 文件列表

curl -s "http://192.168.146.128:8103/files?fields=id,title,filename_download&sort=-uploaded_on&limit=2" -H "Authorization: Bearer $TOKEN"

实测回显:

{"data":[{"id":"1017d537-07c9-4227-a078-34a0642bb44b","title":"D5 File","filename_download":"d5-file.txt"},{"id":"fba3d2d4-8fb5-4bf5-8076-d7010209dd4b","title":"C1 Upload","filename_download":"c1-upload.txt"}]}

7.3 资产下载

文件内容经 /assets/<id> 读取,?filename= 可改下载名:

curl -s "http://192.168.146.128:8103/assets/1017d537-07c9-4227-a078-34a0642bb44b?filename=renamed-test.txt" \
  -H "Authorization: Bearer $TOKEN" -w " HTTP %{http_code}\n"

实测回显:

Directus D5 api test file 2026-10-19
 HTTP 200

八、用户、角色与权限

8.1 用户列表与字段权限

curl -s "http://192.168.146.128:8103/users?fields=id,first_name,email,status&limit=2" -H "Authorization: Bearer $TOKEN"

实测回显:

{"data":[{"id":"7103b67b-b376-4ffb-ac0d-14c5ccfd17db","first_name":"Admin","email":"admin@example.com","status":"active"},{"id":"b012bfcf-f600-42d8-a59f-cf14abc16ccb","first_name":"Viewer","email":"viewer@example.com","status":"active"}]}

⚠️ 实测发现:查 /roles 时若带 fields=admin_access,管理员也会被拒:

{"errors":[{"message":"You don't have permission to access field \"admin_access\" in collection \"directus_roles\" or it does not exist. Queried in root.","extensions":{"code":"FORBIDDEN"}}]}

HTTP 403

安全字段查询正常:

curl -s "http://192.168.146.128:8103/roles?fields=id,name&limit=3" -H "Authorization: Bearer $TOKEN"
{"data":[{"id":"05a409d1-3cbb-4a92-b72b-596838eb08cb","name":"c1-viewer"},{"id":"337dfcdc-c532-494b-996c-8669fbc0e368","name":"Administrator"}]}

8.2 角色与 Policy(v12 新模型)

v12 的权限挂在 policy 上,policy 再关联到角色或用户(库表为 directus_access)。查角色时 policies 字段返回的是关联行 ID 而非 policy 本身(实测发现):

curl -s "http://192.168.146.128:8103/roles?fields=id,name,policies&limit=2" -H "Authorization: Bearer $TOKEN"

实测回显(45c68d98…、b135f9fe… 实为 directus_access 行 ID):

{"data":[{"id":"05a409d1-3cbb-4a92-b72b-596838eb08cb","name":"c1-viewer","policies":["45c68d98-dfd8-453a-84c1-71af80900c9e"]},{"id":"337dfcdc-c532-494b-996c-8669fbc0e368","name":"Administrator","policies":["b135f9fe-e8e3-4618-ad2e-a793f5c9a32c"]}]}

要拿真实 policy ID,用嵌套字段 policies.policy:

curl -s "http://192.168.146.128:8103/roles?fields=id,name,policies.policy&limit=2" -H "Authorization: Bearer $TOKEN"

实测回显:

{"data":[{"id":"05a409d1-3cbb-4a92-b72b-596838eb08cb","name":"c1-viewer","policies":[{"policy":"649260da-449a-49eb-8cf5-0ad87664f0b9"}]},{"id":"337dfcdc-c532-494b-996c-8669fbc0e368","name":"Administrator","policies":[{"policy":"5f57ab2d-d3c5-45bf-81c4-c66f2538abe0"}]}]}

policy 本身列表:

curl -s "http://192.168.146.128:8103/policies?fields=id,name&limit=5" -H "Authorization: Bearer $TOKEN"

实测回显:

{"data":[{"id":"5f57ab2d-d3c5-45bf-81c4-c66f2538abe0","name":"Administrator"},{"id":"649260da-449a-49eb-8cf5-0ad87664f0b9","name":"c1-viewer-policy"},{"id":"abf8a154-5b1c-4a46-ac9c-7300570f4f17","name":"$t:public_label"}]}

⚠️ 实测发现:通过 API 直接对角色写 policies 别名字段(PATCH /roles/<id> 带 policies 数组)在本实例返回 403;本批权限实验最终以 directus_access 关联落库完成绑定并生效(见《配置参数详解》)。做权限自动化时建议直接以 /policies + 权限记录为操作对象。

8.3 权限边界实测

无权限的普通账号(viewer@example.com,仅 articles 只读)访问管理端点:

curl -s "http://192.168.146.128:8103/users?fields=id&limit=1" -H "Authorization: Bearer $VTOKEN" -w "\nHTTP %{http_code}\n"

实测回显:

{"errors":[{"message":"You don't have permission to access collection \"directus_users\" or it does not exist. Queried in root.","extensions":{"code":"FORBIDDEN"}}]}

HTTP 403

访问已授权集合正常:

{"data":[{"id":1,"title":"缓存实测第四版"}]}

HTTP 200

九、GraphQL 与 REST 对比

9.1 查询

curl -s -X POST http://192.168.146.128:8103/graphql \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"query":"{ articles(sort: \"id\", limit: 2) { id title status } }"}' \
  -w "\nHTTP %{http_code}\n"

实测回显:

{"data":{"articles":[{"id":"1","title":"缓存实测第四版","status":"draft"},{"id":"2","title":"API 实测文章(已修订)","status":"published"}]}}
HTTP 200

⚠️ 实测发现两点:

  1. sort 参数必须传字符串(sort: "id"),传数组 [id] 会报 "String cannot represent a non string value: id"(GRAPHQL_VALIDATION,400);
  2. GraphQL 返回的 id 是字符串("1"),REST 返回的是整数(1)——双端消费方做类型对接时注意。

9.2 变更(mutation)

curl -s -X POST http://192.168.146.128:8103/graphql \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"query":"mutation { create_articles_item(data: {title: \"GraphQL 创建的条目\", status: \"published\"}) { id title } }"}' \
  -w "\nHTTP %{http_code}\n"

实测回显:

{"data":{"create_articles_item":{"id":"4","title":"GraphQL 创建的条目"}}}
HTTP 200

变更命名规则:create_<集合单数>_item / update_<集合单数>_item / delete_<集合单数>_item。实测该条目随后用 REST DELETE /items/articles/4 删除成功(204),证明 REST 与 GraphQL 写同一份数据。

9.3 权限裁剪 schema(重点差异)

无权限账号查询未授权集合时,GraphQL 不是返回 FORBIDDEN,而是该字段根本不出现在 schema 里:

curl -s -X POST http://192.168.146.128:8103/graphql \
  -H "Authorization: Bearer $VTOKEN" -H 'Content-Type: application/json' \
  -d '{"query":"{ users { id email } }"}' -w "\nHTTP %{http_code}\n"

实测回显:

{"errors":[{"message":"GraphQL validation error.","extensions":{"errors":[{"message":"Cannot query field \"users\" on type \"Query\".","locations":[{"line":1,"column":3}]}],"code":"GRAPHQL_VALIDATION"}}]}
HTTP 400

同一账号查询已授权集合正常:

{"data":{"articles":[{"id":"1","title":"缓存实测第四版"},{"id":"2","title":"API 实测文章(已修订)"},{"id":"4","title":"GraphQL 创建的条目"}]}}
HTTP 200

9.4 REST 与 GraphQL 选型

维度RESTGraphQL
学习成本低,URL + 查询参数中,需学 schema/字段裁剪
取数灵活性fields 参数一级/嵌套强(任意嵌套、别名、一次多资源)
错误语义FORBIDDEN 403schema 级裁剪(无字段可查)
类型细节id 按字段类型(整数)id 统一字符串
缓存/网关URL 级缓存友好(可配 X-Data-Cache 头,见配置篇 8.3)POST 为主,URL 缓存不适用

简单 CRUD/集成优先 REST;前端需要一次取多层关联数据时用 GraphQL。

十、分页/过滤/排序通用约定

REST 查询参数在 /items、/users、/files 等端点通用(/collections 实测对 limit 不生效,见第五章):

参数形态实测示例与结果
fields逗号分隔,支持嵌套 policies.policyfields=id,title 只返回指定列(6.2 实测)
filterURL 编码 JSON,操作符 _eq/_neq/_in/_like/_gte/...{"status":{"_eq":"published"}}(6.2 实测)
search关键词串search=第二篇 命中 title 含关键词的条目(6.2 实测)
sort逗号分隔字段,- 前缀倒序sort=-id(6.2 实测)
limit / offset整数limit=1&offset=1 翻页(6.2 实测)
metafilter_count / result_count 等meta=filter_count 返回 {"filter_count":3}(6.3 实测)
aggregate对象式{"count":"*"} 返回 {"count":"3"}(6.3 实测)

十一、常见问题

11.1 401 与 403 的判定

实测收集的错误码特征:INVALID_CREDENTIALS(401,口令错或 refresh_token 无效)、TOKEN_EXPIRED(401,JWT 超过 15 分钟——实测 viewer 令牌到期后出现 {"errors":[{"message":"Token expired.","extensions":{"code":"TOKEN_EXPIRED"}}]})、INVALID_TOKEN(403,签名校验不过,如 SECRET 轮换后的旧令牌,见配置篇 5.2)、FORBIDDEN(403,无权限或资源不存在)。处理:401 走重新登录或 refresh;403 检查权限或资源是否存在。

11.2 删除的条目查不到却报 403

Directus 对「不存在」与「无权限」统一返回 FORBIDDEN(6.4 实测),这是防探测设计。排障时先确认资源 ID 存在性(列表端点核对),再查权限。

11.3 refresh_token 为什么突然失效

refresh_token 是一次性轮换的(4.4 实测:旧值再刷立即 INVALID_CREDENTIALS)。并发场景两个请求同时刷新,后到者失败——客户端要原子地保存最新 refresh_token。

11.4 aggregate 报 500

?aggregate=count:* 简写在 12.5 实测返回 INTERNAL_SERVER_ERROR,改用对象式 ?aggregate={"count":"*"}(URL 编码后 %7B%22count%22%3A%22*%22%7D)。

11.5 GraphQL 的 sort 报 String cannot represent

sort 传数组会校验失败,必须传字符串(多字段用逗号分隔的字符串,如 "sort": "-date_created,id")。

十二、参考链接