--- type: decision date: 2026-05-28 status: open tags: [obsidian, rest-api, bug-report, upstream] related: [[obsidian-memory-plugin]] --- # 2026-05-28 — File upstream issue for Obsidian Local REST API heading resolver bug ## Decision Draft and file a bug report at [coddingtonbear/obsidian-local-rest-api](https://github.com/coddingtonbear/obsidian-local-rest-api) for the heading-resolver failure that affects both `Target-Type: heading` PATCH and `GET /heading/...` reads. ## Why - Bug persists across a major plugin version bump (3.6 → 4.1.1), so it's not a regression in a transient build — it's a real defect that's been sitting in the resolver for at least one release. - The bug silently degrades the obsidian-memory skill: every documented PATCH-heading example in the skill's `references/api-reference.md` fails, forcing fallbacks to POST/PUT/frontmatter-PATCH. We have working workarounds, but they preclude surgical section edits that the skill instructs Claude to do. - Reproducer is small (4 curl calls) and deterministic — high signal-to-noise for a maintainer. - No alternative — only the upstream maintainer can fix it. Local workarounds will live in [[obsidian-memory-plugin]] indefinitely until this lands. ## How to apply - The full draft (title, repro, variations tried, ruled-out causes, suspected cause) is at `D:\REMOTE CODING\unifi-access-dashboard\obsidian-rest-api-issue-draft.md` on the dashboard machine. - Before filing: fill in two placeholders — Obsidian app version and vault filesystem. - After filing: come back to this decision and update `status:` from `open` → `filed` with the issue URL; if/when fixed, mark `resolved` and remove the workaround section from [[obsidian-memory-plugin]]. ## Context that won't be in the issue body - All testing was done against the vault at `https://10.2.0.2:27124` from Claude Code sessions over the LAN. - The skill's documented PATCH-heading example (in `references/api-reference.md`) references a key that has a typo (`8c` instead of `e7` in one digit) — unrelated to this bug but worth noting if anyone tries to copy-paste from there. - `Target-Type: block` also fails with `invalid-target`. Probably the same resolver but not verified — left out of the issue body to keep the report tight; can add as a follow-up comment if relevant. ## Open - [ ] Fill in placeholders in the draft and post the issue - [ ] Paste the issue URL back here under "How to apply" - [ ] When fixed upstream: drop the workaround section from [[obsidian-memory-plugin]] and switch the skill back to PATCH-heading where it makes sense ## Draft body (copy-paste ready) See `obsidian-rest-api-issue-draft.md` on the dashboard machine, or mirrored below: --- ### 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/` with `Target-Type: heading` → HTTP 400 `invalid-target` - `GET /vault//heading/` → HTTP 404 `Not 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** ```bash 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":"...invalid-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`, and `replace` - Both freshly-PUT files (LF endings) and files originally written by Obsidian - `Target-Type: block` also returns `invalid-target` **What I ruled out** - CRLF / BOM in source file — fresh file was written by `curl --data-binary` with 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: frontmatter` succeeds 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: curl over HTTPS to :27124 with `-k` (self-signed cert) - Vault filesystem: `` **Suspected cause** Since `GET /vault//heading/` and `Target-Type: heading` PATCH 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 the wrong field. Happy to bisect or add logging if you can point me at the relevant file.