Skip to content

Commit

Permalink
Fix meta header and support rest api compatibility (#59)
Browse files Browse the repository at this point in the history
* Fix meta header client version detection

* Support default mime type and ES rest API compatibility
  • Loading branch information
stevejgordon authored Dec 13, 2022
1 parent 8682b3d commit 88a4b31
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,12 @@ IReadOnlyDictionary<TcpState, int> tcpStats
.StatusCodeToResponseSuccess(requestData.Method, statusCode.Value);
}

// TODO: Perf - avoid extra allocation by comparing spans.
var trimmedMimeType = mimeType?.Replace(" ", "");
var trimmedAccept = requestData.Accept.Replace(" ", "");

//mimeType can include charset information on .NET full framework
if (!string.IsNullOrEmpty(mimeType) && !mimeType.StartsWith(requestData.Accept))
if (!string.IsNullOrEmpty(trimmedMimeType) && !trimmedMimeType.StartsWith(trimmedAccept))
success = false;

var details = new ApiCallDetails
Expand Down Expand Up @@ -191,7 +195,11 @@ private TResponse SetBody<TResponse>(ApiCallDetails details, RequestData request
return response;
}

return mimeType == null || !mimeType.StartsWith(requestData.Accept, StringComparison.Ordinal)
// TODO: Perf - avoid extra allocation by comparing spans.
var trimmedMimeType = mimeType?.Replace(" ", "");
var trimmedAccept = requestData.Accept.Replace(" ", "");

return trimmedMimeType == null || !trimmedMimeType.StartsWith(trimmedAccept, StringComparison.Ordinal)
? null
: serializer.Deserialize<TResponse>(responseStream);
}
Expand Down Expand Up @@ -283,7 +291,11 @@ CancellationToken cancellationToken
return response;
}

return mimeType == null || !mimeType.StartsWith(requestData.Accept, StringComparison.Ordinal)
// TODO: Perf - avoid extra allocation by comparing spans.
var trimmedMimeType = mimeType?.Replace(" ", "");
var trimmedAccept = requestData.Accept.Replace(" ", "");

return trimmedMimeType == null || !trimmedMimeType.StartsWith(trimmedAccept, StringComparison.Ordinal)
? default
: await serializer
.DeserializeAsync<TResponse>(responseStream, cancellationToken)
Expand Down
7 changes: 3 additions & 4 deletions src/Elastic.Transport/Components/Pipeline/RequestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ MemoryStreamFactory memoryStreamFactory

Pipelined = local?.EnableHttpPipelining ?? global.HttpPipeliningEnabled;
HttpCompression = global.EnableHttpCompression;
RequestMimeType = local?.ContentType ?? MimeType;
Accept = local?.Accept ?? MimeType;
ContentType = local?.ContentType ?? global.ProductRegistration.DefaultMimeType ?? MimeType;
Accept = local?.Accept ?? global.ProductRegistration.DefaultMimeType ?? MimeType;

if (global.Headers != null)
Headers = new NameValueCollection(global.Headers);
Expand Down Expand Up @@ -160,8 +160,7 @@ public Node Node
public string ProxyAddress { get; }
public string ProxyPassword { get; }
public string ProxyUsername { get; }
// TODO: rename to ContentType in 8.0.0
public string RequestMimeType { get; }
public string ContentType { get; }
public TimeSpan RequestTimeout { get; }
public string RunAs { get; }
public IReadOnlyCollection<int> SkipDeserializationForStatusCodes { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public RequestDataContent(RequestData requestData)
{
_requestData = requestData;
_token = default;
Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType);

Headers.TryAddWithoutValidation("Content-Type", requestData.ContentType);

if (requestData.HttpCompression)
Headers.ContentEncoding.Add("gzip");

Expand All @@ -61,7 +63,9 @@ public RequestDataContent(RequestData requestData, CancellationToken token)
{
_requestData = requestData;
_token = token;
Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType);

Headers.TryAddWithoutValidation("Content-Type", requestData.ContentType);

if (requestData.HttpCompression)
Headers.ContentEncoding.Add("gzip");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ private static HttpRequestMessage CreateRequestMessage(RequestData requestData)

requestMessage.Headers.Connection.Clear();
requestMessage.Headers.ConnectionClose = false;
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(requestData.Accept));
requestMessage.Headers.TryAddWithoutValidation("Accept", requestData.Accept);

var userAgent = requestData.UserAgent?.ToString();
if (!string.IsNullOrWhiteSpace(userAgent))
Expand Down Expand Up @@ -417,7 +417,7 @@ private static void SetContent(HttpRequestMessage message, RequestData requestDa
if (requestData.HttpCompression)
message.Content.Headers.ContentEncoding.Add("gzip");

message.Content.Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType);
message.Content.Headers.TryAddWithoutValidation("Content-Type", requestData.ContentType);
}
}

Expand Down Expand Up @@ -455,7 +455,7 @@ private static async Task SetContentAsync(HttpRequestMessage message, RequestDat
if (requestData.HttpCompression)
message.Content.Headers.ContentEncoding.Add("gzip");

message.Content.Headers.ContentType = new MediaTypeHeaderValue(requestData.RequestMimeType);
message.Content.Headers.TryAddWithoutValidation("Content-Type", requestData.ContentType);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ private static HttpWebRequest CreateWebRequest(RequestData requestData)
var request = (HttpWebRequest)WebRequest.Create(requestData.Uri);

request.Accept = requestData.Accept;
request.ContentType = requestData.RequestMimeType;
request.ContentType = requestData.ContentType;
#if !DOTNETCORE
// on netstandard/netcoreapp2.0 this throws argument exception
request.MaximumResponseHeadersLength = -1;
Expand Down
3 changes: 3 additions & 0 deletions src/Elastic.Transport/Products/DefaultProductRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public sealed class DefaultProductRegistration : ProductRegistration
/// <inheritdoc cref="ProductRegistration.ResponseBuilder"/>
public override ResponseBuilder ResponseBuilder => new DefaultResponseBuilder<EmptyError>();

/// <inheritdoc cref="ProductRegistration.DefaultMimeType"/>
public override string DefaultMimeType => null;

/// <inheritdoc cref="ProductRegistration.HttpStatusCodeClassifier"/>
public override bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) =>
statusCode >= 200 && statusCode < 300;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class ElasticsearchProductRegistration : ProductRegistration
{
private readonly HeadersList _headers;
private readonly MetaHeaderProvider _metaHeaderProvider;
private readonly int? _clientMajorVersion;

/// <summary>
/// Create a new instance of the Elasticsearch product registration.
Expand All @@ -30,7 +31,12 @@ public class ElasticsearchProductRegistration : ProductRegistration
///
/// </summary>
/// <param name="markerType"></param>
public ElasticsearchProductRegistration(Type markerType) : this() => _metaHeaderProvider = new DefaultMetaHeaderProvider(markerType, "es");
public ElasticsearchProductRegistration(Type markerType) : this()
{
var clientVersionInfo = ReflectionVersionInfo.Create(markerType);
_metaHeaderProvider = new DefaultMetaHeaderProvider(clientVersionInfo, "es");
_clientMajorVersion = clientVersionInfo.Version.Major;
}

/// <summary> A static instance of <see cref="ElasticsearchProductRegistration"/> to promote reuse </summary>
public static ProductRegistration Default { get; } = new ElasticsearchProductRegistration();
Expand All @@ -53,6 +59,9 @@ public class ElasticsearchProductRegistration : ProductRegistration
/// <inheritdoc cref="ProductRegistration.ResponseBuilder"/>
public override ResponseBuilder ResponseBuilder => new ElasticsearchResponseBuilder();

/// <inheritdoc cref="ProductRegistration.DefaultMimeType"/>
public override string DefaultMimeType => _clientMajorVersion.HasValue ? $"application/vnd.elasticsearch+json;compatible-with={_clientMajorVersion.Value}" : null;

/// <summary> Exposes the path used for sniffing in Elasticsearch </summary>
public const string SniffPath = "_nodes/http,settings";

Expand Down
5 changes: 5 additions & 0 deletions src/Elastic.Transport/Products/ProductRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ namespace Elastic.Transport.Products;
/// </summary>
public abstract class ProductRegistration
{
/// <summary>
/// The default MIME type used for Accept and Content-Type headers for requests.
/// </summary>
public abstract string DefaultMimeType { get; }

/// <summary>
/// The name of the current product utilizing <see cref="HttpTransport{TConnectionSettings}"/>
/// <para>This name makes its way into the transport diagnostics sources and the default user agent string</para>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public DefaultMetaHeaderProvider(Type clientType, string serviceIdentifier)
_syncMetaDataHeader = new MetaDataHeader(clientVersionInfo, serviceIdentifier, false);
}

/// <summary>
///
/// </summary>
internal DefaultMetaHeaderProvider(ReflectionVersionInfo reflectionVersionInfo, string serviceIdentifier)
{
_asyncMetaDataHeader = new MetaDataHeader(reflectionVersionInfo, serviceIdentifier, true);
_syncMetaDataHeader = new MetaDataHeader(reflectionVersionInfo, serviceIdentifier, false);
}

/// <summary>
///
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private static string DetermineVersionFromType(Type type)
{
try
{
var productVersion = "8.0.0-alpha.8+02b315d290415a4eb153beb827a879d037e904f6 (Microsoft Windows 10.0.19044; .NET 6.0.4; Elastic.Clients.Elasticsearch)"; //FileVersionInfo.GetVersionInfo(type.GetTypeInfo().Assembly.Location)?.ProductVersion ?? EmptyVersion;
var productVersion = FileVersionInfo.GetVersionInfo(type.GetTypeInfo().Assembly.Location)?.ProductVersion ?? EmptyVersion;

if (productVersion == EmptyVersion)
productVersion = Assembly.GetAssembly(type).GetName().Version.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="xunit" Version="2.4.1" />
Expand Down
54 changes: 54 additions & 0 deletions tests/Elastic.Transport.IntegrationTests/Http/MetaHeaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Linq;
using System.Threading.Tasks;
using Elastic.Transport.IntegrationTests.Plumbing;
using Elastic.Transport.IntegrationTests.Plumbing.Stubs;
using Elastic.Transport.Products.Elasticsearch;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Xunit;

namespace Elastic.Transport.IntegrationTests.Http;

/// <summary>
/// Tests that the test framework loads a controller and the exposed transport can talk to its endpoints.
/// Tests runs against a server that started up once and its server instance shared among many test classes
/// </summary>
public class MetaHeaderTests : AssemblyServerTestsBase
{
public MetaHeaderTests(TransportTestServer instance) : base(instance) { }

[Fact]
public async Task AddsExpectedMetaHeader()
{
var connection = new TestableHttpConnection(responseMessage =>
{
responseMessage.RequestMessage.Content.Headers.ContentType.MediaType.Should().Be("application/vnd.elasticsearch+json");
var parameter = responseMessage.RequestMessage.Content.Headers.ContentType.Parameters.Single();
parameter.Name.Should().Be("compatible-with");
parameter.Value.Should().Be("8");

var acceptValues = responseMessage.RequestMessage.Headers.GetValues("Accept");
acceptValues.Single().Replace(" ", "").Should().Be("application/vnd.elasticsearch+json;compatible-with=8");

var contentTypeValues = responseMessage.RequestMessage.Content.Headers.GetValues("Content-Type");
contentTypeValues.Single().Replace(" ", "").Should().Be("application/vnd.elasticsearch+json;compatible-with=8");
});

var connectionPool = new SingleNodePool(Server.Uri);
var config = new TransportConfiguration(connectionPool, connection, productRegistration: new ElasticsearchProductRegistration(typeof(Clients.Elasticsearch.ElasticsearchClient)));
var transport = new DefaultHttpTransport(config);

var response = await transport.PostAsync<StringResponse>("/metaheader", PostData.String("{}"));
}
}

[ApiController, Route("[controller]")]
public class MetaHeaderController : ControllerBase
{
[HttpPost()]
public async Task<int> Post() => await Task.FromResult(100);
}

0 comments on commit 88a4b31

Please sign in to comment.