reactiveui/refit · error · ArgumentException

Method "{methodInfo.Name}" is invalid. All REST Methods must

Error message

Method "{methodInfo.Name}" is invalid. All REST Methods must return either Task<T> or ValueTask<T> or IObservable<T>

What it means

Thrown by DetermineReturnTypeInfo when a public, top-level Refit interface method has a return type that is not an async-compatible shape (Task<T>, ValueTask<T>, IObservable<T>, IAsyncEnumerable<T>, or a non-generic Task) and is not handled by a registered return-type adapter. Refit needs an awaitable/observable return so it can drive the HTTP pipeline; a bare synchronous return (e.g. string, int, void) has no way to surface the async HTTP work.

Source

Thrown at src/Refit.Reflection/RestMethodInfoInternal.cs:465

        if (adapterResultType is not null)
        {
            return (returnType, adapterResultType, DetermineDeserializedResultType(adapterResultType));
        }

        // Allow synchronous return types only for methods that are implemented by generated stubs
        // (for example explicit/default interface implementations). Public top-level Refit methods must
        // still use async-compatible return shapes.
#if NET8_0_OR_GREATER        
        var isExplicitInterfaceMember = methodInfo.Name.Contains('.');
#else
        var isExplicitInterfaceMember = methodInfo.Name.Contains(".");
#endif

        var isNonPublic = !methodInfo.IsPublic;

        if (!isExplicitInterfaceMember && !isNonPublic)
        {
            throw new ArgumentException(
                $"Method \"{methodInfo.Name}\" is invalid. All REST Methods must return either Task<T> or ValueTask<T> or IObservable<T>");
        }

        return (returnType, returnType, DetermineDeserializedResultType(returnType));
    }

    /// <summary>Determines the type that response content is deserialized into for the given result type.</summary>
    /// <param name="returnResultType">The result type wrapped by the return type.</param>
    /// <returns>The type to deserialize response content into.</returns>
    internal static Type DetermineDeserializedResultType(Type returnResultType)
    {
        if (
            returnResultType.IsGenericType
            && (
                returnResultType.GetGenericTypeDefinition() == typeof(ApiResponse<>)
                || returnResultType.GetGenericTypeDefinition() == typeof(IApiResponse<>)
            )
        )

View on GitHub (pinned to b455f65ecc)

Solutions

  1. Wrap the return type in Task<T> (or ValueTask<T>), e.g. change `User GetUser()` to `Task<User> GetUser()`.
  2. For streaming/reactive scenarios use IObservable<T> or IAsyncEnumerable<T>.
  3. If you genuinely need the raw response, use Task<HttpResponseMessage> or Task<IApiResponse<T>> (these are Task<>, so they are accepted).
  4. For custom async return shapes, register an IReturnTypeAdapter in RefitSettings so adapterResultType is populated.

Example fix

// before
public interface IApi
{
    [Get("/users/{id}")]
    User GetUser(int id);
}

// after
public interface IApi
{
    [Get("/users/{id}")]
    Task<User> GetUser(int id);
}
Defensive patterns

Strategy: type-guard

Type guard

// Reject interfaces whose Refit methods lack an async-compatible return.
static bool IsRefitAsyncReturn(Type t) =>
    t == typeof(Task) ||
    (t.IsGenericType && t.GetGenericTypeDefinition() is var g &&
        (g == typeof(Task<>) || g == typeof(ValueTask<>) ||
         g == typeof(IObservable<>) || g == typeof(IAsyncEnumerable<>)));

foreach (var m in typeof(IMyApi).GetMethods())
    if (!IsRefitAsyncReturn(m.ReturnType))
        throw new InvalidOperationException($"{m.Name} must return Task<T>/ValueTask<T>/IObservable<T>/IAsyncEnumerable<T>");

Prevention

When it happens

Trigger: Declaring a Refit interface method that returns a non-async type, e.g. `string GetUser()` or `void Delete()` or `HttpResponseMessage Get()` (HttpResponseMessage alone is not awaitable). The check is skipped for explicit interface members and non-public methods (generated stubs), so it only hits public declared methods.

Common situations: Newcomer mistake of returning the deserialized type directly instead of wrapping it in Task<T>; migrating from HttpClient.GetAwaiter patterns; forgetting ValueTask; a method returning Task (non-generic) when content is expected — note plain `Task` IS allowed and returns void, so the real trigger is a truly synchronous signature.

Related errors


AI-assisted analysis of reactiveui/refit@b455f65ecc (2026-08-13). Data as JSON: /api/errors/fa80dd9441c42bd4. Report an issue: GitHub.