Stack size exceeded in browser for Auto UI forms

This is using 6.03 from MyGet.

If I have a lot (514+ in Edge, 616+ in Chrome) of Service classes, the browser will show a console error “Uncaught RangeError: Maximum call stack size exceeded” when rendering the Auto UI forms on the /ui routes generated for us.

When this happens, the browser no longer responds as it should (probably rightly so) and so things like authenticating if the route required authentication no longer work.

I’ve made a little repro project and published it on Github : https://github.com/JiwaFinancials/DEV-9177.
(Sorry about the name, DEV-9177 - that’s our company internal tracking number for this issue).

I’m going to split our services into microservices to partition various areas of concern in our API which should allow us to get past this issue, but I thought I’d raise it here in case it was something that could be addressed more simply.

This is going to be impossible to fix as it fails when trying to load the API Metadata which exceeds the allowable JSONP callback size.

It’s failing when trying to import the metadata via script tag:

<script>
    function loadApp(app) { window.APP = app }
</script>
<script src="/metadata/app?callback=loadApp&jsconfig=eccn"></script>

This metadata .js payload is 469KB which the browsers are saying exceeds the the allowable size in a function call.

There’s potential mitigations we can do like using shorter property names or splitting the metadata into multiple chunks, e.g. different APIs to retrieve Types + APIs which wont solve the upper limit but will buy you some space to fit more APIs, but that would break the API contract ServiceStack Studio is relying on so it could only occur after we implement everything from ServiceStack Studio and retire the App.

But yeah in the meantime you should split related APIs in Micro Services to reduce the API count of each.

You can buy a bit more space by pruning & preventing serialization of empty collections, e.g:

public override void Configure(Funq.Container container)
{
    this.AddToAppMetadata(meta =>
    {
        meta.Api.Operations.Each(op =>
        {
            if (op.RequiredRoles?.Count == 0)
                op.RequiredRoles = null;
            if (op.RequiresAnyRole?.Count == 0)
                op.RequiresAnyRole = null;
            if (op.RequiredPermissions?.Count == 0)
                op.RequiredPermissions = null;
            if (op.RequiresAnyPermission?.Count == 0)
                op.RequiresAnyPermission = null;
        });
        meta.Api.GetAllTypes().Each(type =>
        {
            if (type.Implements?.Length == 0)
                type.Implements = null;
        });
    });
}

It’s not a solution but I’ll look at places where we can save on space without breaking the schema.

I’ve done some pruning to remove false properties and empty collections in this commit, this should get your example down from 469KB to ~365KB so something like a 22% savings.

That’s the most we’ll be able to do before retiring Studio so we can break the schema by using shorter names or consolidating props.

This change is now available from the latest v6.0.3 that’s now available on MyGet

Thanks for your attention and efforts on this.

When splitting into microservices, as you can’t have multiple AppHosts in the same process I’m assuming I would need to have a separate process for each microservice.

The impact for us and our customers of that would be fairly significant - so I might look at a different approach and see if I can limit the metadata based on subdomain - so I’m thinking something like:

customers.example.com is a configured DNS name which points to the same location as orders.example.com.
Service classes are decorated with an attribute indicating which subdomain they belong to.
The metadata exposed will filter based on the subdomain of the request matching the attribute of the classes. So https://customers.example.com/metadata will only get the metadata for service classes and related responses and requests which have been decorated with that attribute.

I’m open to criticisms on this approach :slight_smile:

It could work if you use AddToAppMetadata(meta => ...) to remove Operations & Types from the returned metadata. You’ll need to be careful not to remove types that are needed in which case I recommend grouping APIs + Types with Tag Groups which your filter inspects to removes all operations/types not in that tag group, so if you run into issues with missing types you can include the Type in that Tag group.

1 Like

So, thanks to your guidance, I’ve successfully used the AddToAppMetadata to reduce the operations shown in the /ui routes to be only ones where the associated request classes have a Tag attribute matching the subdomain.

However, I’m now trying to make the /metadata page behave consistently and also limit what Operations are shown there based on the current subdomain and the request class associated with operation.

I’ve seen how I can override the default template IndexOperations.html with my own - I can’t see how I can use that to selectively limit the operations shown - I think whatever is using that template needs to be responsible for curating the list of operations - any suggestions on how I achieve this?

It’s not a server configurable feature so you’d need to filter it on the client like the /metadata page does with the filter feature.

E.g. if you already have APIs tagged, you could redirect to #tag=tagname by default to only show those APIs.

The latest v6.0.3 on MyGet now avoids this large payload issue by avoiding JSONP and embedding the API Response inline to load the metadata.

1 Like

That’s awesome - thanks so much!

If it’s still useful, I’ve just added the ability to restrict APIs that gets returned in the App Metadata & UI with the queryString:

/ui/?IncludeTypes={tag}

This follows the Include Types pattern where you can view multiple tag groups with:

/ui/?IncludeTypes={tag1},{tag2}

Yes, that should be very useful - thanks for the heads up!

1 Like