THRIFT-5953: Rust codegen forward-compatible union deserialization in structs#3414
THRIFT-5953: Rust codegen forward-compatible union deserialization in structs#3414santiagomed wants to merge 2 commits intoapache:masterfrom
Conversation
6c16a79 to
d3225c2
Compare
…n structs
When a struct field has a union type, the generated Rust deserializer
now catches 'received empty union' errors from unknown variants and
treats them as None instead of propagating the error.
Previously, if a Thrift union received a field ID not present in the
local IDL, the union deserializer returned Err('received empty union'),
which cascaded up and caused the entire parent struct deserialization
to fail. This broke forward compatibility.
The fix wraps union field reads in struct deserializers with a match
that catches the specific 'received empty union' error and silently
skips the field. The struct field (already Option<UnionType>) remains
None, matching the behavior as if the field were absent. All other
errors (malformed data, I/O failures) still propagate normally.
Handles both direct union fields and Box<Union> (forward typedef)
fields by resolving the type before generating the method call.
This aligns Rust with Java, Go, Python, and C++ Thrift behavior.
d3225c2 to
61e19a6
Compare
|
BTW We still look for Rust maintainers as the previous one got lost somehow. |
We have rules about AI generated content. Acrtually in the codebase. And THRIFT-XXXX is not a valid number. |
| f_gen_ << indent() << "match " << read_call << " {" << '\n'; | ||
| indent_up(); | ||
| f_gen_ << indent() << "Ok(val) => { " << struct_field_read_temp_variable(tfield) << " = Some(" << val_expr << "); }," << '\n'; | ||
| f_gen_ << indent() << "Err(thrift::Error::Protocol(ref e)) if e.message.contains(\"received empty union\") => {" << '\n'; |
There was a problem hiding this comment.
I would consider that string search a code smell. What about adding a dedicated ProtocolErrorKind variant? Similar to ProtocolErrorKind::InvalidData for the "empty union" case around line 1837.
There was a problem hiding this comment.
I added ProtocolErrorKind::EmptyUnion = 7 to the runtime library and updated both sides of the codegen to use it. The union deserializer now emits EmptyUnion instead of InvalidData, and the struct-level catch arm matches on e.kind == ProtocolErrorKind::EmptyUnion instead of the string check. Also added an integration test covering the unknown variant path.
|
@Jens-G thanks for the review. Will apply these changes. Also, I am happy to take on as maintainer - we at xAI use Rust + Thrift quite a bit in production and need active support. Let me know what's needed EDIT: I requested a Jira account for the apache thrift project to create the ticket and apply as maintainer. |
… test - Add EmptyUnion variant to ProtocolErrorKind for type-safe error matching - Union codegen emits EmptyUnion instead of InvalidData for empty unions - Struct codegen catches EmptyUnion by kind instead of string contains - Add AidKit struct and integration test for unknown union variant handling
|
@Jens-G created a Jira ticket: https://issues.apache.org/jira/projects/THRIFT/issues/THRIFT-5953?filter=allopenissues |
|
This looks like a large change, and the fact we have tests disabled on CI does not inspire confidence. @santiagomed would it make sense to create a separate pull request to enable back See e4666f2 |
Problem
When a struct contains a union-typed field and the wire data has a union variant (field ID) not present in the local IDL, the generated Rust code fails to deserialize the entire struct — not just the unknown union field.
The root cause: the union's
read_from_in_protocolcorrectly returnsErr("received empty union")for unknown variants (fixed in PR #3336). But the parent struct's generated deserializer propagates this error via?, causing the whole struct to fail.This breaks Thrift's forward-compatibility guarantee. When a server adds a new union variant, all clients running an older IDL version fail to parse any message containing that variant — even though the client doesn't need to understand the new variant to process the rest of the struct.
Java, Go, Python, and C++ Thrift all handle this gracefully: unknown union variants are silently skipped, and the struct field is left unset.
Fix
In
render_struct_sync_read, detect struct fields whose type is a union. For those fields, wrap theread_from_in_protocolcall in amatchthat catches the"received empty union"error and treats it asNone(field absent):Non-union fields are unchanged. The union's own
TSerializableimpl is unchanged — it still returnsErrfor unknown variants, preserving the contract for callers that read unions directly.Impact
TSerializabletrait or the thrift runtime libraryNoneinstead of causing parse failure