Skip to content

fix: URL-encode Target header for non-ASCII heading names#69

Open
marcoaperez wants to merge 1 commit intojacksteamdev:mainfrom
marcoaperez:fix/encode-patch-target-header
Open

fix: URL-encode Target header for non-ASCII heading names#69
marcoaperez wants to merge 1 commit intojacksteamdev:mainfrom
marcoaperez:fix/encode-patch-target-header

Conversation

@marcoaperez
Copy link
Copy Markdown

@marcoaperez marcoaperez commented Feb 26, 2026

Summary

Two fixes for the PATCH endpoint in patch_vault_file and patch_active_file:

1. URL-encode Target header for non-ASCII heading names

patch_vault_file and patch_active_file fail with Header 'Target' has invalid value when 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

Header 'Target' has invalid value: 'Información del host'
Header 'Target' has invalid value: 'Diario de acciones – BenditaRespaldo'
Header 'Target' has invalid value: 'Decisiones y convenciones::Método de alta'

Root cause

HTTP headers only accept ASCII characters. The Target header value was passed as-is without encoding. The Local REST API OpenAPI spec explicitly states:

Target to patch; this value can be URL-Encoded and must be URL-Encoded if it includes non-ASCII characters.

(See packages/obsidian-plugin/docs/openapi.yaml, lines 361-362)

2. Prevent replace operation from duplicating headings

When using replace on 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-Missing header was always sent as true, even for replace operations. 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.ts

  • Apply encodeURIComponent() to the Target header in both patch_active_file and patch_vault_file
  • Apply the same encoding to the Target-Delimiter header
  • Only send Create-Target-If-Missing: true for append/prepend operations, not for replace

packages/shared/src/types/plugin-local-rest-api.ts

  • Improve target parameter 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

  • Change value imports from "obsidian" to type-only imports (import type { ... })
  • All imported symbols are only used in type positions, so they don't need runtime imports

Test plan

  • Verified patch_vault_file works with headings containing accented characters (e.g., Información del host)
  • Verified replace with full heading path works correctly (content replaced, no duplication)
  • Verified replace with leaf-only heading name now returns invalid-target error instead of duplicating
  • Verified append with Create-Target-If-Missing still creates headings when missing (no regression)
  • TypeScript check passes on both shared and mcp-server packages
  • No changes to existing behavior for ASCII-only headings

🤖 Generated with Claude Code

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>
@netlify
Copy link
Copy Markdown

netlify bot commented Feb 26, 2026

Deploy Preview for superb-starlight-b5acb5 canceled.

Name Link
🔨 Latest commit cadfc41
🔍 Latest deploy log https://app.netlify.com/projects/superb-starlight-b5acb5/deploys/69a0c830c7966f000850a722

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant