HedgeDoc 1.12.0 API 使用详解

HedgeDoc 1.12.0 HTTP API 全域实测详解:状态、认证、笔记、历史、导出五域端点的 curl 请求与响应回显,429/413/403/404/400/500 错误形态与限流行为实测

HedgeDocHedgeDocAPI2026/10/22

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,两个技巧贯穿全文:

  1. Cookie 会话保持:登录用 -c cookies.txt 把会话 Cookie 写入文件,后续请求用 -b cookies.txt 携带,这是 HedgeDoc 1.x 唯一的认证通道(见 3.3 节)。
  2. 重定向观测:HedgeDoc 的大量端点返回 302,本文统一用 -o /dev/null -w 'POST /login -> %{http_code} redirect=%{redirect_url}\n' 这类格式串把状态码与跳转地址打印出来(%{http_code} 是 curl 的内置变量,读者照抄即可看到与本文一致的结果)。

三、API 总览

本章先建立全局认知:协议风格、端点清单、认证方式、限流约定与通用响应特征,后续各章按域展开。

3.1 协议与风格

HedgeDoc 1.x 的 HTTP 接口有三个鲜明的风格特征,逐条说明:

  1. 无 API 前缀、与页面共享命名空间:端点形如 /new、/history、/me,没有 /api/v1 这类前缀。好处是路径直观,代价是不能随便给笔记起与保留路径冲突的别名——服务端内置了保留名清单(robots.txt、favicon.ico、api、build、css、docs、fonts、js、uploads、vendor、views),以这些名字创建别名笔记会被拒绝。
  2. 面向浏览器表单流的应答形态:创建、登录、注册、历史写入等「写操作」的应答是 302 重定向(Location 指向结果页),而不是 JSON 返回体;「读操作」(/status、/me、/:noteId/info、/history 等)才是 JSON。
  3. 读取端点开放 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 GistGitHub 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 /register10 次 / 5 分钟CMD_RATE_LIMIT_USER
新建笔记限流POST /new、GET /new20 次 / 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在线注册用户数 / 去重值登录用户活跃度
isConnectionBusySocket.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 字节触发)
4295 分钟窗口内创建超 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> <短语> <文案>:

状态码短语错误页标题(实测)典型触发场景
400Bad Request400 Bad Request something not right./login 缺字段(实测);POST /history/:noteId 缺 pinned(实测)
403Forbidden403 Forbidden oh no.无权限访问;POST /new/:noteId 未开 allowFreeURL(实测);DELETE /history 缺 token(实测)
404Not Found404 Not Found oops.笔记不存在(实测);历史条目不存在(实测)
409Conflict409 Conflict This note already exists.别名冲突(需开 allowFreeURL,源码确认)
413Payload Too Large413 Payload Too Large Shorten your note!正文超 documentMaxLength=100000(实测 100214 字节触发)
429Too Many Requests429 Too Many Requests Try again later.登录/注册超 10 次/5 分钟(实测第 11 次触发);创建超 20 次/5 分钟(配置确认)
500Internal ErrorError(未捕获异常形态,实测)或 500 Internal Error wtf.服务端异常;POST /history/:noteId 缺表单 Content-Type(实测 Error 形态)
503Service UnavailableI'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 之类的查询参数(实测与源码结构一致),过滤逻辑需在客户端完成。对自动化场景的三点建议:

  1. 历史域:历史列表通常很短(个人浏览记录),全量拉取后按 pinned/time 自行过滤即可;
  2. 修订域:长生命周期笔记的修订数会持续增长,定期巡检修订列表的 length 字段(数组元素数)作为数据膨胀指标;
  3. 实例级清单: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-Type500请求体解析器未运行,服务端读取字段时未捕获异常,返回朴素 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):

官方来源(查询日期 2026-10-22):