HedgeDoc 1.12.0 API 使用详解
HedgeDoc 1.12.0 HTTP API 全域实测详解:状态、认证、笔记、历史、导出五域端点的 curl 请求与响应回显,429/413/403/404/400/500 错误形态与限流行为实测
HedgeDoc 1.12.0 API 使用详解
目标:系统梳理 HedgeDoc 1.12.0 对外暴露的 HTTP API——服务状态、账号认证、笔记创建与读取、浏览历史、用户数据导出五大域,逐端点给出方法、参数、curl 实测请求与响应回显,并沉淀 400/403/404/409/413/429/500 等错误形态与限流行为的实测结论,帮助读者用脚本安全地批量管理笔记与实例状态。本文全部命令与回显均在 Ubuntu 22.04.4 LTS(x86_64,服务器 IP 192.168.146.128)+ HedgeDoc 1.12.0(PostgreSQL 16 存储)上实测通过,实例部署过程见 《HedgeDoc 1.12.0 Docker Compose 部署指南》。
先读这段(1.x API 的特殊性):HedgeDoc 1.x 与很多平台不同——它没有一份正式发布的公开 API 规范,本文所述端点全部来自 1.12.0 稳定版的实际行为(每个端点均以 curl 实测验证),与官方前端页面使用的接口同源。因此:端点路径不带 /api 前缀,与普通页面共享命名空间;大量端点以 302 重定向 应答(为浏览器表单流设计),脚本调用时需要读 Location 头而不是等 JSON 返回体;错误应答统一是 HTML 错误页而非 JSON 错误体。把这三点记牢,后面所有章节都会顺畅得多。
一、环境信息
本章给出实测环境的关键信息,便于读者对照复现。
- 操作系统:Ubuntu 22.04.4 LTS(x86_64),服务器 IP
192.168.146.128 - 应用版本:HedgeDoc 1.12.0(完整版本串
1.12.0-b9335040716278d029b5872e3825b92821d9cb70,由GET /config实测取得,见 5.1 节) - 数据库:PostgreSQL 16
- 服务访问入口:
http://192.168.146.128:8132(HTTP) - 示例账号:
admin@hedgedoc.local/q1w2e3r4(本文所有登录态操作均使用该账号;另有注册演示账号d5tester@hedgedoc.local/q1w2e3r4,见 4.4 节) - 部署形态:HedgeDoc 1.12.0 + PostgreSQL 16 的官方推荐部署,过程见 《HedgeDoc 1.12.0 Docker Compose 部署指南》;配置项语义见 《HedgeDoc 1.12.0 配置参数详解》
- 客户端工具:系统自带
curl(实测版本8.5.0,curl --version取得)
💡 说明:HedgeDoc 1.x 的笔记内容编辑不走 REST,而走 Socket.IO 实时通道(详见 5.6 节),因此 REST 端点覆盖的是「创建、读取、导出、历史、状态」五类操作。这是与 GitLab、Gitea 等平台 API 最大的心智差异。
二、前置条件
本章确认调用 API 前必须就绪的三件事:服务可达、账号可用、工具就位。
2.1 服务可达性验证
所有 API 调用的前提是服务存活。HedgeDoc 内置了存活探针端点 GET /_health,无需认证:
# 存活探针:返回 JSON,ready 为 true 即服务就绪
curl -s http://192.168.146.128:8132/_health
实测回显:
{"ready":true}
再顺手确认首页 HTTP 状态码为 200:
# 首页可达性
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.146.128:8132/
实测回显:
200
两项都通过,说明服务与数据库连接正常(ready:true 的判定包含数据库可用性,服务启动未就绪时该端点会返回非 200)。
2.2 账号准备
本文示例使用已开通的管理员账号 admin@hedgedoc.local(注册过程与回显见 《HedgeDoc 1.12.0 Docker Compose 部署指南》的账号开通章节)。若你的实例是全新的,可在本文 4.4 节照抄注册端点的实测命令自行开通。
⚠️ 注意:注册与登录共用同一个限流器(10 次 / 5 分钟,见 3.4 节),批量脚本不要在短时间内反复打这两个端点,否则会触发 429。
2.3 客户端工具
本文全部示例只用 curl,两个技巧贯穿全文:
- Cookie 会话保持:登录用
-c cookies.txt把会话 Cookie 写入文件,后续请求用-b cookies.txt携带,这是 HedgeDoc 1.x 唯一的认证通道(见 3.3 节)。 - 重定向观测:HedgeDoc 的大量端点返回 302,本文统一用
-o /dev/null -w 'POST /login -> %{http_code} redirect=%{redirect_url}\n'这类格式串把状态码与跳转地址打印出来(%{http_code}是 curl 的内置变量,读者照抄即可看到与本文一致的结果)。
三、API 总览
本章先建立全局认知:协议风格、端点清单、认证方式、限流约定与通用响应特征,后续各章按域展开。
3.1 协议与风格
HedgeDoc 1.x 的 HTTP 接口有三个鲜明的风格特征,逐条说明:
- 无 API 前缀、与页面共享命名空间:端点形如
/new、/history、/me,没有/api/v1这类前缀。好处是路径直观,代价是不能随便给笔记起与保留路径冲突的别名——服务端内置了保留名清单(robots.txt、favicon.ico、api、build、css、docs、fonts、js、uploads、vendor、views),以这些名字创建别名笔记会被拒绝。 - 面向浏览器表单流的应答形态:创建、登录、注册、历史写入等「写操作」的应答是
302 重定向(Location指向结果页),而不是 JSON 返回体;「读操作」(/status、/me、/:noteId/info、/history等)才是 JSON。 - 读取端点开放 CORS:
/:noteId/info、/:noteId/download的响应头带Access-Control-Allow-Origin: *,允许第三方页面跨域读取笔记元数据与正文(见 5.3.1 的响应头实测)。
3.2 端点速查表
下表汇总 1.12.0 全部可用的 HTTP 端点,按域分组。「认证」列标注调用要求:无 = 完全公开;可选 = 匿名与登录均可调用但行为不同;Cookie = 必须携带登录会话。
| 域 | 方法与端点 | 功能 | 认证 |
|---|---|---|---|
| 服务状态 | GET /_health | 存活探针 | 无 |
| 服务状态 | GET /status | 实时状态统计 | 无 |
| 服务状态 | GET /config | 前端配置快照 | 可选(登录后追加 userToken) |
| 认证 | POST /login | 邮箱密码登录 | 无 |
| 认证 | POST /register | 邮箱注册(默认开启) | 无 |
| 认证 | GET /logout | 退出登录 | Cookie |
| 用户 | GET /me | 当前用户信息 | 可选 |
| 用户 | GET /me/export | 导出本人全部笔记(zip) | Cookie |
| 用户 | GET /user/:username/avatar.svg | 用户头像 | 无 |
| 用户 | GET /me/delete/:token | 账号自助删除页 | Cookie + token |
| 笔记 | POST /new | 以请求体 Markdown 创建笔记 | 可选 |
| 笔记 | GET /new | 创建空笔记 | 可选 |
| 笔记 | POST /new/:noteId | 以自定义别名创建笔记 | Cookie |
| 笔记 | GET /:noteId/info | 笔记元数据 | 可选(受笔记权限) |
| 笔记 | GET /:noteId/download | 下载原始 Markdown | 可选(受笔记权限) |
| 笔记 | GET /:noteId/revision | 修订版本列表 | 可选(受笔记权限) |
| 笔记 | GET /:noteId/revision/:time | 指定修订内容 | 可选(受笔记权限) |
| 笔记 | GET /:noteId/publish | 跳转发布页 | 无 |
| 笔记 | GET /:noteId/slide | 跳转幻灯片页 | 无 |
| 笔记 | GET /:noteId/gist | 转存 GitHub Gist | GitHub OAuth |
| 历史 | GET /history | 浏览历史列表 | Cookie |
| 历史 | POST /history | 整表写入历史 | Cookie |
| 历史 | POST /history/:noteId | 单条置顶/取消置顶 | Cookie |
| 历史 | DELETE /history/:noteId | 删除单条历史 | Cookie + token |
| 历史 | DELETE /history | 清空历史 | Cookie + token |
| 实时 | Socket.IO(/socket.io) | 协同编辑与在线状态通道 | Cookie |
解读:读状态与读笔记两类端点基本公开;写操作中,笔记创建对匿名开放(默认 allowAnonymous=true),而历史域全部要求登录态。表中没有「更新笔记内容」的端点——这不是遗漏,1.x 的内容编辑只走 Socket.IO(5.6 节专述)。
3.3 认证方式
HedgeDoc 1.12.0 的 API 认证只有一种:Cookie 会话。POST /login 成功后服务端下发 connect.sid 会话 Cookie(HttpOnly、SameSite=Lax),后续请求携带即可;服务端不提供 API Token / Bearer Token 机制,Authorization 请求头在 1.x 上没有任何效果。唯一像「令牌」的东西是 deleteToken(GET /config 中以 window.userToken 下发),但它仅用于账号删除与历史清空这两个高危操作(见 5.4.4、5.5 节),不能当通用 API 凭据。
💡 技术拓展:Web 服务的 API 认证方案不止「Cookie 会话」一种,常见方案横向对比——
方案 原理/特点 适用场景 Cookie Session 登录后下发会话 ID,服务端存会话状态;浏览器天然携带 与 Web 页面同源共用的接口 Bearer Token(JWT/静态 Token) 请求头携带令牌,服务端无状态校验 开放给第三方脚本/移动端 OAuth 2.0 委托授权,第三方以有限权限访问 平台开放生态 mTLS / HMAC 签名 证书或签名证明身份 服务间高安全调用 HedgeDoc 1.x 因 API 与前端页面同源共存、且定位「浏览器协作为主」,实际采用 Cookie Session;如果你的自动化场景需要 Token 化调用,要么维持 Cookie 会话(本文做法),要么在网关层自行封装。
3.4 限流约定
1.12.0 内置两类限流,均为「固定窗口计数」,窗口 5 分钟:
| 限流器 | 覆盖端点 | 默认额度 | 环境变量 |
|---|---|---|---|
| 用户端点限流 | POST /login、POST /register | 10 次 / 5 分钟 | CMD_RATE_LIMIT_USER |
| 新建笔记限流 | POST /new、GET /new | 20 次 / 5 分钟 | CMD_RATE_LIMIT_NEW_NOTES |
计数键:已登录用户按用户 ID 计数,匿名请求按来源 IP 计数;额度用尽后返回 429(实测见 6.2 节,第 11 次请求精确触发)。默认值来自官方配置默认项(rateLimitUser: 10、rateLimitNewNotes: 20,实机行为已验证一致);环境变量的完整语义见 《HedgeDoc 1.12.0 配置参数详解》。
💡 技术拓展:限流算法不止「固定窗口」一种,常见实现横向对比——
算法 原理/特点 适用场景 固定窗口计数 每个时间窗重置计数器,实现最简单 低成本防刷(HedgeDoc 采用) 滑动窗口 以当前时刻回望一个窗口统计,边界更平滑 精确限流 令牌桶 恒速发放令牌,允许短时突发 削峰、允许突发流量 漏桶 恒速放行请求,整形流量 严格匀速场景 HedgeDoc 1.12.0 使用的 express-rate-limit 中间件实际采用固定窗口,因此窗口边界处可能出现「短时间内 2 倍额度」的突发,写自动化脚本时应按额度打 7 折预留余量。
3.5 通用响应特征
以 GET /:noteId/info 的响应头为例(实测节选,已省略与 API 无关的 CSP 等安全头):
# 观察读取端点的响应头
curl -sI http://192.168.146.128:8132/CdL5SNDGTuarQ9NexbQdYQ/info \
| grep -iE '^HTTP/|hedgedoc|access-control|content-type|x-robots|cache-control'
实测回显:
HTTP/1.1 200 OK
HedgeDoc-Version: 1.12.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Range
Access-Control-Expose-Headers: Cache-Control, Content-Encoding, Content-Range
Cache-Control: private
X-Robots-Tag: noindex, nofollow
Content-Type: application/json; charset=utf-8
三点值得注意:其一,每个响应都带 HedgeDoc-Version 头,脚本可以用它做版本探测而不用解析页面;其二,Access-Control-Allow-Origin: * 说明读取端点天然支持跨域脚本调用;其三,X-Robots-Tag: noindex, nofollow 说明官方明确不希望这些元数据端点被搜索引擎收录。错误应答则统一是 HTML 错误页(错误码体系见第六章)。
四、认证与首次调用
本章用 curl 走通「登录 → 验证会话 → 识别未认证形态 → 登出」的完整闭环,这是所有需要登录态的端点(历史域、别名创建、数据导出)的前置动作。
4.1 登录获取会话
POST /login 接收 application/x-www-form-urlencoded 表单(字段 email、password),成功后返回 302 并通过 Set-Cookie 下发会话:
# 登录:-c 把会话 Cookie 保存到 cookies.txt
curl -s -o /dev/null -w 'POST /login -> %{http_code} redirect=%{redirect_url}\n' \
-c cookies.txt \
-d 'email=admin@hedgedoc.local' -d 'password=q1w2e3r4' \
http://192.168.146.128:8132/login
实测回显:
POST /login -> 302 redirect=http://192.168.146.128/
⚠️ 注意:实测发现跳转地址是
http://192.168.146.128/(不带 8132 端口)。这是服务端serverURL拼接行为——默认urlAddPort=false时不把非标端口拼进绝对地址。脚本不要直接跟随这个 Location(会连到 80 端口),应忽略跳转只取 Cookie;生产部署时用CMD_DOMAIN配置规范域名即可规避(详见 8.1 节与 《HedgeDoc 1.12.0 配置参数详解》)。
4.2 验证会话:GET /me
带着上一步保存的 Cookie 调用 GET /me,返回当前用户的 JSON 档案:
# 携带会话 Cookie 查询当前用户
curl -s -b cookies.txt http://192.168.146.128:8132/me
实测回显:
{"status":"ok","id":"30a12425-8adb-49cb-b3c7-bddc8d508ad5","name":"admin","photo":"https://cdn.libravatar.org/avatar/1a43fa92ea14849c2273769e6c21db66?default=identicon&s=96"}
字段说明:status 为 ok 表示会话有效;id 是用户内部 UUID;name 是展示名;photo 是头像 URL(默认走 libravatar 服务)。脚本可用此端点做登录态探活。
4.3 未认证形态
未带 Cookie 时的两种典型形态要区分清楚——/me 不报错而是返回特殊 JSON,/history 则直接重定向回首页:
# 未认证调用 GET /me
curl -s http://192.168.146.128:8132/me
实测回显:
{"status":"forbidden"}
# 未认证调用 GET /history
curl -s -o /dev/null -w 'GET /history(未认证)-> %{http_code} redirect=%{redirect_url}\n' \
http://192.168.146.128:8132/history
实测回显:
GET /history(未认证)-> 302 redirect=http://192.168.146.128/
解读:/me 用 {"status":"forbidden"} + HTTP 200 表达「未登录」,脚本判断登录态要检查 JSON 字段而不是状态码;/history 则把匿名请求 302 回首页。两者形态不同是 1.x 接口「页面化」风格的又一例证。
4.4 登出与注册
GET /logout 销毁会话并重定向首页;POST /register 开通新账号(默认 allowEmailRegister=true):
# 登出:销毁当前会话
curl -s -o /dev/null -w 'GET /logout -> %{http_code} redirect=%{redirect_url}\n' \
-b cookies.txt http://192.168.146.128:8132/logout
实测回显:
GET /logout -> 302 redirect=http://192.168.146.128/
# 注册新账号 d5tester(本文注册域的独立实测)
curl -s -o /dev/null -w 'POST /register -> %{http_code} redirect=%{redirect_url}\n' \
-d 'email=d5tester@hedgedoc.local' -d 'password=q1w2e3r4' \
http://192.168.146.128:8132/register
实测回显:
POST /register -> 302 redirect=http://192.168.146.128/
注册成功的旁证——GET /status 的注册用户计数从 2 变 3(首次实测值见 5.1.2):
# 查看注册用户计数
curl -s http://192.168.146.128:8132/status | grep -o '"registeredUsers":[0-9]*'
实测回显:
"registeredUsers":3
⚠️ 注意:注册端点对重复邮箱不会报 4xx,而是 302 回首页并在页面闪现失败提示,脚本判断注册成败应随后调用
GET /status或POST /login验证,不要只看状态码。
五、各 API 域详解
本章按五个域逐端点展开:每个端点给出方法、参数、实测请求与响应,并在域末小结错误形态。
5.1 服务状态域
服务状态域的三个端点全部无需认证,是监控探针与容量巡检的数据源。
5.1.1 存活探针:GET /_health
最小化的存活判定端点(已在 2.1 节实测):
curl -s http://192.168.146.128:8132/_health
实测回显:
{"ready":true}
服务启动未完成或数据库失联时 ready 会为 false 且状态码非 200,可直接对接负载均衡健康检查。
5.1.2 实时状态:GET /status
返回实例的实时统计快照,无需认证:
curl -s http://192.168.146.128:8132/status
实测回显(首次采集时实例内有 5 篇笔记、2 个注册用户):
{"onlineNotes":0,"onlineUsers":0,"distinctOnlineUsers":0,"notesCount":5,"registeredUsers":2,"onlineRegisteredUsers":0,"distinctOnlineRegisteredUsers":0,"isConnectionBusy":false,"connectionSocketQueueLength":0,"isDisconnectBusy":false,"disconnectSocketQueueLength":0}
字段逐项说明:
| 字段 | 含义 | 用途 |
|---|---|---|
| onlineNotes | 当前打开中的笔记数 | 实时协作热度 |
| onlineUsers / distinctOnlineUsers | 在线用户数 / 去重在线数 | 并发监控 |
| notesCount | 笔记总数 | 容量巡检 |
| registeredUsers | 注册用户总数 | 增长统计 |
| onlineRegisteredUsers / distinctOnlineRegisteredUsers | 在线注册用户数 / 去重值 | 登录用户活跃度 |
| isConnectionBusy | Socket.IO 连接队列是否拥塞 | 过载告警 |
| connectionSocketQueueLength / disconnectSocketQueueLength | 连接/断开队列长度 | 过载细节 |
解读:isConnectionBusy 与两个队列长度是 1.x 实时通道的压力指标,长时间非零说明在线编辑负载逼近瓶颈。
5.1.3 配置快照:GET /config
返回服务端下发给前端的配置快照(JavaScript 变量形态)。未认证时:
curl -s http://192.168.146.128:8132/config
实测回显(输出节选,保留全部关键行):
window.domain = '192.168.146.128'
window.urlpath = ''
window.debug = false
window.version = '1.12.0-b9335040716278d029b5872e3825b92821d9cb70'
window.enableUploads = 'all'
window.allowedUploadMimeTypes = ["image/jpeg","image/png","image/gif","image/svg+xml","image/webp"]
window.linkifyHeaderStyle = 'keep-case'
window.externalLinkWarning = true
window.externalLinkWhitelist = []
window.DROPBOX_APP_KEY = ''
window.cookiePolicy = 'lax'
window.userToken = ''
携带登录 Cookie 再取一次,window.userToken 会从空串变为当前账号的 deleteToken:
curl -s -b cookies.txt http://192.168.146.128:8132/config | grep -E 'userToken|version'
实测回显:
window.version = '1.12.0-b9335040716278d029b5872e3825b92821d9cb70'
window.userToken = '04ccd1ab-dea2-40da-93b8-be758c538f7d'
⚠️ 注意:
userToken(即 deleteToken)等同账号级高危凭据——它可清空账号全部历史、直达账号删除页(5.4.4 与 5.5 节)。本文展示的是将于实验结束后销毁的演示实例值,生产环境的 deleteToken 必须按凭据管理,不得外泄。该值也是GET /me/delete/:token、DELETE /history的必带参数。
解读:/config 是脚本感知实例配置的最佳入口——版本号、上传开关、上传类型白名单、cookie 策略都能一次取到,无需登录即可完成实例指纹识别。
5.2 笔记创建域
笔记创建域围绕 /new 展开,共三种创建方式;这是整个 API 里对自动化最有价值的域。
5.2.1 正文创建:POST /new
以请求体 Markdown 创建笔记。请求头必须是 text/markdown 或 text/plain(服务端按这两种类型解析正文),成功后 302 跳转到新笔记。先准备演示内容文件 demo-note.md:
# D5 API 实测演示笔记
本笔记通过 `POST /new`(Content-Type: text/markdown)创建,用于[《HedgeDoc 1.12.0 API 使用详解》](https://www.quzuo.net/document/9uNvKPWSuycG)端点实测。
## 验证要点
- `GET /:noteId/info` 返回元数据 JSON
- `GET /:noteId/download` 返回原始 Markdown
- `GET /:noteId/revision` 返回修订列表
携带登录态创建:
curl -s -o /dev/null -w 'POST /new -> %{http_code} redirect=%{redirect_url}\n' \
-b cookies.txt \
-H 'Content-Type: text/markdown' --data-binary @demo-note.md \
http://192.168.146.128:8132/new
实测回显:
POST /new -> 302 redirect=http://192.168.146.128/CdL5SNDGTuarQ9NexbQdYQ
Location 路径末段 CdL5SNDGTuarQ9NexbQdYQ 就是新笔记的 noteId(22 位对外标识,后续所有读取端点都用它)。注意 --data-binary 而不是 -d——后者会按表单编码处理换行,破坏 Markdown 格式。
⚠️ 注意:与 4.1 节相同,
Location里没有 8132 端口(urlAddPort=false的默认行为),脚本要从redirect_url里截取路径部分作为 noteId,不要直接访问跳转地址。
匿名创建同样可行(默认 allowAnonymous=true):
# 不带任何 Cookie 的匿名创建
curl -s -o /dev/null -w 'POST /new(匿名,无 cookie)-> %{http_code} redirect=%{redirect_url}\n' \
-H 'Content-Type: text/markdown' --data-binary '# 匿名创建测试' \
http://192.168.146.128:8132/new
实测回显:
POST /new(匿名,无 cookie)-> 302 redirect=http://192.168.146.128/oiKn8iICRFKOXpiuxPG6hQ
匿名创建的笔记归匿名所有(不归属任何账号),后续只能凭 noteId 访问。生产实例若要关闭此行为,设 CMD_ALLOW_ANONYMOUS=false。
5.2.2 空笔记创建:GET /new
GET /new 也能创建笔记——创建一篇空笔记后跳转过去(适合「浏览器开个新标签就开始写」的场景):
curl -s -o /dev/null -w 'GET /new(匿名,无正文)-> %{http_code} redirect=%{redirect_url}\n' \
http://192.168.146.128:8132/new
实测回显:
GET /new(匿名,无正文)-> 302 redirect=http://192.168.146.128/dGHFjrztTaiBcii2NKT9SQ
⚠️ 注意:爬虫或监控误扫
GET /new会持续产生垃圾空笔记(该端点同样受 20 次/5 分钟限流保护)。生产实例建议配合CMD_ALLOW_FREE_URL与反爬策略治理,或定期巡检清理。
5.2.3 自定义别名创建:POST /new/:noteId
在 /new 后附加路径段即可指定别名(如 /new/my-meeting-notes),创建后可用别名代替 noteId 访问。但该能力默认关闭(allowFreeURL=false),实测默认形态:
curl -s -o /dev/null -w 'POST /new/my-note-alias -> %{http_code}\n' \
-b cookies.txt \
-H 'Content-Type: text/markdown' --data-binary '# 自定义别名' \
http://192.168.146.128:8132/new/my-note-alias
实测回显:
POST /new/my-note-alias -> 403
错误页标题(实测):
<title>403 Forbidden oh no.</title>
解读:默认配置下 POST 形态的自定义别名创建一律 403(requireFreeURLAuthentication=false 时未开 allowFreeURL 直接拒绝)。开启 CMD_ALLOW_FREE_URL=true 后,登录用户可创建别名笔记;若别名与既有笔记冲突,服务端返回 409 Conflict(错误文案 This note already exists.,经服务端源码确认的分支行为——本文实例未开启自由别名,未实测到 409 形态,读者开启后自测)。
5.2.4 超长正文限制:413 实测
POST /new 对正文长度有硬限制:默认 documentMaxLength=100000 字节(环境变量 CMD_DOCUMENT_MAX_LENGTH)。用一份 100214 字节的超限文件实测:
# oversize.md 为 100214 字节(wc -c 实测),超过 100000 上限
curl -s -o /dev/null -w 'POST /new(oversize.md 100214 字节)-> %{http_code}\n' \
-b cookies.txt \
-H 'Content-Type: text/markdown' --data-binary @oversize.md \
http://192.168.146.128:8132/new
实测回显:
POST /new(oversize.md 100214 字节)-> 413
错误页标题(实测):
<title>413 Payload Too Large Shorten your note!</title>
解读:这一层 413 是应用层的「笔记太长」判定(文案 Shorten your note!)。另有一层更底部的保护——请求体超过 10MB 时由 HTTP 层直接拒绝(同样 413 但形态不同),两层限制的语义见 8.2 节 FAQ。
5.2.5 创建域错误码小结
| 状态码 | 触发条件(实测/确认) |
|---|---|
| 302 | 创建成功,Location 指向新笔记(不带端口,见 8.1) |
| 403 | 自定义别名未开 allowFreeURL(实测);实例关闭匿名后匿名创建(配置确认) |
| 409 | 别名与既有笔记冲突(需先开 allowFreeURL,源码确认) |
| 413 | 正文超 documentMaxLength(实测 100214 字节触发) |
| 429 | 5 分钟窗口内创建超 20 次(额度确认,突发实测见 6.2 同类形态) |
5.3 笔记读取域
读取域覆盖元数据、原始正文与修订历史,全部支持匿名访问(受笔记权限约束),且带 Access-Control-Allow-Origin: *(3.5 节)。以下统一用 5.2.1 创建的笔记 CdL5SNDGTuarQ9NexbQdYQ 实测。
5.3.1 笔记元数据:GET /:noteId/info
curl -s http://192.168.146.128:8132/CdL5SNDGTuarQ9NexbQdYQ/info
实测回显:
{"title":"D5 API 实测演示笔记","description":"# D5 API 实测演示笔记 本笔记通过 `POST /new`(Content-Type: text/markdown)创建,用于[《HedgeDoc 1.12.0 API 使用详解》](https://www.quzuo.net/document/9uNvKPWSuycG)端点实测。 ","viewcount":0,"createtime":"2026-10-22T02:08:14.243Z","updatetime":null}
字段说明:
| 字段 | 含义 | 说明 |
|---|---|---|
| title | 笔记标题 | 取 front-matter 的 title,否则取首行 |
| description | 笔记描述 | 取 front-matter 的 description,否则由正文自动生成 |
| viewcount | 浏览计数 | 通过笔记页访问时递增,info/download 不计数 |
| createtime | 创建时间(ISO 8601) | 服务端 UTC 时间 |
| updatetime | 最后修改时间 | 新建笔记为 null,编辑发生后为时间戳 |
响应头(实测节选,含版本与 CORS 头)见 3.5 节,此处不再重复。
5.3.2 原始正文下载:GET /:noteId/download
curl -s http://192.168.146.128:8132/CdL5SNDGTuarQ9NexbQdYQ/download
实测回显(与创建时提交的 demo-note.md 逐字一致):
# D5 API 实测演示笔记
本笔记通过 `POST /new`(Content-Type: text/markdown)创建,用于[《HedgeDoc 1.12.0 API 使用详解》](https://www.quzuo.net/document/9uNvKPWSuycG)端点实测。
## 验证要点
- `GET /:noteId/info` 返回元数据 JSON
- `GET /:noteId/download` 返回原始 Markdown
- `GET /:noteId/revision` 返回修订列表
该端点响应带 Content-disposition: attachment,是笔记内容备份的最简通道——配合脚本遍历 noteId 批量 download,即可实现轻量级内容备份(备份策略的完整方案见 《HedgeDoc 1.12.0 Docker Compose 部署指南》的备份章节)。
5.3.3 修订列表:GET /:noteId/revision
curl -s -b cookies.txt http://192.168.146.128:8132/CdL5SNDGTuarQ9NexbQdYQ/revision
实测回显:
{"revision":[{"time":1792634894247,"length":216}]}
返回修订数组,每项含修订时间戳(毫秒)与正文长度。新建笔记只有 1 个修订;time 值即为下一个端点的版本定位参数。
5.3.4 指定修订内容:GET /:noteId/revision/:time
把 5.3.3 取到的 time 值拼入路径,取该版本的完整内容(含增量 patch 与作者署名信息):
curl -s http://192.168.146.128:8132/CdL5SNDGTuarQ9NexbQdYQ/revision/1792634894247
实测回显(正文含转义换行,JSON 内一整行):
{"content":"# D5 API 实测演示笔记\n\n本笔记通过 `POST /new`(Content-Type: text/markdown)创建,用于[《HedgeDoc 1.12.0 API 使用详解》](https://www.quzuo.net/document/9uNvKPWSuycG)端点实测。\n\n## 验证要点\n\n- `GET /:noteId/info` 返回元数据 JSON\n- `GET /:noteId/download` 返回原始 Markdown\n- `GET /:noteId/revision` 返回修订列表\n","patch":[],"authorship":[]}
解读:content 是该版本全文,patch 是与前一版本的差异补丁(首版为空数组),authorship 是逐字符作者署名(单人首版为空)。修订数据是审计「谁在什么时候改了什么」的底层依据。
5.3.5 发布页与幻灯片跳转:GET /:noteId/publish、GET /:noteId/slide
两个端点把笔记跳转到对应的公开视图(/s/<shortid> 与 /p/<shortid>),属页面级端点,脚本场景用处有限,列出备查:
curl -s -o /dev/null -w 'GET /:noteId/publish -> %{http_code} redirect=%{redirect_url}\n' \
http://192.168.146.128:8132/CdL5SNDGTuarQ9NexbQdYQ/publish
实测回显:
GET /:noteId/publish -> 302 redirect=http://192.168.146.128/s/73cnnf2poV
💡 说明:
publish与pretty等价(pretty是遗留别名);slide跳转幻灯片视图。仅当笔记设置了「发布」权限时对外可见。
5.3.6 转存 Gist:GET /:noteId/gist
该端点把笔记转存为 GitHub Gist,依赖 GitHub OAuth 集成(CMD_GITHUB_CLIENTID 等)。未配置 GitHub 集成的实例上,它会发起 GitHub OAuth 授权跳转(浏览器场景),无法在纯 curl 流程中完成,本文实例未配置 GitHub 集成、不展开实测。生产上建议改用 5.3.2 的 download 端点自行推送 Gist。
5.3.7 读取域错误形态:404
访问不存在的 noteId,返回 404 HTML 错误页:
curl -s -o /dev/null -w 'GET /notexist0000000000000000/info -> %{http_code}\n' \
http://192.168.146.128:8132/notexist0000000000000000/info
实测回显:
GET /notexist0000000000000000/info -> 404
错误页标题(实测):
<title>404 Not Found oops.</title>
5.4 浏览历史域
历史域记录「当前用户打开过哪些笔记」,四个端点全部要求登录态(匿名访问的 302 行为见 4.3 节)。历史数据按用户存储于数据库,是个人工作台「历史记录」面板的数据源。
5.4.1 历史列表:GET /history
curl -s -b cookies.txt -w '\nHTTP %{http_code}\n' http://192.168.146.128:8132/history
实测回显(初始为空列表):
{"history":[]}
HTTP 200
5.4.2 整表写入:POST /history
以表单字段 history 提交 JSON 数组,整表替换当前用户的历史记录。这是前端「历史面板同步」的通道,也可用于脚本注入结构化历史:
curl -s -o /dev/null -w 'POST /history(整表写入)-> %{http_code}\n' \
-b cookies.txt \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'history=[{"id":"CdL5SNDGTuarQ9NexbQdYQ","text":"D5 API 实测演示笔记","time":1792634894247}]' \
http://192.168.146.128:8132/history
实测回显:
POST /history(整表写入)-> 200
复核写入结果:
curl -s -b cookies.txt http://192.168.146.128:8132/history
实测回显:
{"history":[{"id":"CdL5SNDGTuarQ9NexbQdYQ","text":"D5 API 实测演示笔记","time":1792634894247}]}
⚠️ 注意:这是整表替换语义——提交的数组会成为全部历史,原有条目会丢失。
text建议填笔记标题(历史面板展示用),time用毫秒时间戳。
5.4.3 单条置顶:POST /history/:noteId
对已在历史中的条目做置顶/取消置顶。要求表单字段 pinned 取 true/false 字符串:
curl -s -o /dev/null -w 'POST /history/CdL5SNDGTuarQ9NexbQdYQ(带表单,置顶)-> %{http_code}\n' \
-b cookies.txt \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'pinned=true' \
http://192.168.146.128:8132/history/CdL5SNDGTuarQ9NexbQdYQ
实测回显:
POST /history/CdL5SNDGTuarQ9NexbQdYQ(带表单,置顶)-> 200
复核置顶状态:
curl -s -b cookies.txt http://192.168.146.128:8132/history
实测回显:
{"history":[{"id":"CdL5SNDGTuarQ9NexbQdYQ","text":"D5 API 实测演示笔记","time":1792634894247,"pinned":true}]}
该端点有三种失败形态,全部实测到位(是本文实测中最典型的「页面化接口陷阱」,排障分析见 8.2 节):
# 形态一:缺 pinned 字段(表单合法但字段缺失)
curl -s -o /dev/null -w 'POST /history/CdL5SNDGTuarQ9NexbQdYQ(缺 pinned)-> %{http_code}\n' \
-b cookies.txt -H 'Content-Type: application/x-www-form-urlencoded' -d 'foo=bar' \
http://192.168.146.128:8132/history/CdL5SNDGTuarQ9NexbQdYQ
实测回显:
POST /history/CdL5SNDGTuarQ9NexbQdYQ(缺 pinned)-> 400
# 形态二:笔记不在历史中(置顶不存在的条目)
curl -s -o /dev/null -w 'POST /history/oiKn8iICRFKOXpiuxPG6hQ(不在历史中)-> %{http_code}\n' \
-b cookies.txt -H 'Content-Type: application/x-www-form-urlencoded' -d 'pinned=true' \
http://192.168.146.128:8132/history/oiKn8iICRFKOXpiuxPG6hQ
实测回显:
POST /history/oiKn8iICRFKOXpiuxPG6hQ(不在历史中)-> 404
# 形态三:不带表单 Content-Type(服务端解析不到请求体,未捕获异常)
curl -s -o /dev/null -w 'POST /history/CdL5SNDGTuarQ9NexbQdYQ(无 Content-Type)-> %{http_code}\n' \
-b cookies.txt -X POST \
http://192.168.146.128:8132/history/CdL5SNDGTuarQ9NexbQdYQ
实测回显:
POST /history/CdL5SNDGTuarQ9NexbQdYQ(无 Content-Type)-> 500
此时错误页是最朴素的未捕获异常页(实测标题 <title>Error</title>),与 400/404 的标准错误页形态不同。
5.4.4 删除历史:DELETE /history/:noteId 与 DELETE /history
删除单条历史必须带 token 参数(取自 5.1.3 的 window.userToken,即 deleteToken)。先实测不带 token 的拒绝形态:
curl -s -o /dev/null -w 'DELETE /history/CdL5SNDGTuarQ9NexbQdYQ(无 token)-> %{http_code}\n' \
-b cookies.txt -X DELETE \
http://192.168.146.128:8132/history/CdL5SNDGTuarQ9NexbQdYQ
实测回显:
DELETE /history/CdL5SNDGTuarQ9NexbQdYQ(无 token)-> 403
再带 token 删除(<deleteToken> 用 5.1.3 实测取得的值代入):
curl -s -o /dev/null -w 'DELETE /history/CdL5SNDGTuarQ9NexbQdYQ?token=<deleteToken> -> %{http_code}\n' \
-b cookies.txt -X DELETE \
'http://192.168.146.128:8132/history/CdL5SNDGTuarQ9NexbQdYQ?token=04ccd1ab-dea2-40da-93b8-be758c538f7d'
实测回显:
DELETE /history/CdL5SNDGTuarQ9NexbQdYQ?token=<deleteToken> -> 200
复核删除结果:
curl -s -b cookies.txt http://192.168.146.128:8132/history
实测回显:
{"history":[]}
不带 :noteId 的 DELETE /history(同样要求 ?token=)一次性清空全部历史,本文不做清空实测以免破坏演示数据,行为语义经服务端源码确认。
⚠️ 注意:deleteToken 是账号删除与历史清空共用的凭据,出现在 URL 查询串里会被访问日志留存,生产环境调用后应轮转日志或避免在共享终端执行。
5.5 用户数据域
用户数据域覆盖头像、全量数据导出与账号自助删除。
5.5.1 用户头像:GET /user/:username/avatar.svg
curl -s -o /dev/null -w 'GET /user/admin/avatar.svg -> %{http_code} type=%{content_type}\n' \
http://192.168.146.128:8132/user/admin/avatar.svg
实测回显:
GET /user/admin/avatar.svg -> 200 type=image/svg+xml; charset=utf-8
服务端动态生成 SVG 头像(默认按 libravatar 外链生成兜底图),可直接嵌入第三方页面。
5.5.2 全量导出:GET /me/export
把当前用户名下全部笔记打包为 zip 下载:
curl -s -b cookies.txt http://192.168.146.128:8132/me/export -o me-export.zip
unzip -l me-export.zip
实测回显:
Archive: me-export.zip
Length Date Time Name
--------- ---------- ----- ----
322 2026-10-22 02:12 API 使用演示笔记.md
322 2026-10-22 02:12 API 使用演示笔记-1.md
312 2026-10-22 02:12 D5 API 实测演示笔记.md
--------- -------
956 3 files
解读:zip 内是每篇笔记一个 .md 文件(重名自动加 -1 后缀),这正是用户级内容备份的最快通道。配合 GET /status 的 notesCount 巡检,可覆盖「实例级 zip 备份 + 用户级 zip 导出 + 单篇 download」的三层备份体系。
5.5.3 账号自助删除:GET /me/delete/:token
带 deleteToken 访问该路径进入账号删除确认页(页面交互确认后删除账号)。这是危险操作端点,本文不做删除实测,仅给出调用形态供运维知晓——账号删除属低频管理动作,建议走数据库层面的规范处理并提前完成 5.5.2 的全量导出。
5.6 内容更新通道:Socket.IO
1.12.0 没有「更新笔记内容」的 REST 端点。 这是使用 API 时最重要的认知:内容编辑(含 collaborative editing、版本落盘)全部通过 Socket.IO 实时通道完成,前端编辑器就是该通道的客户端。两个可直接验证的证据:
其一,服务端笔记路由中不存在 PUT/POST /:noteId 形态的内容更新端点(开源仓库 lib/web/note/router.js 可查,本文实测的 POST /:noteId 请求实际会 404);
其二,Socket.IO 通道实机握手正常:
# Socket.IO 引擎握手(engine.io 协议,返回会话元数据)
curl -s 'http://192.168.146.128:8132/socket.io/?EIO=4&transport=polling'
实测回显:
0{"sid":"aAm9Oy02zTVeb0eIAAAA","upgrades":["websocket"],"pingInterval":5000,"pingTimeout":10000,"maxPayload":1000000}
给自动化场景的落地方案:需要「更新内容」时,走「POST /new 创建新版本笔记 + GET /:noteId/download 校验 + 应用层切换引用」的重建式更新;或在脚本中引入 Socket.IO 客户端库模拟编辑器会话(需要处理握手、认证与房间加入协议,复杂度高,不推荐轻量使用)。单篇内容的读取校验始终用 GET /:noteId/download。
六、错误码速查与限流实测
本章把散落在各域的错误形态汇总成速查表,并给出限流 429 的完整实测过程——这是自动化脚本对接前必须预演的两种失败。
6.1 错误码总表
1.12.0 全部错误码与实测/确认形态如下。除 500 的未捕获异常形态外,错误应答统一是 HTML 错误页,标题格式为 <code> <短语> <文案>:
| 状态码 | 短语 | 错误页标题(实测) | 典型触发场景 |
|---|---|---|---|
| 400 | Bad Request | 400 Bad Request something not right. | /login 缺字段(实测);POST /history/:noteId 缺 pinned(实测) |
| 403 | Forbidden | 403 Forbidden oh no. | 无权限访问;POST /new/:noteId 未开 allowFreeURL(实测);DELETE /history 缺 token(实测) |
| 404 | Not Found | 404 Not Found oops. | 笔记不存在(实测);历史条目不存在(实测) |
| 409 | Conflict | 409 Conflict This note already exists. | 别名冲突(需开 allowFreeURL,源码确认) |
| 413 | Payload Too Large | 413 Payload Too Large Shorten your note! | 正文超 documentMaxLength=100000(实测 100214 字节触发) |
| 429 | Too Many Requests | 429 Too Many Requests Try again later. | 登录/注册超 10 次/5 分钟(实测第 11 次触发);创建超 20 次/5 分钟(配置确认) |
| 500 | Internal Error | Error(未捕获异常形态,实测)或 500 Internal Error wtf. | 服务端异常;POST /history/:noteId 缺表单 Content-Type(实测 Error 形态) |
| 503 | Service Unavailable | I'm busy right now, try again later.(纯文本,源码确认) | tooBusy 过载保护触发 |
解读:脚本侧的错误处理建议按「4xx 解析 HTML 标题取原因、5xx 直接重试告警」的策略落地;匿名与登录态的 403 行为不同——匿名请求部分端点(如 /history)返回的是 302 重定向而非 403(见 4.3 节),判断权限失败要同时覆盖两种形态。
6.2 登录限流 429 实测
限流器为固定窗口:POST /login 与 POST /register 共享 10 次/5 分钟额度(默认 rateLimitUser=10)。实测过程:在 5 分钟窗口内,先用正确凭据成功登录 1 次、缺字段请求 2 次(400 也计数),随后连续发起 9 次密码错误的登录请求(第 4~12 次窗口内请求),单次请求命令:
curl -s -o /dev/null -w '第 N 次 -> %{http_code}\n' \
-d 'email=admin@hedgedoc.local' -d 'password=wrong-pass' \
http://192.168.146.128:8132/login
实测回显(第 4~12 次):
第 4 次 -> 302
第 5 次 -> 302
第 6 次 -> 302
第 7 次 -> 302
第 8 次 -> 302
第 9 次 -> 302
第 10 次 -> 302
第 11 次 -> 429
第 12 次 -> 429
第 11 次精确触发 429(窗口内累计第 10 次之后的首次请求),与默认额度 10 次/5 分钟完全一致。429 的错误页形态:
<title>429 Too Many Requests Try again later.</title>
解读:三个对脚本实用的推论——其一,失败的登录请求同样计数(密码错误也烧额度),暴力重试脚本会迅速自我封禁;其二,窗口 5 分钟后自动恢复,无需服务端干预;其三,POST /new 的 20 次/5 分钟限流同理(环境变量 CMD_RATE_LIMIT_NEW_NOTES 可调),批量建笔记脚本按额度 7 折限速即可长期安全运行。
6.3 超长正文 413 与两层限制
5.2.4 已给出应用层 413 的完整实测(100214 字节 > 100000 上限)。补充第二层的边界说明:请求体解析器对 text/markdown 类型设有 10MB 的解析上限,超过时由 HTTP 层直接拒绝(同为 413 但错误形态来自请求体解析器)。对绝大多数场景,先碰到的都是 100000 字节这层——即默认单篇笔记约 10 万字符上限,可按 CMD_DOCUMENT_MAX_LENGTH 调整(配置语义见 《HedgeDoc 1.12.0 配置参数详解》)。
七、分页、过滤与排序通用约定
本章说明 1.x API 在「列表类数据」上的通用行为——结论很简单,但必须提前知道,避免在错误预期上浪费脚本设计时间。
1.x 的列表端点没有分页、过滤与排序参数。 逐端点确认:
| 端点 | 返回形态 | 分页/排序行为 |
|---|---|---|
| GET /history | 全量数组 | 按写入顺序返回(历史面板以 time 展示排序) |
| GET /:noteId/revision | 全量数组 | 按修订时间升序 |
| GET /status、GET /config | 全量对象 | 不适用 |
1.12.0 的历史与修订接口都是「全量返回」语义:参数表里没有 page/limit/sort 之类的查询参数(实测与源码结构一致),过滤逻辑需在客户端完成。对自动化场景的三点建议:
- 历史域:历史列表通常很短(个人浏览记录),全量拉取后按
pinned/time自行过滤即可; - 修订域:长生命周期笔记的修订数会持续增长,定期巡检修订列表的
length字段(数组元素数)作为数据膨胀指标; - 实例级清单:1.x 没有公开的「全实例笔记列表」端点,批量遍历需自行维护 noteId 台账(
GET /me/export可作为当前账号名下笔记的清单来源)。
💡 说明:HedgeDoc 2.x 重构了 API 层(规范化的
/api/v2资源式接口),但截至本文撰写时 1.12.0 仍是稳定主线,本文口径以 1.12.0 实测为准。
八、常见问题
本章沉淀实测过程中遇到的典型问题与排障结论。
Q1:为什么所有 302 的 Location 都不带 8132 端口?会不会影响脚本?
serverURL 的拼接默认 urlAddPort=false,只在 80/443 标准端口下省略端口的设计被「自定义端口 + 默认配置」的组合踩中:Location 变成 http://192.168.146.128/<noteId>。影响与对策:脚本不要跟随 Location 访问(80 端口并无服务),而是截取路径部分使用;生产部署用 CMD_DOMAIN 配置规范对外域名(标准 443 端口下无此问题)。详见 《HedgeDoc 1.12.0 配置参数详解》的 domain/urlAddPort 参数节。
Q2:POST /history/:noteId 报 500/400/404 分别是什么原因?
三种形态本文全部实测复现,根因各不相同:
| 形态 | 状态码 | 根因 | 解法 |
|---|---|---|---|
| 不带表单 Content-Type | 500 | 请求体解析器未运行,服务端读取字段时未捕获异常,返回朴素 Error 页 | 补 Content-Type: application/x-www-form-urlencoded |
| 表单合法但缺 pinned 字段 | 400 | 服务端显式校验 pinned 必填 | 补 -d 'pinned=true' |
| 笔记不在历史中 | 404 | 置顶操作只作用于既有历史条目 | 先通过 5.4.2 整表写入或访问笔记页生成历史 |
Q3:为什么没有「更新笔记内容」的 REST 接口?
1.x 架构里内容编辑是实时协同行为,统一走 Socket.IO 通道(5.6 节),REST 只承担创建与读取。这不是接口缺失而是架构设计;自动化更新走「重建式更新」(新建版本笔记)或 Socket.IO 客户端,见 5.6 节的落地建议。
Q4:自定义别名笔记(POST /new/:noteId)为什么 403?怎么开?
默认 allowFreeURL=false,服务端显式拒绝(实测 403,403 Forbidden oh no.)。需要自定义别名时设 CMD_ALLOW_FREE_URL=true 并重启服务生效;开启后注意保留名清单(api、uploads 等)不可用作别名,别名冲突会返回 409。开启方式与生效验证见 《HedgeDoc 1.12.0 Docker Compose 部署指南》的环境变量章节。
Q5:DELETE /history 返回 403,token 从哪里来?
DELETE /history(含单条删除)要求 ?token= 与账号的 deleteToken 匹配,该值通过登录态调用 GET /config 从 window.userToken 取得(5.1.3 实测)。两步流程:登录 → 取 config 提取 userToken → 拼 URL 删除。deleteToken 同时是账号删除页的凭据,按 5.4.4 的警示管理。
Q6:触发 429 后多久恢复?会影响哪些端点?
固定窗口 5 分钟后自动恢复,无需重启。登录限流只作用于 POST /login 与 POST /register(两者共享额度),不影响笔记读取等其他端点;创建限流同理只作用于 /new。实测证据见 6.2 节(第 11 次请求 429,窗口滑过后恢复)。
Q7:匿名调用为什么有时 403 有时 302?
取决于端点的匿名语义:读取类端点(info/download)匿名放行;/history 等登录态端点对匿名返回 302 回首页(浏览器语义:请先登录);POST /new/:noteId 这类显式校验失败才返回 403 错误页。脚本判断「未登录」还是「无权限」时,把 302 与 403 一起纳入分支判断。
九、参考链接
站内系列(HedgeDoc 专栏,专栏 id 744):
- 《HedgeDoc 介绍与选型指南》——定位、架构与同类平台量化选型对比
- 《HedgeDoc 1.12.0 Docker Compose 部署指南》——本文实例的部署来源,含持久化、备份与升级
- 《HedgeDoc 1.12.0 配置参数详解》——本文涉及的全部 CMD_* 参数语义与默认值
官方来源(查询日期 2026-10-22):
- HedgeDoc 官网:https://hedgedoc.org
- 官方文档(含 1.x 配置参考):https://docs.hedgedoc.org
- 官方仓库:https://github.com/hedgedoc/hedgedoc
- 1.12.0 发布页:https://github.com/hedgedoc/hedgedoc/releases/tag/1.12.0