Skip to content

Separate 404 from other errors - #60

Merged
revett merged 18 commits into
8thpark:mainfrom
kwame-Owusu:fix/separate-404-from-other-errors
Jul 18, 2026
Merged

revett merged 18 commits into
8thpark:mainfrom
kwame-Owusu:fix/separate-404-from-other-errors

Conversation

@kwame-Owusu

@kwame-Owusu kwame-Owusu commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #41.

Problem

GetResult and ListResult (and the other storage result types) collapse every failure into ok: false plus a message string. A 404 (object genuinely absent) is indistinguishable from a 403, 500, or network error without parsing the message. The sync engine must tell "remote file deleted" apart from "transient failure", otherwise it will propagate deletions on a flaky connection.

Changes

  • Add ResultStatus type ("ok" | "not_found" | "auth" | "server" | "network")
    to all five storage result types: ConnectionResult, PutResult, GetResult,
    DeleteResult, ListResult
  • Map HTTP status codes and fetch exceptions to the appropriate status in every
    S3 function (testConnection, s3PutObject, s3GetObject, s3DeleteObject,
    s3ListObjects)
  • Extract XML parsing helpers (fieldFrom, decodeXmlText, parseListObjectsXml)
    into src/utils/storage/xml.ts
  • Extract error mapping helpers (messageFor, statusForHttp) into
    src/utils/storage/errors.ts
  • Update unit and integration tests to assert on the new status field

Why

The sync engine needs structural error classification to make safe decisions. With status, a caller can switch on the outcome:

  • "ok" — use the result
  • "not_found" — safe to treat as remote deletion
  • "auth" — stop, credentials or config need fixing
  • "server" / "network" — retry with backoff

The utility extraction keeps storage.ts focused on the S3 client interface and implementation while giving XML parsing and error mapping their own single-concern modules.

Greptile Summary

This PR adds a structured ResultStatus type ("ok" | "not_found" | "auth" | "server" | "network") to all five storage result types and maps HTTP status codes and fetch exceptions to the appropriate status in every S3 function, letting the sync engine distinguish a genuine remote deletion from a transient failure without parsing message strings. It also extracts XML parsing and error-mapping helpers into dedicated single-concern modules under src/utils/storage/.

  • ResultStatus is threaded through ConnectionResult, PutResult, GetResult, DeleteResult, and ListResult, with every return site updated consistently.
  • statusForHttp in errors.ts covers 404 → "not_found", 401/403 → "auth", and 500+ → "server", but the if (code >= 500) guard and the final fallback both return "server", leaving the guard as dead code.
  • Unit and integration tests are updated to assert the new status field, including the "not_found" case on a missing key.

Confidence Score: 5/5

Safe to merge — the change is additive, every return site is updated consistently, and tests cover the key status values including not_found.

The core classification logic is correct and all five storage operations are updated. The only finding is dead code in statusForHttp where the if (code >= 500) guard returns the same value as the fallback, which does not affect runtime behaviour.

src/utils/storage/errors.ts — the if (code >= 500) guard is dead code worth cleaning up before the function grows.

Important Files Changed

Filename Overview
src/utils/storage/errors.ts New helper module mapping HTTP codes to ResultStatus; the if (code >= 500) guard is dead code because the fallback also returns "server"
src/storage.ts Adds ResultStatus to all five result types and threads the new status field through every S3 operation; extraction of helpers to utils/ is clean
src/utils/storage/xml.ts Direct lift-and-shift of XML parsing helpers from storage.ts; logic unchanged
src/utils/storage/encode.ts Extracted encodeKey into its own file; loop replaced with .map() — correct and equivalent
src/storage.test.ts Updates import path for parseListObjectsXml and adds status assertions to the missing-field connection tests
src/storage.itest.ts Adds status assertions to all integration test cases including the not_found case for a missing key

Reviews (2): Last reviewed commit: "Update src/utils/storage/errors.ts" | Re-trigger Greptile

@revett

revett commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@kwame-Owusu Could you resolve conflicts please?

kwame-Owusu and others added 5 commits July 15, 2026 19:22
Resolves 8thpark#36.

`s3PutObject`, `s3GetObject`, and `s3DeleteObject` interpolate keys into
URLs unencoded (`${baseUrl}/${key}`). A filename containing `#` gets
truncated at the fragment, `?` starts a query string, and a literal `%`
can produce invalid escapes, so requests hit the wrong key or fail. This
is inconsistent with `s3ListObjects`, which already encodes its prefix
with `encodeURIComponent`.

- Add module-private `encodeKey` helper that splits a key on `/`,
encodes each segment with `encodeURIComponent`, and rejoins with `/`,
preserving path structure while escaping special characters within
filenames.
- Update `s3PutObject`, `s3GetObject`, and `s3DeleteObject` to pass keys
through `encodeKey` before building the request URL.
- Add two integration tests against live MinIO: one round-trips a key
with space and ampersand (`Foo & Bar.md`), the other with percent and
hash (`100% #special.md`).

Vault files aren't restricted to URL-safe characters, Obsidian lets
users create notes with `#`, `?`, `%`, spaces, and `&` in their names.
Without encoding, these keys silently corrupt the request URL or produce
404s. `s3ListObjects` already handled this correctly; the other three
functions were the gap.
Resolves 8thpark#36.

`s3PutObject`, `s3GetObject`, and `s3DeleteObject` interpolate keys into
URLs unencoded (`${baseUrl}/${key}`). A filename containing `#` gets
truncated at the fragment, `?` starts a query string, and a literal `%`
can produce invalid escapes, so requests hit the wrong key or fail. This
is inconsistent with `s3ListObjects`, which already encodes its prefix
with `encodeURIComponent`.

- Add module-private `encodeKey` helper that splits a key on `/`,
encodes each segment with `encodeURIComponent`, and rejoins with `/`,
preserving path structure while escaping special characters within
filenames.
- Update `s3PutObject`, `s3GetObject`, and `s3DeleteObject` to pass keys
through `encodeKey` before building the request URL.
- Add two integration tests against live MinIO: one round-trips a key
with space and ampersand (`Foo & Bar.md`), the other with percent and
hash (`100% #special.md`).

Vault files aren't restricted to URL-safe characters, Obsidian lets
users create notes with `#`, `?`, `%`, spaces, and `&` in their names.
Without encoding, these keys silently corrupt the request URL or produce
404s. `s3ListObjects` already handled this correctly; the other three
functions were the gap.
Resolves 8thpark#4.

## Problem

src/main.ts registers the vault create handler in onload. Obsidian fires
create for every file during vault load, so startup schedules a
redundant debounced refresh on top of the onLayoutReady one. The
documented pattern is to register vault event handlers inside
onLayoutReady.

## Changes

Move the event handlers inside the `onLayoutReady` callback.

## Why

This is the documented pattern and also we do not want to fire these
register vault event handlers for every file.

---------

Co-authored-by: Charlie Revett <[email protected]>
@revett revett added this to the v0.1.0 (Bedrock) milestone Jul 15, 2026
kwame-Owusu and others added 9 commits July 15, 2026 19:30
Resolves 8thpark#36.

`s3PutObject`, `s3GetObject`, and `s3DeleteObject` interpolate keys into
URLs unencoded (`${baseUrl}/${key}`). A filename containing `#` gets
truncated at the fragment, `?` starts a query string, and a literal `%`
can produce invalid escapes, so requests hit the wrong key or fail. This
is inconsistent with `s3ListObjects`, which already encodes its prefix
with `encodeURIComponent`.

- Add module-private `encodeKey` helper that splits a key on `/`,
encodes each segment with `encodeURIComponent`, and rejoins with `/`,
preserving path structure while escaping special characters within
filenames.
- Update `s3PutObject`, `s3GetObject`, and `s3DeleteObject` to pass keys
through `encodeKey` before building the request URL.
- Add two integration tests against live MinIO: one round-trips a key
with space and ampersand (`Foo & Bar.md`), the other with percent and
hash (`100% #special.md`).

Vault files aren't restricted to URL-safe characters, Obsidian lets
users create notes with `#`, `?`, `%`, spaces, and `&` in their names.
Without encoding, these keys silently corrupt the request URL or produce
404s. `s3ListObjects` already handled this correctly; the other three
functions were the gap.
Comment thread src/utils/storage/errors.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@revett revett changed the title Fix: separate 404 from other errors Separate 404 from other errors Jul 17, 2026
@revett
revett merged commit 0d017b4 into 8thpark:main Jul 18, 2026
10 checks passed
@kwame-Owusu
kwame-Owusu deleted the fix/separate-404-from-other-errors branch July 18, 2026 16:57
@revett revett removed this from the v0.1.0 (Bedrock) milestone Jul 21, 2026
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.

Results conflate 404 with other errors

2 participants