Skip to content

Commit e3e477a

Browse files
authored
Skip MIME type validation when no body (#5735)
This skips MIME type validation for 204 responses and responses to HEAD requests. Elastic Cloud responses to HEAD requests strip the content-type header and this causes validation issues. We also skip setting the body based on this same check.
1 parent 5d38238 commit e3e477a

File tree

2 files changed

+91
-8
lines changed

2 files changed

+91
-8
lines changed

src/Elasticsearch.Net/Transport/Pipeline/ResponseBuilder.cs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public static class ResponseBuilder
1717
public const int BufferSize = 81920;
1818

1919
private static readonly Type[] SpecialTypes =
20-
{ typeof(StringResponse), typeof(BytesResponse), typeof(VoidResponse), typeof(DynamicResponse) };
20+
{
21+
typeof(StringResponse), typeof(BytesResponse), typeof(VoidResponse), typeof(DynamicResponse)
22+
};
2123

2224
public static TResponse ToResponse<TResponse>(
2325
RequestData requestData,
@@ -31,8 +33,17 @@ public static TResponse ToResponse<TResponse>(
3133
{
3234
responseStream.ThrowIfNull(nameof(responseStream));
3335
var details = Initialize(requestData, ex, statusCode, warnings, mimeType);
36+
3437
//TODO take ex and (responseStream == Stream.Null) into account might not need to flow to SetBody in that case
35-
var response = SetBody<TResponse>(details, requestData, responseStream, mimeType) ?? new TResponse();
38+
39+
TResponse response = null;
40+
41+
// Only attempt to set the body if the response may have content
42+
if (MayHaveBody(statusCode, requestData.Method))
43+
response = SetBody<TResponse>(details, requestData, responseStream, mimeType);
44+
45+
response ??= new TResponse();
46+
3647
response.ApiCall = details;
3748
return response;
3849
}
@@ -50,12 +61,25 @@ public static async Task<TResponse> ToResponseAsync<TResponse>(
5061
{
5162
responseStream.ThrowIfNull(nameof(responseStream));
5263
var details = Initialize(requestData, ex, statusCode, warnings, mimeType);
53-
var response = await SetBodyAsync<TResponse>(details, requestData, responseStream, mimeType, cancellationToken).ConfigureAwait(false)
54-
?? new TResponse();
64+
65+
TResponse response = null;
66+
67+
// Only attempt to set the body if the response may have content
68+
if (MayHaveBody(statusCode, requestData.Method))
69+
response = await SetBodyAsync<TResponse>(details, requestData, responseStream, mimeType, cancellationToken).ConfigureAwait(false);
70+
71+
response ??= new TResponse();
72+
5573
response.ApiCall = details;
5674
return response;
5775
}
5876

77+
/// <summary>
78+
/// A helper which returns true if the response could potentially have a body.
79+
/// </summary>
80+
private static bool MayHaveBody(int? statusCode, HttpMethod httpMethod) =>
81+
!statusCode.HasValue || (statusCode.Value != 204 && httpMethod != HttpMethod.HEAD);
82+
5983
private static ApiCallDetails Initialize(
6084
RequestData requestData, Exception exception, int? statusCode, IEnumerable<string> warnings, string mimeType
6185
)
@@ -70,7 +94,10 @@ private static ApiCallDetails Initialize(
7094
success = requestData.ConnectionSettings
7195
.StatusCodeToResponseSuccess(requestData.Method, statusCode.Value);
7296
}
73-
if (!RequestData.ValidResponseContentType(requestData.Accept, mimeType))
97+
98+
// We don't validate the content-type (MIME type) for HEAD requests or responses that have no content (204 status code).
99+
// Elastic Cloud responses to HEAD requests strip the content-type header so we want to avoid validation in that case.
100+
if (MayHaveBody(statusCode, requestData.Method) && !RequestData.ValidResponseContentType(requestData.Accept, mimeType))
74101
success = false;
75102

76103
var details = new ApiCallDetails
@@ -143,7 +170,8 @@ private static async Task<TResponse> SetBodyAsync<TResponse>(
143170

144171
var serializer = requestData.ConnectionSettings.RequestResponseSerializer;
145172
if (requestData.CustomResponseBuilder != null)
146-
return await requestData.CustomResponseBuilder.DeserializeResponseAsync(serializer, details, responseStream, cancellationToken).ConfigureAwait(false) as TResponse;
173+
return await requestData.CustomResponseBuilder.DeserializeResponseAsync(serializer, details, responseStream, cancellationToken)
174+
.ConfigureAwait(false) as TResponse;
147175

148176
return !RequestData.ValidResponseContentType(requestData.Accept, mimeType)
149177
? null
@@ -171,8 +199,7 @@ private static bool SetSpecialTypes<TResponse>(string mimeType, byte[] bytes, IM
171199
//if not json store the result under "body"
172200
if (!RequestData.IsJsonMimeType(mimeType))
173201
{
174-
var dictionary = new DynamicDictionary();
175-
dictionary["body"] = new DynamicValue(bytes.Utf8String());
202+
var dictionary = new DynamicDictionary { ["body"] = new(bytes.Utf8String()) };
176203
cs = new DynamicResponse(dictionary) as TResponse;
177204
}
178205
else
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
using System.IO;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Elastic.Elasticsearch.Xunit.XunitPlumbing;
10+
using Elasticsearch.Net;
11+
using FluentAssertions;
12+
using Nest;
13+
using Tests.Core.Client;
14+
15+
namespace Tests.Reproduce
16+
{
17+
public class GitHubIssue5730
18+
{
19+
[U]
20+
public void IndexExistsIsValidForBothLocalAndCloudResponses()
21+
{
22+
var client = TestClient.FixedInMemoryClient(Array.Empty<byte>());
23+
24+
var response = client.Indices.Exists("an-index");
25+
26+
response.IsValid.Should().BeTrue();
27+
response.Exists.Should().BeTrue();
28+
29+
// Elastic Cloud responses for HEAD requests do not include the `content-type` header.
30+
// This test ensures that our validation handles this.
31+
32+
var cloudClient = new ElasticClient(new ConnectionSettings(new SingleNodeConnectionPool(new Uri("http://localhost:9200")),
33+
new CloudIndexExistsInMemoryConnection()));
34+
35+
response = cloudClient.Indices.Exists("an-index");
36+
37+
response.IsValid.Should().BeTrue();
38+
response.Exists.Should().BeTrue();
39+
}
40+
41+
/// <summary>
42+
/// Simulates not returning the content-type header so passes null mimeType to the <see cref="ResponseBuilder" />.
43+
/// </summary>
44+
private class CloudIndexExistsInMemoryConnection : IConnection
45+
{
46+
public TResponse Request<TResponse>(RequestData requestData) where TResponse : class, IElasticsearchResponse, new() =>
47+
ResponseBuilder.ToResponse<TResponse>(requestData, null, 200, null, Stream.Null, null);
48+
49+
public Task<TResponse> RequestAsync<TResponse>(RequestData requestData, CancellationToken cancellationToken)
50+
where TResponse : class, IElasticsearchResponse, new() =>
51+
Task.FromResult(ResponseBuilder.ToResponse<TResponse>(requestData, null, 200, null, Stream.Null, null));
52+
53+
public void Dispose() { }
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)