Fix TS Result serializer using the ok payload type for err - #5972
Open
captain-mirage wants to merge 1 commit into
Open
captain-mirage wants to merge 1 commit into
captain-mirage wants to merge 1 commit into
Conversation
`SumType.makeSerializer`'s Result fast path built both the `ok` and the
`err` serializer from `ty.variants[0].algebraicType`, so a `Result<Ok, Err>`
whose `Err` payload type differs from `Ok` was written with the `ok`
encoder. `makeDeserializer` already reads `err` with `variants[1]`, so the
two halves disagree: the value is encoded as `Ok` and decoded as `Err`.
The failure is silent whenever the wrong encoder still produces bytes the
`err` decoder accepts. For `Result<u8, string>`, `{ err: 'boom' }` encodes
as the single byte `0x00` (a `string` coerced through `writeU8`); for a
`Result<string, { code: i32 }>`, `{ err: { code: 42 } }` encodes as the
UTF-8 string `"[object Object]"`. Nothing throws at encode time; the
corruption surfaces later as a wrong value or a confusing decode error.
Use `variants[1].algebraicType` for `serializeErr`, matching the
deserializer.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of Changes
SumType.makeSerializer'sResultfast path builds theerrserializer from theokvariant's type:crates/bindings-typescript/src/lib/algebraic_type.ts:655-662(on1906706)The matching
makeDeserializeralready readserrwithty.variants[1].algebraicType(
algebraic_type.ts:764-771), so the two halves of the codec disagree: aResult<Ok, Err>whoseErrpayload type differs from itsOkpayload type iswritten with the
okencoder and read with theerrdecoder.Minimal reproduction:
Nothing throws at encode time, which is what makes this unpleasant.
'boom'goes throughwriteU8, coerces toNaN, and lands as the single byte0x00; the payload is simplygone. The decode side then reads a
u32string length out of bytes that were neverwritten. With a product
Errit is quieter still: forResult<string, { code: i32 }>,{ err: { code: 42 } }is encoded as the UTF-8 string"[object Object]"(15 bytes) and decodes without complaint into whatever those byteshappen to mean as an
i32.Results whoseokanderrtypes coincide are unaffected, which is presumably why thishas survived: the two serializers are then the same function. Mixed-type
Results are notexotic, though —
modules/sdk-test-ts/src/index.ts:422-453declares six tables built onthem (
resultI32Stringist.result(t.i32(), t.string()),resultStringI32is themirror image, plus
Result<Identity, string>,Result<SimpleEnum, i32>,Result<EveryPrimitiveStruct, string>andResult<Vec<i32>, string>), matching the Rust,C# and C++ sdk-test modules. A TypeScript module inserting an
errvalue into any ofthose tables writes a row that this library's own reader cannot decode. The client-api
protocol has the same shape in
OneOffQueryResult.result(
src/sdk/client_api/types.ts:80,Result<QueryRows, string>); that one only travelsserver → client, so it is decode-only on this side and is not corrupted today.
The fix is one character — use
ty.variants[1].algebraicTypeforserializeErr, matchingthe deserializer.
I checked for a second instance of the same slip. Besides line 660,
variants[0]appearseight times in
algebraic_type.tsand all eight are right: 635/639 and 742/746 are theOptionfast path, whose payload genuinely is variant 0; 652 and 761 are the'ok'nameguards; 656 and 765 are the
okserializer and deserializer themselves.src/server/views.ts:303also indexesvariants[0], but that is theOption-returningview path. The C# and Rust bindings derive
ResultBSATN per concrete type at codegentime rather than from a runtime variant list, so they have no analogous site;
crates/bindings-cppmatches onvariants[0].name == "ok"only to recognise aResult, and reads each variant's own type thereafter.API and ABI breaking changes
None. The wire format is unchanged — this makes the encoder produce the BSATN that the
existing decoder, the Rust/C# implementations and the SATS spec already define for a
Resultsum. The only observable difference is thaterrpayloads whose type differsfrom the
okpayload's stop being corrupted.If anyone has worked around this by pre-encoding
errvalues to match theoktype, thatworkaround will now double-encode; I would be surprised if such code exists, since the
corrupted bytes are not decodable by this library's own reader.
Rollback safety impact
n/a — no ControlDB table or reducer, system table, or on-disk format is written, changed,
or made unsupported by this PR.
Expected complexity level and risk
Resultfast path isself-contained; the
Optionfast path above it and the generic sum path below it areuntouched.
Testing
crates/bindings-typescript/tests/serde.test.ts, inthe existing "it correctly serializes and deserializes algebraic values" style
(explicit expected byte arrays, then a decode back to the original value):
Result<u8, string>with anokpayload (the case that already worked, kept as aregression guard), the same type with an
errpayload, andResult<string, { code: i32 }>with a producterrpayload.errtests fail onmasterbefore the change —[1, 0]instead of
[1, 4, 0, 0, 0, 98, 111, 111, 109], and the"[object Object]"encoding for the product case — and pass after it.
pnpm testincrates/bindings-typescript: 30 files, 319 tests, all passing.pnpm build(tsup+tsc -p tsconfig.build.json) andpnpm lint(
eslint .+prettier . --check) clean.Resulttables inmodules/sdk-test-tslook like the natural placefor end-to-end coverage of the
errdirection across the SDK test suite — happy toadd it here if you'd like it in the same PR, but it needs a running host so I left it
out.
old, wrong bytes. Those rows are not decodable by the current reader either, so I
believe there is nothing to migrate, but you have visibility I don't.