Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GetDataStreamAsync throws JSON value could not be converted to Elastic.Clients.Elasticsearch.HealthStatus #7236

Closed
no1melman opened this issue Feb 16, 2023 · 12 comments · Fixed by #7253
Labels
8.x Relates to 8.x client version
Milestone

Comments

@no1melman
Copy link

Elastic.Clients.Elasticsearch version: 8.0.5

Elasticsearch version: 8.6.0

.NET runtime version: 6

Operating system version: macOS Monetery : 12.6.3

Description of the problem including expected versus actual behavior:

After a CreateDataStreamAsync and the process has finished, I re-spin up the process and it executes GetDataStreamAsync which then throws this exception

The JSON value could not be converted to Elastic.Clients.Elasticsearch.HealthStatus. Path: $.data_streams[0].status | LineNumber: 18 | BytePositionInLine: 24.

of type: UnexpectedTransportException

Steps to reproduce:

module EsDataStream =
    let dataStreamExists (getClient: unit -> ElasticsearchClient) (logIndex: string) (cancellationToken: CancellationToken) = task {
        let client = getClient ()
        let dsRequest = GetDataStreamRequest(logIndex)
        let! exists = client.Indices.GetDataStreamAsync(dsRequest, cancellationToken)
        
        return if not exists.IsValidResponse || exists.DataStreams.Count <> 0 then false
                   else exists.DataStreams.Count = 1 // = is equiv to ==
    }
    
    let dataStreamCreate (getClient: unit -> ElasticsearchClient) (logIndex: string) (cancellationToken: CancellationToken) = task {
        let client = getClient ()
        let request = CreateDataStreamRequest(logIndex)
        let! response = client.Indices.CreateDataStreamAsync(request, cancellationToken)
        return response.IsValidResponse && response.Acknowledged
    }

These are used like so:

let! result =
    EsDataStream.dataStreamExists getClient logIndex cancellationToken
    |> Task.bind (fun result ->
        if not result then EsDataStream.dataStreamCreate getClient logIndex cancellationToken
        else Task.FromResult(true))
    
return result

All that function basically does, is if the result from dataStreamExists is false, then it will fire off dataStreamCreate.

The exception gets thrown from the GetDataStreamAsync

Elastic.Transport.UnexpectedTransportException: The JSON value could not be converted to Elastic.Clients.Elasticsearch.HealthStatus. Path: $.data_streams[0].status | LineNumber: 18 | BytePositionInLine: 24.
---> System.Text.Json.JsonException: The JSON value could not be converted to Elastic.Clients.Elasticsearch.HealthStatus. Path: $.data_streams[0].status | LineNumber: 18 | BytePositionInLine: 24.
at Elastic.Clients.Elasticsearch.ThrowHelper.ThrowJsonException(String message) in //src/Elastic.Clients.Elasticsearch/Core/Exceptions/ThrowHelper.cs:line 14
at Elastic.Clients.Elasticsearch.HealthStatusConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options) in /
/src/Elastic.Clients.Elasticsearch/Generated/Types/Enums/Enums.NoNamespace.g.cs:line 480
at System.Text.Json.Serialization.JsonConverter1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.JsonConverter1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonCollectionConverter2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value) at System.Text.Json.Serialization.JsonConverter1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.Metadata.JsonPropertyInfo1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader) at System.Text.Json.Serialization.Converters.ObjectDefaultConverter1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.JsonConverter1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase) at System.Text.Json.JsonSerializer.ContinueDeserialize[TValue](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonConverter converter, JsonSerializerOptions options) at System.Text.Json.JsonSerializer.ReadAllAsync[TValue](Stream utf8Json, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) at Elastic.Transport.DefaultResponseBuilder1.SetBodyAsync[TResponse](ApiCallDetails details, RequestData requestData, Stream responseStream, String mimeType, CancellationToken cancellationToken)
at Elastic.Transport.DefaultResponseBuilder1.ToResponseAsync[TResponse](RequestData requestData, Exception ex, Nullable1 statusCode, Dictionary2 headers, Stream responseStream, String mimeType, Int64 contentLength, IReadOnlyDictionary2 threadPoolStats, IReadOnlyDictionary2 tcpStats, CancellationToken cancellationToken) at Elastic.Transport.HttpTransportClient.RequestAsync[TResponse](RequestData requestData, CancellationToken cancellationToken) at Elastic.Transport.DefaultRequestPipeline1.CallProductEndpointAsync[TResponse](RequestData requestData, CancellationToken cancellationToken)
at Elastic.Transport.DefaultHttpTransport1.RequestAsync[TResponse](HttpMethod method, String path, PostData data, RequestParameters requestParameters, CancellationToken cancellationToken) --- End of inner exception stack trace --- at Elastic.Transport.DefaultHttpTransport1.RequestAsync[TResponse](HttpMethod method, String path, PostData data, RequestParameters requestParameters, CancellationToken cancellationToken)
at Elastic.Clients.Elasticsearch.ElasticsearchClient.<>c__DisplayClass32_0`3.<g__SendRequest|0>d.MoveNext() in /
/src/Elastic.Clients.Elasticsearch/Client/ElasticsearchClient.cs:line 374
--- End of stack trace from previous location ---
at EsLogging.EsDataStream.dataStreamExists@36.MoveNext()

Expected behavior

What should happen is the response gets serialised properly

@no1melman no1melman added the 8.x Relates to 8.x client version label Feb 16, 2023
@no1melman
Copy link
Author

no1melman commented Feb 16, 2023

The output from the DevTools is:

Executing: GET _data_stream/logs-dev?pretty=true&error_trace=true

{
  "data_streams": [
    {
      "name": "logs-dev",
      "timestamp_field": {
        "name": "@timestamp"
      },
      "indices": [
        {
          "index_name": ".ds-logs-dev-2023.02.16-000001",
          "index_uuid": "xyWXN5T1Rm6_sOCayv7GDA"
        }
      ],
      "generation": 1,
      "_meta": {
        "description": "default logs template installed by x-pack",
        "managed": true
      },
      "status": "GREEN",
      "template": "logs",
      "ilm_policy": "logs",
      "hidden": false,
      "system": false,
      "allow_custom_routing": false,
      "replicated": false
    }
  ]
}

@no1melman
Copy link
Author

The auto gen'd code is:

[JsonConverter(typeof(HealthStatusConverter))]
public enum HealthStatus
{
	[EnumMember(Value = "yellow")]
	Yellow,
	[EnumMember(Value = "red")]
	Red,
	[EnumMember(Value = "green")]
	Green
}

internal sealed class HealthStatusConverter : JsonConverter<HealthStatus>
{
	public override HealthStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		var enumString = reader.GetString();
		switch (enumString)
		{
			case "yellow":
				return HealthStatus.Yellow;
			case "red":
				return HealthStatus.Red;
			case "green":
				return HealthStatus.Green;
		}

		ThrowHelper.ThrowJsonException();
		return default;
	}

	public override void Write(Utf8JsonWriter writer, HealthStatus value, JsonSerializerOptions options)
	{
		switch (value)
		{
			case HealthStatus.Yellow:
				writer.WriteStringValue("yellow");
				return;
			case HealthStatus.Red:
				writer.WriteStringValue("red");
				return;
			case HealthStatus.Green:
				writer.WriteStringValue("green");
				return;
		}

		writer.WriteNullValue();
	}
}

@no1melman
Copy link
Author

The casing shouldn't be a problem... but the code doesn't seem to support String.Equals(...,..., StringComparison.OrdinalIgnoreCase)... maybe by design? but then, this probably caused the issue

@stevejgordon
Copy link
Contributor

Thanks for raising this, @no1melman, and for the comprehensive analysis. I'll discuss this with the team as this is not something we capture in the specification, which assumes that all uses of HealthStatus will return the lowercase value. We'll need to identify a way to address this for code generation.

@no1melman
Copy link
Author

Also, @stevejgordon , I set EnableDebugMode() which I assumed would write out the response serialised to string? Or do I have that confused

@stevejgordon
Copy link
Contributor

When enabled, the request/response should appear in the DebugInformation property of the response.

@stevejgordon
Copy link
Contributor

We do handle this inconsistent casing for HealthStatus in the spec, but the .NET generator doesn't apply the aliases. We can hopefully solve this by updating the converter code gen for enums to include the aliases when available. We'll aim to get this fixed in the next release.

@stevejgordon stevejgordon added this to the 8.0.6 milestone Feb 16, 2023
@no1melman
Copy link
Author

When enabled, the request/response should appear in the DebugInformation property of the response.

Yeah this doesn't do this for some reason - let me have a more of an investigation - this is obviously really useful for debugging this kind of thing, rather than going round the houses

@no1melman
Copy link
Author

Ahh - maybe because of this JSON format exception - maybe you don't back track, and reserialise the stream as a string for the exception.

psuedo

try {
   T model = JsonSerializer.Deserialize(stream)
catch JsonEx {
   DebugInformation.Add(stream.ToString()) // get the raw response out.
}

@stevejgordon
Copy link
Contributor

@no1melman, I see what you mean. Yes, if an exception is thrown this low down, we don't collect and attach the response body anywhere. That's something we can look at in the future if it's practical.

@no1melman
Copy link
Author

It becomes quite the chore to debug if the JSON fails and we can't see what the original string response was to work out where in could have possibly failed

@stevejgordon
Copy link
Contributor

I agree. In this case, this is an exception in deserialising our type, so it shouldn't be up to you to debug it. For source-related exceptions, this would be far more valid for debugging. I've captured this on our roadmap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
8.x Relates to 8.x client version
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants