fix: URL-encode Target header for non-ASCII heading names#69
Open
marcoaperez wants to merge 1 commit intojacksteamdev:mainfrom
Open
fix: URL-encode Target header for non-ASCII heading names#69marcoaperez wants to merge 1 commit intojacksteamdev:mainfrom
marcoaperez wants to merge 1 commit intojacksteamdev:mainfrom
Conversation
The Local REST API OpenAPI spec states that the Target header value "can be URL-Encoded and *must* be URL-Encoded if it includes non-ASCII characters." However, patch_vault_file and patch_active_file were passing the Target header value as-is, causing HTTP header validation errors for any heading containing accented characters (e.g., "Información", "Método") or special characters (e.g., em-dash). This commit: - Applies encodeURIComponent() to the Target header in both patch_active_file and patch_vault_file - Applies the same encoding to Target-Delimiter header - Changes obsidian imports in plugin-templater.ts to type-only imports, since they are only used in type positions (interfaces/type aliases) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
✅ Deploy Preview for superb-starlight-b5acb5 canceled.
|
istefox
added a commit
to istefox/obsidian-mcp-tools
that referenced
this pull request
Apr 11, 2026
…uster A)
Fix three independent bugs in patch_active_file and patch_vault_file that
together made the tools dangerous and unusable for many real-world cases:
1. Silent content corruption with nested headings
The Local REST API markdown-patch library indexes headings by full
hierarchical path with the "::" delimiter ("Top Level::Section A"). The
server was sending partial leaf names ("Section A") verbatim, and because
Create-Target-If-Missing was hardcoded to true, the lookup failed silently
and a new heading was appended at EOF instead of patching the intended
one. The server now parses the target file, resolves the leaf name to
its full ancestor path via a new resolveHeadingPath helper, and sends
that to the API. Strict mode is exposed through the new
createTargetIfMissing optional parameter so agents can opt into
explicit errors instead of auto-creation.
2. Non-ASCII heading failures
Target and Target-Delimiter HTTP headers were sent raw, which fails for
Japanese, emoji, or any heading with characters outside the ASCII
header grammar. Both headers are now URL-encoded — crucially, *after*
path resolution, so the indexer lookup still sees plain strings.
3. Missing trailing newlines on append
When operation == "append", content without trailing newlines would run
visually into the next section. Normalized to end with "\n\n".
Approach combines three community PRs never merged upstream:
- PR jacksteamdev#72 (grimlor): resolveHeadingPath + createTargetIfMissing
- PR jacksteamdev#69 (marcoaperez): URL-encode Target / Target-Delimiter
- PR jacksteamdev#48 (vanmarkic): test coverage patterns for header encoding
Adds patchVaultFile.test.ts with 10 unit tests for resolveHeadingPath:
empty content, no headings, H1 match, H2 partial, deep H3 nesting,
missing leaf, duplicate names, custom delimiter, stack reset on level
return, and non-heading lines mid-paragraph.
Performance note: resolving a partial heading name adds one extra HTTP
request per PATCH call (to fetch the target file content). Full-path
targets (already containing the delimiter) skip the resolution step.
Manual end-to-end verification pending on Stefano's Obsidian + Local
REST API setup.
Refs: jacksteamdev#30, jacksteamdev#71
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two fixes for the PATCH endpoint in
patch_vault_fileandpatch_active_file:1. URL-encode Target header for non-ASCII heading names
patch_vault_fileandpatch_active_filefail withHeader 'Target' has invalid valuewhen the target heading contains non-ASCII characters (e.g., accented letters likeó,é,á, or special characters like–).This is a common scenario for users writing in Spanish, French, German, and other languages that use diacritics.
Example errors
Root cause
HTTP headers only accept ASCII characters. The
Targetheader value was passed as-is without encoding. The Local REST API OpenAPI spec explicitly states:(See
packages/obsidian-plugin/docs/openapi.yaml, lines 361-362)2. Prevent replace operation from duplicating headings
When using
replaceon a heading, content was duplicated instead of replaced. A new heading was created at the end of the file with the new content, while the original heading and its content remained untouched.Root cause
The
Create-Target-If-Missingheader was always sent astrue, even forreplaceoperations. When the API couldn't find the target heading (e.g., when using a leaf heading name instead of the full path), instead of returning an error, it silently created a new duplicate heading at the end of the file.For
replace, if the target doesn't exist, the operation should fail — you can't replace something that doesn't exist.Changes
packages/mcp-server/src/features/local-rest-api/index.tsencodeURIComponent()to theTargetheader in bothpatch_active_fileandpatch_vault_fileTarget-DelimiterheaderCreate-Target-If-Missing: trueforappend/prependoperations, not forreplacepackages/shared/src/types/plugin-local-rest-api.tstargetparameter description to emphasize that headings must use the full path delimited by::(e.g.Heading 1::Subheading 1:1)packages/shared/src/types/plugin-templater.ts"obsidian"to type-only imports (import type { ... })Test plan
patch_vault_fileworks with headings containing accented characters (e.g.,Información del host)replacewith full heading path works correctly (content replaced, no duplication)replacewith leaf-only heading name now returnsinvalid-targeterror instead of duplicatingappendwithCreate-Target-If-Missingstill creates headings when missing (no regression)sharedandmcp-serverpackages🤖 Generated with Claude Code