zed-industries/zed · error
registered handler for the same message twice
Error message
registered handler for the same message twice
What it means
A ProtoClient is Zed's connection for the collab/rpc protocol; it routes each incoming message type to exactly one handler stored in a map keyed by the message's TypeId. add_message_handler (crates/rpc/src/proto_client.rs:127) installs that mapping and panics if the type already has one, because two handlers would both try to consume the same envelope. This is a wiring assertion: each message type must be registered once per connection.
Source
Thrown at crates/rpc/src/proto_client.rs:127
impl ProtoMessageHandlerSet {
pub fn clear(&mut self) {
self.message_handlers.clear();
self.entities_by_message_type.clear();
self.entities_by_type_and_remote_id.clear();
self.entity_id_extractors.clear();
}
fn add_message_handler(
&mut self,
message_type_id: TypeId,
entity: gpui::AnyWeakEntity,
handler: ProtoMessageHandler,
) {
self.entities_by_message_type
.insert(message_type_id, entity);
let prev_handler = self.message_handlers.insert(message_type_id, handler);
if prev_handler.is_some() {
panic!("registered handler for the same message twice");
}
}
fn add_entity_message_handler(
&mut self,
message_type_id: TypeId,
entity_type_id: TypeId,
entity_id_extractor: fn(&dyn AnyTypedEnvelope) -> u64,
handler: ProtoMessageHandler,
) {
self.entity_id_extractors
.entry(message_type_id)
.or_insert(entity_id_extractor);
self.entity_types_by_message_type
.insert(message_type_id, entity_type_id);
let prev_handler = self.message_handlers.insert(message_type_id, handler);
if prev_handler.is_some() {
panic!("registered handler for the same message twice");View on GitHub (pinned to 9d272b0363)
Solutions
- Move registration so it runs exactly once per connection: install handlers in the function that constructs and owns the ProtoClient.
- For reconnect flows, construct a new ProtoClient for the new connection instead of re-registering on the old one.
- Use the backtrace to identify the message type and the two call sites that both registered it, then keep one.
- In tests, build a fresh client (or client pair) per test rather than reusing a global client.
Example fix
// before: registration runs per session, panics on the second
fn join_room(client: &Arc<TypedProtoClient>) {
client.add_message_handler(handle_room_message);
}
// after: register once at connection construction
fn build_client(conn: Connection) -> Arc<TypedProtoClient> {
let client = Arc::new(TypedProtoClient::new(Arc::new(conn)));
client.add_message_handler(handle_room_message);
client
} Defensive patterns
Strategy: validation
Validate before calling
use std::any::TypeId;
use std::collections::HashSet;
struct HandlerRegistry {
registered: HashSet<TypeId>,
}
impl HandlerRegistry {
// call before every add_message_handler in shared setup code
fn can_register(&mut self, id: TypeId) -> bool {
self.registered.insert(id)
}
} Prevention
- Register handlers in exactly one place that owns the connection's construction.
- Treat re-registration as a design smell: reconnect should mean a new client object.
- In tests, construct a fresh client per test instead of reusing a global one.
- Assert init routines are guarded by a once-flag when reachable from multiple entry points.
When it happens
Trigger: Calling client.add_message_handler (or typed wrappers such as the request/stream handler helpers on TypedProtoClient) twice for the same message type M on the same connection — typically a setup routine that should run once per connection being invoked again, e.g. a second session join or a re-init path reusing the live client.
Common situations: Opening a second collaborative session over an existing connection; refactors that moved handler registration from connection construction into per-view or per-session code; tests that loop and register handlers on a shared client instead of building a fresh client per iteration.
Related errors
- already subscribed to entity
- unsupported TOML value in .env.toml for key {}
- row out of range
- invalid display row {}
- blocking sender returned without value
AI-assisted analysis of zed-industries/zed@9d272b0363 (2026-08-20).
Data as JSON: /api/errors/4e2b4fdad7c830b9.
Report an issue: GitHub.