Bug Report
When using schema-based serde (@smithy/core ≥ 3.24.6), modeled exceptions on event streams are thrown as plain JavaScript objects instead of instances of the registered error class. This means instanceof checks and catch filtering by exception type are broken for any event-stream operation that uses modeled exceptions.
The HTTP error path (SmithyRpcV2CborProtocol.handleError) correctly resolves the registered error constructor via TypeRegistry.getErrorCtor and wraps the deserialized body with Object.assign(new ErrorCtor(...), ...). The event-stream path in EventStreamSerde does not — it returns the raw deserialized struct directly, which the caller then throws as-is.
Reproduction
Minimal repro (no server needed):
import { ServiceException } from "@smithy/smithy-client";
import { TypeRegistry } from "@smithy/core/schema";
import { getMessageUnmarshaller } from "@smithy/core/event-streams";
// 1. Define and register an exception (same as codegen output)
class MyStreamError extends ServiceException {
constructor(opts) {
super({ name: "MyStreamError", $fault: "client", ...opts });
Object.setPrototypeOf(this, MyStreamError.prototype);
}
}
const schema = [-3, "com.example", "MyStreamError", { error: "client", httpError: 400 }, ["message"], [0]];
TypeRegistry.for("com.example").registerError(schema, MyStreamError);
// 2. Simulate an event-stream exception wire message
const msg = {
headers: {
":message-type": { type: "string", value: "exception" },
":exception-type": { type: "string", value: "MyStreamError" },
":content-type": { type: "string", value: "application/json" },
},
body: new TextEncoder().encode(JSON.stringify({ message: "something went wrong" })),
};
// 3. Unmarshal it
const unmarshaller = getMessageUnmarshaller(async (event) => {
const key = Object.keys(event).find(k => k !== "__type");
return { [key]: JSON.parse(new TextDecoder().decode(event[key].body)) };
}, (b) => new TextDecoder().decode(b));
try { await unmarshaller(msg); } catch (e) {
console.log(e instanceof MyStreamError); // false ← BUG
console.log(e instanceof Error); // false ← BUG
console.log(e); // { message: 'something went wrong' }
}
// Meanwhile, the registry HAS the ctor:
TypeRegistry.for("com.example").getErrorCtor(schema) === MyStreamError; // true
Expected behavior
The thrown value should be instanceof MyStreamError (and instanceof Error), matching the HTTP error path behavior.
Current behavior
The thrown value is a plain object { message: "something went wrong" } — not an Error, not instanceof any registered exception class.
Root cause
In EventStreamSerde.ts, the deserialization callback returns:
return { [unionMember]: await this.deserializer.read(eventStreamSchema, body) };
This raw struct is then thrown by getMessageUnmarshaller without ever consulting TypeRegistry.getErrorCtor to wrap it in the registered constructor.
Environment
@smithy/core: 3.24.6 (also verified against current main at 5e1e7df4b1c0e2ca5982a1a46b874bf7a68f1f3a)
Affects any generated client using schema-based serde with event-stream operations that have modeled @error members
Suggested fix
In the event-stream exception path, after deserializing the body, check if the member schema is an error schema (schema[0] === -3). If so, resolve the constructor via TypeRegistry.for(namespace).getErrorCtor(schema) and wrap with Object.assign(new ErrorCtor({message}), {$fault}, deserialized) — the same pattern used by SmithyRpcV2CborProtocol.handleError. Fall back to current behavior for unregistered/unknown exception types.
Bug Report
When using schema-based serde (
@smithy/core≥ 3.24.6), modeled exceptions on event streams are thrown as plain JavaScript objects instead of instances of the registered error class. This meansinstanceofchecks andcatchfiltering by exception type are broken for any event-stream operation that uses modeled exceptions.The HTTP error path (
SmithyRpcV2CborProtocol.handleError) correctly resolves the registered error constructor viaTypeRegistry.getErrorCtorand wraps the deserialized body withObject.assign(new ErrorCtor(...), ...). The event-stream path inEventStreamSerdedoes not — it returns the raw deserialized struct directly, which the caller then throws as-is.Reproduction
Minimal repro (no server needed):
Expected behavior
The thrown value should be instanceof MyStreamError (and instanceof Error), matching the HTTP error path behavior.
Current behavior
The thrown value is a plain object { message: "something went wrong" } — not an Error, not instanceof any registered exception class.
Root cause
In EventStreamSerde.ts, the deserialization callback returns:
return { [unionMember]: await this.deserializer.read(eventStreamSchema, body) };
This raw struct is then thrown by getMessageUnmarshaller without ever consulting TypeRegistry.getErrorCtor to wrap it in the registered constructor.
Environment
Suggested fix
In the event-stream exception path, after deserializing the body, check if the member schema is an error schema (schema[0] === -3). If so, resolve the constructor via TypeRegistry.for(namespace).getErrorCtor(schema) and wrap with Object.assign(new ErrorCtor({message}), {$fault}, deserialized) — the same pattern used by SmithyRpcV2CborProtocol.handleError. Fall back to current behavior for unregistered/unknown exception types.