ReviewOS

craft-native/craft

A schemeless <Image> source is treated as a bundled asset, so an API's relative paths never load

#378
Closed glennmichael123 opened this 8 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
8 days ago

What happens

An <Image> whose source has no scheme is treated as the name of a bundled asset, never as a URL. From updateImage in CraftNativeScreen.swift:

if !uri.contains(":") {
    guard let image = UIImage(named: uri) else {
        imageFailure(view, uri: uri, message: "Bundled image was not found")
        return
    }
    ...
}

So /api/avatars/9/0a1b2c3d-….jpg is looked up as an asset called /api/avatars/9/0a1b2c3d-….jpg, fails, and nothing is ever fetched.

Why this bites

It is the one place where a native screen and a web page disagree about a value that is correct in both worlds. An API that answers relative paths — ours does, and so does every Rails/Laravel/Stacks app I have seen — works untouched in the browser and silently shows nothing natively.

Found on a device yesterday: every face in Wildloop's athlete directory and leaderboard was a blank circle. The failure compounds, which is why it took a device to see:

  • the image never loads, because it was looked up as an asset;
  • the markup is <Image :show="photo(row)" :source="photo(row)" /> with a <Text :show="!photo(row)"> initial behind it, so :show sees a non-empty string, renders the image that cannot load, and hides the fallback.

A blank 40×40 gap, no error, nothing in any log. Six of our screens had already grown the same inline workaround independently:

url.startsWith('/') ? nativeApiOrigin(craft) + url : url

Two had not. That ratio is the argument for fixing it in the host rather than in every app.

What would help

Resolve a schemeless source against the app's own origin before deciding it is an asset. The host already has it — craft.config.json carries the server the build targets, and app.url is readable from the script side. Something like:

  1. if the uri has a scheme, use it as today;
  2. else if it starts with /, resolve against the configured origin and fetch;
  3. else treat it as a bundled asset name.

That ordering keeps every existing bundled-asset use working, since those are bare names rather than paths.

The same reasoning applies to fetch with a relative path, which a native screen also cannot do today, and for the same reason: shared code that works in a page has to be rewritten to know its own origin.

Versions

craft-native 0.0.237, @stacksjs/stx 0.2.563, iOS 26.5 simulator, Xcode 26.6.

8 days ago

Resolved on main by 7c07a18 (fix(native): resolve app-relative image sources). iOS resolves leading-slash sources against devServerURL before asset lookup; Android uses the same configured-origin resolution. Source-contract coverage is in packages/ios/src/index.test.ts, packages/android/src/index.test.ts, and scripts/native-renderer-contract.test.ts. This is on main and unreleased.

Sign in to comment on this issue.