Thanks to visit codestin.com
Credit goes to github.com

Skip to content

alloy_pubsub: subscription notifications silently dropped when server returns <32-byte subscription ID #3948

Description

@lhadhazy

Summary

alloy_pubsub silently drops every notification when the server returns a subscription ID shorter than 32 bytes. The subscription itself is reported as successful, the WebSocket stream stays open, but subscribe_blocks().into_stream() never yields a single header.

This affects spec-compliant Ethereum nodes — the JSON-RPC subscription spec (eth eth_subscribe) does not constrain the subscription ID to a fixed width. Some providers (notably wss://ethereum-rpc.publicnode.com) return a 16-byte hex ID; alloy's subscribe_blocks() deserialises the response into FixedBytes<32>, which causes a key mismatch in the pubsub frontend's subscription registry.

Reproduction

// Cargo.toml
//   alloy   = { version = "2", features = ["full"] }
//   tokio   = { version = "1", features = ["full"] }
//   futures = "0.3"

use alloy::providers::{Provider, ProviderBuilder};
use alloy::transports::ws::WsConnect;
use futures::StreamExt;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = "wss://ethereum-rpc.publicnode.com";

    let provider = ProviderBuilder::new()
        .connect_ws(WsConnect::new(url))
        .await?;

    let sub = provider.subscribe_blocks().await?;
    let mut stream = sub.into_stream();

    println!("subscribed; waiting up to 60s for a header…");

    match tokio::time::timeout(Duration::from_secs(60), stream.next()).await {
        Ok(Some(header)) => println!("OK — got header {}", header.number),
        Ok(None)         => eprintln!("FAIL — stream closed without yielding"),
        Err(_)           => eprintln!("FAIL — no header received in 60s"),
    }
    Ok(())
}

Run with:

RUST_LOG=alloy=trace cargo run

websocat against the same endpoint receives eth_subscription notifications every ~12 s, confirming the endpoint is healthy and pushing data. Alchemy and Infura (which return 32-byte hex IDs) work correctly with the same code.

Expected behaviour

stream.next() yields a new header per block (~12 s on Ethereum mainnet).

Actual behaviour

stream.next() blocks forever; no headers are emitted.

Trace evidence

Subscription ID gets corrupted between the WS layer and the pubsub frontend:

Layer Subscription ID seen
alloy_transport_ws deserialised message 0x7413bf1aeb8f1c0087c36b4243f7a41a (16 bytes — server response)
alloy_pubsub::frontend retrieved response 0x77ee11bf27e54080a8ae7be2e6b2fa08f48f6f45c9660376b0b7e2e3cc41677a (32 bytes — derived)
alloy_pubsub::service registers GetSub(0x77ee...)
Incoming notifications carry subscription: Number(154292977224430911757972734000217170970) (= original 16-byte ID as u128)

The deserialize_response span shows the type alloy is forcing the response into:

"ty":"alloy_primitives::bits::fixed::FixedBytes<32>"

Lookup key (32-byte derived value) ≠ notification key (original 16-byte ID) → notifications are silently dropped.

Environment

  • alloy: 2.0.4 (workspace dep alloy = { version = "2", features = ["full"] })
  • rustc: 1.95.0 (59807616e 2026-04-14)
  • OS: macOS 14 (arm64, Apple Silicon)
  • Endpoint: wss://ethereum-rpc.publicnode.com

Possible direction

The subscription ID type in alloy_pubsub could be loosened from FixedBytes<32> (i.e. B256) to a SubscriptionId enum that accepts variable-length hex strings or numeric IDs — matching EthNotification::subscription which is already typed as a flexible Number / String variant.

Happy to PR if there's a preferred direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions