Skip to content

fix: ensure rm-filename header always includes file extension (#62)#65

Open
VladimirSenflokBrasty wants to merge 1 commit into
ddvk:masterfrom
VladimirSenflokBrasty:fix/issue-62-rm-filename-extension
Open

fix: ensure rm-filename header always includes file extension (#62)#65
VladimirSenflokBrasty wants to merge 1 commit into
ddvk:masterfrom
VladimirSenflokBrasty:fix/issue-62-rm-filename-extension

Conversation

@VladimirSenflokBrasty

Copy link
Copy Markdown

Background

Since around 18 May 2026, the reMarkable Cloud API has been rejecting rm-filename HTTP header values that don't include a file extension. Every request gets back:

HTTP/2.0 400 Bad Request
{"message":"unexpected 'rm-filename' http header"}

This breaks all sync operations on existing accounts (rmapi ls / already fails because BuildTree / Mirror can't fetch any child blob). See #62.

Root cause

api/sync15/blobstorage.go passes the caller-supplied filename straight through to the rm-filename header. Several callers in the read path pass bare names without an extension:

Caller Value
tree.go:377 (BuildTree) e.DocumentID (bare UUID)
blobdoc.go:217 (BlobDoc.Mirror) e.DocumentID (bare UUID)
tree.go:363 (BuildTree, legacy path) literal "roothash"

The Mirror happy path at tree.go:278 already wraps the name with addExt("root", DocSchemaExt) and works fine — only the bare-name call sites trip the new server-side validation.

I verified this directly with curl against internal.cloud.remarkable.com:

Header value Response
(no header) HTTP 400 "unexpected 'rm-filename' http header"
rm-filename: <uuid> HTTP 400 "unexpected 'rm-filename' http header"
rm-filename: x-rm-filename: <uuid> (renamed) HTTP 400
rm-filename: <uuid>.docSchema HTTP 200 OK + real payload
rm-filename: root.docSchema HTTP 200 OK

So the server is happy as long as the value has any extension; the existing addExt(...) callers all satisfy that already.

Fix

Add a small ensureExtension helper in BlobStorage that appends .docSchema when the supplied filename has no ., and wrap both GetReader and UploadBlob with it. At this layer every extensionless name represents a doc-level docSchema blob (legacy "roothash", top-level e.DocumentID from parseIndex of the root index, etc.), so .docSchema is the right default. Callers that already pass a full filename (e.g. addExt(doc.DocumentID, archive.MetadataExt)<uuid>.metadata) are untouched.

This is intentionally a defensive single-point fallback rather than fixing each individual call site, because:

  1. it's minimal (~16 lines added, 1 line touched),
  2. it leaves the existing addExt(name, ext) helper in tree.go as the canonical "I know the right extension" path,
  3. it survives any future caller that forgets the suffix.

If you'd prefer the per-callsite variant, happy to redo it that way.

Verification

  • Curl matrix above against the live server.
  • Built GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build, hot-swapped into a running container, ran the patched binary against an account that was 400'ing all morning. rmapi ls /, rmapi mkdir /test_$(date +%s), rmapi put some.pdf /Articles/ all work again. End-to-end n8n → rmapi → reMarkable upload pipeline restored.

Notes

  • WriteRootIndex (blobstorage.go:55 in current master) sends rm-filename: "roothash" against RootPut = /sync/v3/root, which is a different endpoint and wasn't exercised by my tests since rmapi ls is read-only. If that endpoint adopts the same validation, that header value should also gain a .docSchema suffix — happy to extend the PR if you'd like.
  • Tests in tree_test.go already use full filenames in their fixtures, so they continue to pass; no test changes were required.

Fixes #62.

reMarkable Cloud started rejecting rm-filename header values without a
file extension on ~18 May 2026, returning HTTP 400 with
{"message":"unexpected 'rm-filename' http header"} for bare UUIDs.
This breaks all sync operations (rmapi ls, mkdir, put, mv, etc.) because
BuildTree / Mirror fail on the very first child blob fetch.

Several callers pass extensionless names through to the transport layer:
- tree.BuildTree:377  - provider.GetReader(e.Hash, e.DocumentID)
- blobdoc.Mirror:217  - r.GetReader(e.Hash, e.DocumentID)
- tree.BuildTree:363  - provider.GetReader(rootHash, "roothash") (legacy)

At the BlobStorage layer all such reads/writes represent doc-level
docSchema blobs, so default to the .docSchema suffix when the supplied
filename has no extension. Callers that already pass a full filename
(e.g. addExt(name, MetadataExt) → '<uuid>.metadata') are unaffected.

Verified via curl against internal.cloud.remarkable.com with a valid
bearer token:
  rm-filename: <uuid>            -> HTTP 400 (unexpected header)
  rm-filename: <uuid>.docSchema  -> HTTP 200 (real payload returned)

After the patch, rmapi ls / works on accounts that were locked out
since 18 May 2026.

Fixes ddvk#62

Signed-off-by: Vladimir Senflok <[email protected]>
@Azeirah

Azeirah commented May 18, 2026

Copy link
Copy Markdown

#63 was already actively being worked on, I don't think it's a good idea to have competing PRs for the same issue.

If you want to discuss implementation details, it's probably best to discuss it there.

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.

Any operations fail with status 400

3 participants