RPC in a Component's BeginPlay can potentially disconnect a client

Hello! Let me start by saying that this is a very specific bug, but also very easy to solve.

Steps to Reproduce

Adding a replicated UActorComponent in runtime to a remotely-owned actor (like a PlayerController) and then calling a server RPC right in BeginPlay() can potentially disconnect the owning client. I’ve attached a [tiny project][2] that does just that. Just run TestMap in the editor with two instances. If you look at the log, the client is disconnected after about 2 seconds.

Probable Cause

When a replicated component is added server-side, that same component is created in each client’s machine by Unreal’s sub-object replication system. That creation happens in DataChannel.cpp around line 2960:

SubObj = NewObject< UObject >(Actor, SubObjClass);
// Notify actor that we created a component from replication
Actor->OnSubobjectCreatedFromReplication( SubObj );
// Register the component guid
Connection->Driver->GuidCache->RegisterNetGUID_Client( NetGUID, SubObj );

That call to AActor::OnSubobjectCreatedFromReplication() makes sure that, if that sub-object is a component, its RegisterComponent() function is called. Inside that registration, it eventually checks if the Actor has already begun play, and if it has, also calls the component’s BeginPlay(). Now here’s the catch, notice how the NetGUID that should identify that component is only registered in the client after this potential call to BeginPlay().

If we try to call any RPCs before the NetGUID is registered, the client sends a default NetGUID instead, along with the sub-object’s pathname. Then, when the server gets that payload, it’ll see the default GUID and try to find the sub-object by name. However, if that name is not stable, the server (usually) won’t find it. And, as you can see from code in DataChannel.cpp around line 2852, the client is disconnected:

UE_LOG( LogNetTraffic, Error, TEXT( "ReadContentBlockHeader: Client attempted to create sub-object. Actor: %s" ), *Actor->GetName() );

Possible Solution

Moving that call to OnSubobjectCreatedFromReplication() down a few lines, after NetGUID registration, should fix it. As an obvious workaround, I’m avoiding RPCs in BeginPlay().

Use Cases

That might seem like an unusual scenario but in my game I use it all the time. I like to encapsulate modular networked logic by creating components that are dynamically added to the GameState and PlayerStates. It’s like activating a temporary fully-networked GameState anywhere and anytime, very useful and maintainable.

[1]:213524- [2]: 213526-testproject.zip (15.3 KB)

I’ve just created a pull request regarding this issue (PR #4018).