Shad
Shad

Reputation: 4464

Get SignalR Core received message size

There is a setting to limit maximum size of received signalr core messages:

services.AddSignalR(options =>
{
    options.MaximumReceiveMessageSize = 32768;
});

I need to track actual size of incoming signalr core messages, but didn't find the way to get it. Is there any way to do this? It would be nice if it's possible inside IHubFilter implementation.

Upvotes: 1

Views: 288

Answers (3)

Shad
Shad

Reputation: 4464

Actually I've found the way to measure signalr message size, adding decorator for IHubProtocol. It doesn't look like right extension point, but it allows to get precise message size without additional serialization.

Decorator:

public sealed class HubProtocolTrackingDecorator : IHubProtocol
{
    private readonly IHubProtocol _hubProtocol;

    public string Name => _hubProtocol.Name;
    public int Version => _hubProtocol.Version;
    public TransferFormat TransferFormat => _hubProtocol.TransferFormat;

    public HubProtocolMonitoringDecorator(IHubProtocol hubProtocol)
    {
        _hubProtocol = hubProtocol;
    }

    public void WriteMessage(HubMessage message, IBufferWriter<byte> output)
        => _hubProtocol.WriteMessage(message, output);

    public ReadOnlyMemory<byte> GetMessageBytes(HubMessage message)
        => _hubProtocol.GetMessageBytes(message);

    public bool IsVersionSupported(int version)
        => _hubProtocol.IsVersionSupported(version);

    public bool TryParseMessage(
        ref ReadOnlySequence<byte> input,
        IInvocationBinder binder,
        out HubMessage message)
    {
        var size = input.Length;
        var isParsed = _hubProtocol.TryParseMessage(ref input, binder, out message);

        if (message is InvocationMessage invocationMessage) {
            // TODO: track size, invocationMessage.Target
        }

        return isParsed;
    }
}

DI registration (example for JSON protocol):

services.AddSignalR()
    .Services
    .AddSingleton<JsonHubProtocol>()
    .AddSingleton<IHubProtocol>(sp => new HubProtocolTrackingDecorator(
        sp.GetRequiredService<JsonHubProtocol>()));

Upvotes: 0

Prolog
Prolog

Reputation: 3374

TL; DR

It's not possible with current connection handler implementation. Getting the actual size of raw message without re-serialization would interfere with underlying PipeReader breaking the SignalR's message reading logic.

Full explanation

SignalR is using a ConnectionContext to hold ASP.NET Core's individual connection data. That in turn uses System.IO.Pipelines to allow transporting data on the connection, since pipelines are dedicated to read and write streamable data. You can get access to the underlying connection context through feature collection when inside a hub filter:

public class MessageSizeFilter : IHubFilter
{
    public async Task InvokeMethodAsync(InvokeMethodAsync invocationContext, Func<InvokeMethodAsync, Task> next)
    {
        var connectionContext = invocationContext.Context.Features
            .Select(x => x.Value)
            .OfType<ConnectionContext>()
            .FirstOrDefault();
        var pipeReader = connectionContext.Transport.Input;

        await next(invocationContext);
    }
}

Getting the length of a single message in itself is not that difficult. You'd normally use reading capabilities from the PipeReader:

var result = await pipeReader.ReadAsync();
var messageLength = result.Buffer.Length;

But reading from a pipe that already is in process of reading started from different place is difficult. It is not how pipes were designed to be used. If the pipe is already in the reading state, it will throw an exception when you try to start a new reading process. You'd have to cancel or complete the original process, thus interfering with the reading logic on SignalR side. You can see on your own how the reading process looks like in the SignalR's connection handler. Notice the big while loop that keeps things running and deals with incoming messages as they come, reads them and tries to dispatch to the correct consumer.

All right, then maybe start reading before the SignalR's connection handler starts its work? Well... good luck with that. You'd have to re-implement the connection handler as there is no fitting method to override, so you could simply throw in only pieces of your own code.

IMO, it will be easier to create an issue on GitHub, so folk from MS can at least give you some guidance. I haven't been able to get past the interference issue of PipeReader throwing an InvalidOperationException: Reading is already in progress. You may try diving into that topic on your own. Here's some good discussion on PipeReader on GitHub.

Alternatively, try requesting a missing functionality.

Alternative solution with serialization

If you have luxury of knowing the way of communication used and can assume that is not going to change depending on clients, say it is going to always be JSON, then you can try serialization and to the length obtained from JSON bytes add few more for the message envelope, which is not huge.

Upvotes: 1

Yumiao Kong
Yumiao Kong

Reputation: 542

You can try this code to track actual size of incoming signalr core messages inside IHubFilter implementation:

public class HubLogLength : IHubFilter
{
    private readonly ILogger<HubLogFilter> _logger;

    public HubLogLength(ILogger<HubLogFilter> logger)
    {
        _logger = logger;
    }

    public async ValueTask<object?> InvokeMethodAsync(
        HubInvocationContext invocationContext,
        Func<HubInvocationContext, ValueTask<object?>> next
    )
    {
        try
        {
            var result = await next(invocationContext);

            result = "failed";
            // this means we can find the message sent from client
            if (invocationContext.HubMethodArguments.Count > 0)
            {
                result = "succeed";
            }

            var arguments = invocationContext.HubMethodArguments;

            // the total length of the json:

            /*var contentLength = System.Text.Json.JsonSerializer.Serialize(arguments).Length;
            _logger.LogInformation("message content length {ContentLength}", contentLength);*/


            // actual size of incoming signalr core messages:
            if (arguments.Count > 1 && arguments[1] is string message)
            {
                int messageLength = message.Length;

                _logger.LogInformation("message content length: {MessageLength}", messageLength);
            }

            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError($"Exception calling '{invocationContext.HubMethodName}': {ex}");
            throw;
        }
    }
}

The test result:

https://i.sstatic.net/QSkJOX6n.png

Upvotes: 2

Related Questions