Skip to content

AbpRemoteServiceApiDescriptionProvider.GetApiResponseTypes() corrupts shared singleton List under concurrency → "Source array was not long enough" on /Abp/ServiceProxyScript #25536

@HamzaSallakh

Description

@HamzaSallakh

Is there an existing issue for this?

  • I have searched the existing issues

Description

AbpRemoteServiceApiDescriptionProvider.GetApiResponseTypes() appears to mutate shared response type metadata on every API-description rebuild.

Specifically, it iterates over AbpRemoteServiceApiDescriptionProviderOptions.SupportedResponseTypes and appends to each ApiResponseType.ApiResponseFormats list:

apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
{
    Formatter = (IOutputFormatter)responseTypeMetadataProvider,
    MediaType = formatterSupportedContentType
});

AbpRemoteServiceApiDescriptionProviderOptions is provided through IOptions<T>.Value, so the configured SupportedResponseTypes collection is process-wide shared state. As a result, each ApiResponseFormats list is reused across requests and API-description rebuilds.

This creates two problems:

  1. The list grows repeatedly because formats are appended on every call and are never cleared or deduplicated.
  2. Under concurrent requests, multiple threads can call List<T>.Add(...) on the same shared List<>, which can corrupt the list during resize.

Offending method on dev: https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs

Why this runs repeatedly and concurrently

ApiDescriptionGroupCollectionProvider.ApiDescriptionGroups is cached against ActionDescriptors.Version, but two things defeat the "runs once" assumption:

  1. The cache is invalidated whenever any IActionDescriptorChangeProvider fires (e.g. Razor runtime compilation's file watcher, Hot Reload, or any dynamic controller registration). Each invalidation bumps the version and forces a full rebuild, which re-runs GetApiResponseTypes() and appends to the shared lists again.
  2. The ApiDescriptionGroups getter itself is not synchronized. When the cache is stale under load, multiple in-flight requests all enter GetCollection(...) at the same time, so several threads call List<T>.Add(...) on the same shared ApiResponseFormats instances simultaneously — corrupting the list during an internal resize.

This is why the failure appears only after the app has been running for a while (an invalidation has to coincide with concurrent load) and why a restart temporarily fixes it (the singleton options object is recreated empty).

In production, this causes /Abp/ServiceProxyScript and /Abp/ServiceProxyScript?type=odata to start returning 500 errors. Restarting the application temporarily fixes the issue because the singleton options instance is recreated, but the same failure happens again later under concurrent load.

The observed exception is:

System.ArgumentException: Source array was not long enough. Check the source index, length, and the array's lower bounds. (Parameter 'sourceArray')

Relevant stack trace:

System.Array.Copy(...)
System.Collections.Generic.List`1.set_Capacity(Int32 value)
System.Collections.Generic.List`1.AddWithResize(T item)
Volo.Abp.AspNetCore.Mvc.ApiExploring.AbpRemoteServiceApiDescriptionProvider.GetApiResponseTypes()
Volo.Abp.AspNetCore.Mvc.ApiExploring.AbpRemoteServiceApiDescriptionProvider.OnProvidersExecuting(...)
Microsoft.AspNetCore.Mvc.ApiExplorer.ApiDescriptionGroupCollectionProvider.GetCollection(...)
Volo.Abp.AspNetCore.Mvc.AspNetCoreApiDescriptionModelProvider.CreateApiModel(...)
Volo.Abp.Http.ProxyScripting.ProxyScriptManager.CreateScript(...)
Volo.Abp.AspNetCore.Mvc.ProxyScripting.AbpServiceProxyScriptController.GetAll(...)
```text
System.ArgumentException: Source array was not long enough. (Parameter 'sourceArray')

Expected behavior

/Abp/ServiceProxyScript and related dynamic proxy endpoints should be safe under concurrent requests.

GetApiResponseTypes() should not repeatedly append duplicate response formats to shared singleton state. Concurrent requests should not corrupt ApiResponseFormats, and the application should not require a restart to recover.

API response format metadata should be initialized once in a thread-safe way. Alternative acceptable approaches:

  • cloned per API-description build,
  • cleared before rebuilding,
  • deduplicated safely,
  • or protected by synchronization.

Actual behavior

GetApiResponseTypes() appends response formats to the shared ApiResponseFormats lists on every call.

Under concurrent requests, multiple threads can mutate the same List<ApiResponseFormat> at the same time. When this happens during internal list resizing, the list becomes corrupted and throws:

System.ArgumentException: Source array was not long enough. (Parameter 'sourceArray')

After this happens, /Abp/ServiceProxyScript returns 500 errors repeatedly. The application only recovers after restarting the process.

Version

  • ABP: 9.3.3 (the same code path appears to still exist in the current dev / 10.x branch).
  • .NET: 9.0 (net9.0).
  • UI: ASP.NET Core MVC (not Blazor), dynamic JavaScript proxies enabled.

Regression?

No response

Known Workarounds

Suggested Fix

Avoid mutating shared singleton ApiResponseType.ApiResponseFormats collections during every API-description build.

The fix we are proposing in the attached PR: build the response formats once, clearing each ApiResponseFormats first, guarded by a lock so the shared lists are never grown or mutated concurrently. Once built, the shared response types are only read, so concurrent access is safe.

Alternative approaches that would also resolve it:

  • Clear or deduplicate ApiResponseFormats before adding formats.
  • Return cloned ApiResponseType instances from GetApiResponseTypes() so per-request/API-description-build mutation does not affect shared options state.
  • Replace mutable shared List<> usage with immutable or thread-safe response metadata.

Version

9.3.3

User Interface

MVC

Database Provider

EF Core (Default)

Tiered or separate authentication server

None (Default)

Operation System

Windows (Default)

Other information

No response

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions