How to correctly handle 401 from jsonapiclient?

We’re building a blazor was application and found that when using the jsonapiclient to make an unauthenticated request to a [Authenticate] endpoint we don’t get a populated response back, despite having a ResponseStatus property on our dto. Is this expected behaviour and if so what is the best method for capturing the 404 from the client.ApiAsync request?

I was thinking we’d get an error of some kind in the response’s Error property…?

It’s not expected for a failed response not to have a populated Error property, it’s also not the behavior I’m seeing which returns the expected Error Response populated in the Error property:

public class DoesNotExist : IReturn<EmptyResponse> { }

var client = new JsonApiClient("https://blazor-server.jamstacks.net");
var api = await client.ApiAsync(new DoesNotExist());
Console.WriteLine($"{api.Succeeded}: {api.Error?.ErrorCode} {api.Error?.Message}");

Output:

False: NotImplementedException The operation 'DoesNotExist' does not exist for this service

So something’s off, if you can create a stand-alone repro I can investigate.

Do you mind if I give you temporary access to a private repo instead?

No we never want access to any confidential info, needs to be a stand-alone repro, ideally starting from the same empty project template your App uses.

I understand, have tested it in the starter project we used - jamstacks blazor was tailwind and can’t recreate it.

In our application we’re calling ApiAsync from within a fluxor effect so will try adding fluxor into an initialised app on the same starter project

I’ve changed approach and tested HelloSecure from the fluxor effect and that generates an Error as expected, but our endpoint doesn’t.

So the out-of-the-box DTO works…

[Route("/hellosecure/{Name}")]
[ValidateIsAuthenticated]
public class HelloSecure : IReturn<HelloResponse>
{
    public string Name { get; set; }
}

public class HelloResponse
{
    public string Result { get; set; }
    public ResponseStatus ResponseStatus { get; set; }
}

but our DTO doesn’t…

[Route("/systems", "GET")]
[ValidateIsAuthenticated]
public class GetAllSystems : IGet, IReturn<SystemsResponse>
{
}

public class SystemsResponse
{
    public List<Types.System> Results { get; set; }

    public ResultStats ResultStats { get; set; }

    public ResponseStatus ResponseStatus { get; set; }
}

Are you able to suggest any areas of further investigation?

Make sure the API is returning the expected error (i.e. non 200) HTTP Response. Try debug into the framework code, seems impossible for it not to return an exception, all it’s doing is calling SendAsync then creating an Error ApiResult from the Exception:

You can try calling SendAsync directly and making sure that throws an Exception.

Just realised - I’ve been talking about 404 error codes when it’s 401s I’m trying to receive and handle. Will edit the title

The service is returning the expected 401s
image

Calling SendAsync throws the exception as expected, whereas using ApiAsync to request Authenticate and Systems doesn’t


The only property returned from ApiAsync (for the Authenticate and Systems requests) is the StackTrace…

   at ServiceStack.JsonApiClient.ThrowWebServiceException[ErrorResponse](HttpResponseMessage httpRes, Object request, String requestUri, Object response) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack/src/ServiceStack.Client/JsonApiClient.cs:line 925
   at ServiceStack.JsonApiClient.ThrowResponseTypeException[String](HttpResponseMessage httpRes, Object request, String requestUri, Object response) in /home/runner/work/ServiceStack/ServiceStack/ServiceStack/src/ServiceStack.Client/Js…se, Fusion.ServiceModel, Version=2.0.8343.24090, Culture=neutral, PublicKeyToken=null]].MoveNext() in /home/runner/work/ServiceStack/ServiceStack/ServiceStack/src/ServiceStack.Client/JsonApiClient.cs:line 264
   at ServiceStack.ApiResultUtils.<ApiAsync>d__4`1[[Fusion.ServiceModel.SystemsResponse, Fusion.ServiceModel, Version=2.0.8343.24090, Culture=neutral, PublicKeyToken=null]].MoveNext() in /home/runner/work/ServiceStack/ServiceStack/ServiceStack/src/ServiceStack.Client/ApiResult.cs:line 227

Does it populate Error if you populate the ApiResult yourself?

var api = e.ToApiResult<TResponse>();

Nope, I get the same result…


Sounds like a WASM code-gen issue where something is changing the behavior of ToApiResult or the Exception is missing information.

Here is the implementation of the ToApiResult method from the ApiResult.cs class:

public static ApiResult<TResponse> ToApiResult<TResponse>(this Exception ex) => ApiResult.CreateError<TResponse>(ex);

//...
public static ApiResult<TResponse> CreateError<TResponse>(Exception ex) => new(ErrorUtils.CreateError(ex)) { StackTrace = ex.StackTrace };`

//...
public static ResponseStatus CreateError(Exception ex) => ex switch {
    IResponseStatusConvertible rsc => rsc.ToResponseStatus(),
    IHasResponseStatus hasStatus => hasStatus.ResponseStatus,
    ArgumentException { ParamName: { } } ae => CreateFieldError(ae.ParamName, ex.Message, ex.GetType().Name),
    _ => CreateError(ex.Message, ex.GetType().Name)
};

//...
public static ResponseStatus CreateFieldError(string fieldName, string errorMessage,
    string errorCode = FieldErrorCode) =>
    new ResponseStatus().AddFieldError(fieldName, errorMessage, errorCode);

//...
public static ResponseStatus AddFieldError(this ResponseStatus status, string fieldName, string errorMessage,
    string errorCode = FieldErrorCode)
{
    var fieldError = status.FieldError(fieldName);
    if (fieldError != null)
    {
        fieldError.ErrorCode = errorCode;
        fieldError.Message = errorMessage;
    }
    else
    {
        status.Errors ??= new List<ResponseError>();
        status.Errors.Add(new ResponseError {
            FieldName = fieldName,
            Message = errorMessage,
            ErrorCode = errorCode,
        });
    }
    return status;
}

Maybe you can get it to work if you populate it manually.

Its odd how HelloSecure works as expected.

I’ve recreated it using Authenticate on the starter project.

Here’s the repro

I placed the repro code in OnInitializedAsync() in Shared/MainLayout.razor

So if I remove the [ValidateIsAuthenticated] attribute from the DTO and add [Authenticate] to the service method instead HelloSecure then starts exhibiting the same behaviour.

[Route("/hellosecure/{Name}")]
// [ValidateIsAuthenticated]
public class HelloSecure : IReturn<HelloResponse>
{
    public string Name { get; set; }
}

public class MyServices : Service
{
    public static string AssertName(string Name) => Name.IsNullOrEmpty() 
        ? throw new ArgumentNullException(nameof(Name))
        : Name;

    public object Any(Hello request) =>
        new HelloResponse { Result = $"Hello, {AssertName(request.Name)}!" };

    [Authenticate]
    public object Any(HelloSecure request) => 
        new HelloResponse { Result = $"Hello, {AssertName(request.Name)}!" };
}

image

This should hopefully be resolved with the latest v6.4.1+ that’s now available on MyGet.

To minimize breaking changes of ServiceStackStateProvider in future we’ve condensed the constructor down to:

public class ServiceStackStateProvider : BlazorWasmAuthenticationStateProvider
{
    public ServiceStackStateProvider(BlazorWasmAuthContext context, ILogger<BlazorWasmAuthenticationStateProvider> log)
        : base(context, log) { }
}

Which will allow us to add additional dependencies in future without breaking the constructor again.

1 Like

Confirmed: 6.4.1 has fixed it! Thanks for your help

1 Like