Skip to content

UserError message and metadata are masked at the client boundary #5086

@IGassmann

Description

@IGassmann

Bug

UserError values can include a public message, a user-defined code, and structured metadata. The message and metadata should be visible to the caller because UserError represents an intentional user-facing failure, not an internal runtime failure.

Example:

throw new UserError("quota exceeded", {
	code: "quota_exceeded",
	metadata: {
		limit: 20,
		attempted: 25,
	},
});

Expected client-visible fields:

{
	"group": "user",
	"code": "quota_exceeded",
	"message": "quota exceeded",
	"metadata": {
		"limit": 20,
		"attempted": 25
	}
}

Current behavior preserves the group and code, but masks the error at the rivetkit-core client boundary. The caller receives the generic internal-error message and no metadata, even though the error was explicitly constructed as a public UserError.

Cause

The TypeScript and NAPI bridge preserve the structured error correctly. The loss happens later in rivetkit-core when client-facing errors are filtered through client_error_message and client_error_metadata.

Those helpers only preserve message and metadata for groups recognized by public_error_status_code. The user group is missing from that allowlist, so core treats dynamic UserError values as private for message/metadata sanitization.

Impact

Clients can still see group == "user" and the user-defined code, but they lose the human-readable message and any structured metadata. That makes expected domain failures harder to present or handle precisely because details such as limits, attempted values, field names, or validation data are stripped before they reach the caller.

Scoped Fix

Treat group == "user" as public in rivetkit-rust/packages/rivetkit-core/src/error.rs so the core client boundary preserves the intended message and metadata. Add focused regression coverage for a dynamic user error with structured metadata.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions