Skip to content

Kiota throws ApiException when server returns 304 #4190

@bkoelman

Description

@bkoelman

Our server returns HTTP 304 (Not Modified) without a response body when the incoming fingerprint in the If-None-Match header matches the stored version. This enables clients to poll for changes without receiving a response body, unless it has changed since it was last fetched.

Status 304 is described in the OAS document, but Kiota throws an ApiException saying the error isn't mapped:

Microsoft.Kiota.Abstractions.ApiException: The server returned an unexpected status code and no error factory is registered for this code: 304

OAS fragment:

{
  "paths": {
    "/api/people": {
      "get": {
        "tags": [
          "people"
        ],
        "summary": "Retrieves a collection of people.",
        "operationId": "getPersonCollection",
        "responses": {
          "200": {
            "description": "Successfully returns the found people, or an empty array if none were found.",
            "headers": {
              "ETag": {
                "description": "A fingerprint of the HTTP response, which can be used in an If-None-Match header to only fetch changes.",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/personCollectionResponseDocument"
                }
              }
            }
          },
          "400": {
            "description": "The query string is invalid.",
            "content": {
              "application/vnd.api+json": {
                "schema": {
                  "$ref": "#/components/schemas/errorResponseDocument"
                }
              }
            }
          },
          "304": {
            "description": "The fingerprint of the HTTP response matches one of the ETags from the incoming If_None_Match header.",
            "headers": {
              "ETag": {
                "description": "A fingerprint of the HTTP response, which can be used in an If-None-Match header to only fetch changes.",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}

Generated C# code:

namespace GeneratedClient.Api.People {
    public class PeopleRequestBuilder : BaseRequestBuilder {
        /// <summary>
        /// Retrieves a collection of people.
        /// </summary>
        /// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
        /// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public async Task<PersonCollectionResponseDocument?> GetAsync(Action<RequestConfiguration<PeopleRequestBuilderGetQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default) {
#nullable restore
#else
        public async Task<PersonCollectionResponseDocument> GetAsync(Action<RequestConfiguration<PeopleRequestBuilderGetQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default) {
#endif
            var requestInfo = ToGetRequestInformation(requestConfiguration);
            var errorMapping = new Dictionary<string, ParsableFactory<IParsable>> {
                {"400", ErrorResponseDocument.CreateFromDiscriminatorValue},
            };
            return await RequestAdapter.SendAsync<PersonCollectionResponseDocument>(requestInfo, PersonCollectionResponseDocument.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
        }
    }
}

To work around this, we need to catch an exception to detect HTTP 304 is being returned:

var headerInspector = new HeadersInspectionHandlerOption
{
    InspectResponseHeaders = true
};

PersonCollectionResponseDocument? getResponse1 = await client.Api.People.GetAsync(
    configuration => configuration.Options.Add(headerInspector));

string eTag = headerInspector.ResponseHeaders["ETag"].Single();

try
{
    PersonCollectionResponseDocument? getResponse2 = await client.Api.People.GetAsync(
        configuration => configuration.Headers.Add("If-None-Match", eTag));
}
catch (ApiException exception) when (exception.ResponseStatusCode == (int)HttpStatusCode.NotModified)
{
    Console.WriteLine("The HTTP response hasn't changed, so no response body was returned.");
}

This looks like a bug to me. In KiotaBuilder.cs, I found the following line:

    private static readonly HashSet<string> noContentStatusCodes = new(StringComparer.OrdinalIgnoreCase) { "201", "202", "204", "205" };

Should that just be changed to include 304?

Metadata

Metadata

Assignees

Labels

WIPenhancementNew feature or requestgeneratorIssues or improvements relater to generation capabilities.

Type

No type

Projects

Status

Done ✔️

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions