Skip to content

Fix Capacitor iOS binary SQLite param handling#905

Open
swhitt wants to merge 1 commit intopowersync-ja:mainfrom
swhitt:sqlite-connector-fix
Open

Fix Capacitor iOS binary SQLite param handling#905
swhitt wants to merge 1 commit intopowersync-ja:mainfrom
swhitt:sqlite-connector-fix

Conversation

@swhitt
Copy link

@swhitt swhitt commented Mar 22, 2026

Fixes #904.

On Capacitor iOS, PowerSync can pass Uint8Array payloads into:

tx.executeRaw('SELECT powersync_control(?, ?)', [op, payload])

The Capacitor SQLite iOS bridge doesn't seem to accept that shape directly, and sync fails with:

Run: failed(message: "Error in reading buffer")

This normalizes typed-array payloads at the Capacitor/native SQLite boundary before they hit run(...).

I think that keeps the fix in the right place and doesn't break anything unrelated. The sync layer can still use Uint8Array, and we only adapt it where the native bridge needs a different shape.

Not sure if a changeset makes sense here, but I think probably yes if this lands as a package fix.

@changeset-bot
Copy link

changeset-bot bot commented Mar 22, 2026

🦋 Changeset detected

Latest commit: 8cea1af

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@powersync/capacitor Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@swhitt swhitt force-pushed the sqlite-connector-fix branch from 7e5ded0 to e82cba0 Compare March 22, 2026 22:46
Normalize typed-array payloads before calling Capacitor SQLite's iOS run() path. This avoids the "Error in reading buffer" failure when PowerSync sends line_binary payloads through powersync_control(...).
@swhitt swhitt force-pushed the sqlite-connector-fix branch from e82cba0 to 8cea1af Compare March 22, 2026 22:48
@stevensJourney stevensJourney self-assigned this Mar 23, 2026
return value;
}

function uint8ArrayToIOSBuffer(array: Uint8Array): IOSSqliteBufferParam {
Copy link
Contributor

@simolus3 simolus3 Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! It's annoying that this workaround is required, but it is what it is.

This feels so absurdly inefficient that I think we should look into not using line_binary on Capacitor at all. Or maybe we need a hack to encode the blob as hex and then decode it with unhex(?) in SQLite. That's still inefficient and I think we should only do it on platforms where we need it.

Out of curiosity, does this really work on Android or did you just test iOS? Anyway, can we return a regular JSON array here (e.g. return [...array])? That should be slightly more efficient, but it's still not great.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: [...array], I think that'd skip the dict-based buffer path in CapacitorSQLite.swift entirely. The Record<string, number> builds the shape the Swift side expects. Haven't tested Android. Agree on unhex(?) longer-term.

Copy link
Collaborator

@stevensJourney stevensJourney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

I believe I've seen other reports of a similar issue before, but, we've not been able to reproduce this error on our end in our example-capacitor demo yet.

In our example, we can see binary payloads being sent to the Capacitor Community SQLite plugin. See the screenshot below from opening the Safari dev tools

Image

These payloads work as expected on our end.

I believe the Error in reading buffer comes from here. Which would most likely indicate some casting error - as pointed out in your PR description. The fact that the calls work in our demo is still concerning though - which leads me to believe there might be another factor at play.

Could you perhaps share similar logs, which cause the issue mentioned in the PR? I assume the offending payloads should have a different structure.

One suspect might be if some Buffer polyfill is present in your project. If you do see a difference in the logs - could you share any polyfils of bundler config you might be using.

It would be really nice if we could get to the root cause of this issue. Thanks for your efforts!

@swhitt
Copy link
Author

swhitt commented Mar 24, 2026

No Buffer polyfills or custom bundler config here (Next.js 16, static export with a minimal config). I think it's coming from inside @powersync/common itself.

The binary payload reaching the Capacitor bridge isn't a plain Uint8Array. RSocket (bundled in @powersync/common/dist/bundle.mjs) wraps WebSocket data with a Buffer polyfill:

// bundle.mjs line 10809
var buffer = bufferExports.Buffer.from(message.data);

// line 3868 - Buffer is a prototype-swapped Uint8Array
Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype);

// line 4626 - custom toJSON
Buffer.prototype.toJSON = function toJSON() {
  return { type: "Buffer", data: Array.prototype.slice.call(this._arr || this, 0) };
};

Chain: WebSocket ArrayBuffer > RSocket Buffer.from() > instanceof Uint8Array passes (AbstractStreamingSyncImplementation.ts:986) > powersync_control("line_binary", payload) > Capacitor bridge receives a Buffer, not a plain Uint8Array.

The iOS bridge (native-bridge.js:858) uses postMessage() directly, not JSON.stringify, so toJSON() isn't causing it here. I think WKWebView's internal serialization handles a prototype-swapped Uint8Array differently than a plain one (couldn't confirm the exact behavior from source alone).

A log in CapacitorSQLite.swift around line 895 would show what's actually arriving:

print("Buffer read failed: key=\(key), value=\(String(describing: mVal)), type=\(type(of: mVal))")

I'll try to add that locally, but probably won't get to it until later this week. Normalizing to a plain Uint8Array at the adapter boundary fixes it (at least for me). Re: example-capacitor, I'd guess it doesn't use the line_binary path, or Vite handles the RSocket Buffer differently.

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.

Capacitor iOS crashes on line_binary payloads with Error in reading buffer

3 participants