4.1 KiB
Heading resolver broken: Target-Type: heading PATCH and GET /heading/... both fail on any valid heading (4.1.1)
Summary
In Obsidian Local REST API 4.1.1, two endpoints that both rely on heading resolution always fail, even on a freshly written file with trivially-valid markdown headings:
PATCH /vault/<path>withTarget-Type: heading→ HTTP 400invalid-targetGET /vault/<path>/heading/<heading-text>→ HTTP 404Not Found
Target-Type: frontmatter PATCH on the same file works fine, so the PATCH route itself is healthy. The bug is isolated to the heading-resolution code path shared by these two endpoints.
Also reproduced in 3.6 before upgrading; the 3.6 → 4.1.1 upgrade did not change the behavior.
Minimal reproducer
The setup writes a clean file with LF-only line endings, then exercises both heading endpoints and one frontmatter endpoint as a control.
HOST="https://YOUR-HOST:27124"
KEY="YOUR_API_KEY"
# 1. PUT a fresh, simple file (HTTP 204 expected)
cat > /tmp/repro.md <<'EOF'
---
type: scratch
---
# Hello
## Section A
Content under A.
## Section B
Content under B.
EOF
curl -sk -w "PUT: %{http_code}\n" -X PUT \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/repro.md \
"$HOST/vault/scratch/repro.md"
# 2. PATCH append under heading "Section A" — expected 200, actual 400 invalid-target
echo "appended" > /tmp/p.md
curl -sk -w "\nPATCH heading: %{http_code}\n" -X PATCH \
-H "Authorization: Bearer $KEY" \
-H "Operation: append" \
-H "Target-Type: heading" \
-H "Target: Section A" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/p.md \
"$HOST/vault/scratch/repro.md"
# 3. GET the heading's content via URL path — expected 200, actual 404
curl -sk -w "\nGET heading: %{http_code}\n" \
-H "Authorization: Bearer $KEY" \
"$HOST/vault/scratch/repro.md/heading/Section%20A"
# 4. CONTROL: PATCH frontmatter — works (HTTP 200)
curl -sk -w "\nPATCH frontmatter: %{http_code}\n" -X PATCH \
-H "Authorization: Bearer $KEY" \
-H "Operation: replace" \
-H "Target-Type: frontmatter" \
-H "Target: type" \
-H "Content-Type: application/json" \
--data '"updated"' \
"$HOST/vault/scratch/repro.md"
Actual output
PUT: 204
{"message":"The patch you provided could not be applied to the target content.\ninvalid-target","errorCode":40080}
PATCH heading: 400
{"message":"Not Found","errorCode":40400}
GET heading: 404
PATCH frontmatter: 200
Variations tried (all fail identically with invalid-target / 404)
- Single-word heading (
Target: Section) and multi-word (Target: Section A) - URL-encoded header value (
Target: Section%20A) Operation: append,prepend, andreplace- Both freshly-PUT files (LF endings) and files originally written by Obsidian
Target-Type: blockalso returnsinvalid-target— possibly the same resolver
What I ruled out
- CRLF / BOM in source file — fresh file was written by
curl --data-binarywith LF-only content. - Spaces in heading text — single-word headings fail the same way.
- URL encoding of
Target:header value — raw and percent-encoded both fail. - PATCH route generally broken —
Target-Type: frontmattersucceeds on the same file. - Version regression in 4.1.1 — same behavior in 3.6 before upgrading.
Environment
- Obsidian Local REST API: 4.1.1 (also reproduced in 3.6)
- Obsidian:
- OS hosting Obsidian:
- Client:
curlover HTTPS to:27124with-k(self-signed cert) - Vault is on local NTFS / ext4 / APFS (fill in)
Suspected cause
The heading resolver appears to be returning "not found" for every valid heading. Since GET /vault/<path>/heading/<name> and the Target-Type: heading PATCH variant both fail in lockstep (and frontmatter resolution works fine on the same request path), the most likely location is whichever helper enumerates headings within a parsed file's AST — perhaps it's getting an empty list, or matching on a wrong field.
Happy to bisect or add logging if you can point me at the relevant file.