refit: Responses with a 204 status code throw when deserializing content.

When using Refit with System.Text.Json and the SystemTextJsonContentSerializer the following error comes up: Refit.ApiException: An error occured deserializing the response. —> System.Text.Json.JsonException: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0. —> System.Text.Json.JsonReaderException: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. LineNumber: 0 | BytePositionInLine: 0. at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan1 bytes) at System.Text.Json.Utf8JsonReader.Read() at System.Text.Json.Serialization.JsonConverter1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)

This happens when the API returns StatusCodeResult (eg. return NoContent(), return StatusCode(204). When returning StatusCodeResult the HttpContentHeaders in HttpContent are empty and the MediaTypeHeaderValue is null and the content itself has no content to deserialize and an exception in System.Text.Json is thrown. image

My Suggestion is to change the FromHttpContentAsync method to: public async Task<T?> FromHttpContentAsync<T>(HttpContent content, CancellationToken cancellationToken = default) { if(content.Headers.ContentType != null && content.Headers.ContentType.MediaType == "application/json") { var item = await content.ReadFromJsonAsync<T>(jsonSerializerOptions, cancellationToken).ConfigureAwait(false); return item; } return default; }

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 15
  • Comments: 24 (2 by maintainers)

Commits related to this issue

Most upvoted comments

We’re encountering the same issue on 204 requests. Is there a (temporary) workaround possible?

What I did as workaround so far: I created a ‘CustomContentSerializer’, implement the IHttpContentSerializer and in the FromHttpContentAsync method I added the check. Here is a sample of how you could implement a Serializer on your own (the implementation is from Refit, I just added the additional check):

`

 public class CustomContentSerializer : IHttpContentSerializer 

    private readonly JsonSerializerOptions jsonSerializerOptions;

    public CustomContentSerializer() : this(new JsonSerializerOptions(JsonSerializerDefaults.Web))
    {
        jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
        jsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
        jsonSerializerOptions.WriteIndented = true;
        //jsonSerializerOptions.IgnoreNullValues = true;
        jsonSerializerOptions.PropertyNameCaseInsensitive = true;
        jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
        jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter());
        jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
    }

    public CustomContentSerializer(JsonSerializerOptions jsonSerializerOptions)
    {
        this.jsonSerializerOptions = jsonSerializerOptions;
    }

    public async Task<T?> FromHttpContentAsync<T>(HttpContent content, CancellationToken cancellationToken = default)
    {
      // this needs to be added
        if(content.Headers.ContentType != null && content.Headers.ContentType.MediaType == "application/json")
        {
            var item = await content.ReadFromJsonAsync<T>(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
            return item;
        }

        return default;
    }

    public string? GetFieldNameForProperty(PropertyInfo propertyInfo)
    {
        if(propertyInfo is null)
        {
            throw new ArgumentNullException(nameof(propertyInfo));
        }

        return propertyInfo.GetCustomAttributes<JsonPropertyNameAttribute>(true)
                   .Select(a => a.Name)
                   .FirstOrDefault();
    }

    public HttpContent ToHttpContent<T>(T item)
    {
        var content = JsonContent.Create(item, options: jsonSerializerOptions);
        return content;
    }

    // From https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0#deserialize-inferred-types-to-object-properties
    public class ObjectToInferredTypesConverter
       : JsonConverter<object>
    {
        public override object? Read(
          ref Utf8JsonReader reader,
          Type typeToConvert,
          JsonSerializerOptions options) => reader.TokenType switch
          {
              JsonTokenType.True => true,
              JsonTokenType.False => false,
              JsonTokenType.Number when reader.TryGetInt64(out var l) => l,
              JsonTokenType.Number => reader.GetDouble(),
              JsonTokenType.String when reader.TryGetDateTime(out var datetime) => datetime,
              JsonTokenType.String => reader.GetString(),
              _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
          };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
    }
}

`

And in Startup.cs: services.AddHttpClient("CoilDNATenantClient", c => c.BaseAddress = new Uri(Configuration["Endpoints:Tenant"])) .AddTypedClient(c => RestService.For<ITenantClient>(c, new RefitSettings { //https://github.com/reactiveui/refit/blob/main/Refit/SystemTextJsonContentSerializer.cs //ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web) //{ // IgnoreNullValues = true, // ReferenceHandler = ReferenceHandler.Preserve, // WriteIndented = true //}) ContentSerializer = new CustomContentSerializer() }));

Unfortunately this fix is more complicated than here. We can’t return null or default for non-nullable return types. A 204 returns null, effectively. That means we have to detect if the return type is non-nullable or nullable so we can either return null or throw.

Another workaround is to use ApiResponse<> or HttpResponseMessage return types where you can check the status before deserializing the object.

Workaround when an API send 204 no content.

Add a try catch around the refit call. try { // Refit call goes here. } catch (Refit.ApiException ex) when (ex.StatusCode == HttpStatusCode.NoContent) { // Do something. Ex return empty/null/empty array }

Came across this issue today. Does anyone know if this will be addressed?

We also hit the same issue when we updated our library.

Any ETA when this will be fixed? It’s a showstopper for us.

We’re encountering the same issue on 204 requests. Is there a (temporary) workaround possible?

Yeah I keep having to initialize clients with serializers working around this issue; it’s pretty annoying.

It seems like that the issue still exists in Version 6.3.2. To solve, I think the method From HttpContentAsync should lool like the following:

`

 public async Task<T?> FromHttpContentAsync<T>(HttpContent content, CancellationToken cancellationToken = default)
{
       return content.Headers.ContentType?.MediaType == "application/json"
             ? await content.ReadFromJsonAsync<T>(jsonSerializerOptions, cancellationToken).ConfigureAwait(false)
              : default;
}

`

When deserializing NoContent or HTTPStatusCodeResult 204 the HttpContentHeaders in HttpContent are empty and the MediaTypeHeaderValue is null. The content itself has no content to deserialize and an exception in System.Text.Json is thrown.

Also interested in a fix for this. Would expect Task<T?> nullable to work for 204. 204 would return null and 200 return T.

Thanks for the help and quick response!

Refit version 7.0.0 (at the date, last release 4 months ago) still the same issue 😦 Notice that .net 8 has been released yesterday.

any progress on this issue? this is quite a blocker for 204 responses

Thank you @jagnarock your workaround was helpful

I have find a way to correct this problem by detecting on my custom httpClientHandler if the content received was null or a string.Empty, on those cases i transformed it to a JSON null object, that would be a string “null”, this came as a correction because of .NET 5 upgrade, that it wont give you a null content never, just an empty string.

if (string.IsNullOrEmpty(response.Content?.ReadAsStringAsync(ct).Result)) { response.Content = new StringContent( "null", Encoding.UTF8, MediaTypeNames.Application.Json); }