-
I'm calling an OData service which requires me to execute an update in a batch due to how the server-code is implemented. This feels very hack:ish and of course the correct action would be to have the server fixed, but this server is out of my control and getting a bugfix would (if even possible) take a long time to get implemented. My biggest concern is that the message of the exception will change in a future release of the client, and my "solution" will start failing. Do anyone have any suggestions of how to handle this better? The response is: And my "solution" is: try
{
var batch = new ODataBatch(_odataClient);
batch += c => c.Unbound().Set(new { Init = true }).Action("PrePostSave").ExecuteAsync();
// batch += c => c......
batch += c => c.Unbound().Set(new { Init = false }).Action("PrePostSave").ExecuteAsync();
await batch.ExecuteAsync();
}
catch (ODataException ex) when (ex.Message == "The header 'Content-Length' was specified multiple times. Each header must appear only once in a batch part.")
{
// Ignore this specific exception in OData client. The batch is processed successfully and the error is thrown when processing the response due to a bug where server writes the "Content-Length" header twice for certain parts of the batch.
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
I ended up implementing a HttpMessageHandler to "unbug" the server response. Implementation: public class UnBugOdataBatchResponse : DelegatingHandler
{
#region Fields
// Cache StringBuilder to use less memory when processing content.
private StringBuilder? _stringBuilder;
#endregion
#region Methods
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpWebResponse = await base.SendAsync(request, cancellationToken);
if (!httpWebResponse.Content.Headers.Contains("Content-Type"))
{
return httpWebResponse;
}
var contentTypeValue = httpWebResponse.Content.Headers.GetValues("Content-Type").FirstOrDefault();
if (!MediaTypeHeaderValue.TryParse(contentTypeValue, out var mediaType) || mediaType.MediaType?.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase) != true)
{
return httpWebResponse;
}
// Response is a multipart odata batch response and we should copy it to remove duplicate Content-Length headers.
var boundary = mediaType.Parameters.FirstOrDefault(p => p.Name == "boundary")?.Value;
if (boundary == null)
{
throw new Exception("Boundary missing for multipart/mixed content.");
}
httpWebResponse.Content = await ProcessMultipart(mediaType, boundary, await httpWebResponse.Content.ReadAsStreamAsync(cancellationToken), cancellationToken);
return httpWebResponse;
}
private static void CopyHeaders(Dictionary<string, StringValues>? source, HttpContentHeaders destination)
{
if (source == null)
{
return;
}
foreach (var header in source)
{
if (destination.Contains(header.Key))
{
continue;
}
destination.Add(header.Key, header.Value.Cast<string?>());
}
}
private async Task<MultipartContent> ProcessMultipart(MediaTypeHeaderValue multipartMediaType, string boundary, Stream stream, CancellationToken cancellationToken)
{
var subType = multipartMediaType.MediaType?.Split('/').LastOrDefault() ?? "mixed";
var multipartContent = new MultipartContent(subType, boundary);
// Iterate multipart sections.
var multipartReader = new MultipartReader(boundary, stream);
while (await multipartReader.ReadNextSectionAsync(cancellationToken) is { } multipartSection)
{
var contentTypeValue = multipartSection.Headers?.GetValueOrDefault("Content-Type");
if (MediaTypeHeaderValue.TryParse(contentTypeValue, out var newMultipartMediaType) && newMultipartMediaType.MediaType?.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase) == true)
{
var newBoundary = newMultipartMediaType.Parameters.FirstOrDefault(p => p.Name == "boundary")?.Value;
if (newBoundary == null)
{
throw new Exception("Boundary missing for multipart/mixed content.");
}
// Process nested multipart.
var newMultipartContent = await ProcessMultipart(newMultipartMediaType, newBoundary, multipartSection.Body, cancellationToken);
CopyHeaders(multipartSection.Headers, newMultipartContent.Headers);
multipartContent.Add(newMultipartContent);
}
else
{
// Process content.
var contentMediaType = new MediaTypeHeaderValue(newMultipartMediaType?.MediaType ?? "text/plain");
using var bodyReader = new StreamReader(multipartSection.Body, Encoding.GetEncoding(newMultipartMediaType?.CharSet ?? "utf-8"));
var streamContent = new StringContent(
newMultipartMediaType?.MediaType == "application/http"
? await RemoveDuplicateHeadersFromHttpResponseAsync(bodyReader, cancellationToken)
: await bodyReader.ReadToEndAsync(cancellationToken),
contentMediaType
);
CopyHeaders(multipartSection.Headers, streamContent.Headers);
multipartContent.Add(streamContent);
}
}
return multipartContent;
}
private async Task<string> RemoveDuplicateHeadersFromHttpResponseAsync(StreamReader streamReader, CancellationToken cancellationToken)
{
if (_stringBuilder == null)
{
_stringBuilder = new StringBuilder();
}
else
{
_stringBuilder.Clear();
}
// Read the status line.
var statusLine = await streamReader.ReadLineAsync(cancellationToken);
_stringBuilder.AppendLine(statusLine);
// Read the headers.
var headers = new HashSet<string>();
string? line;
while (!string.IsNullOrEmpty(line = await streamReader.ReadLineAsync(cancellationToken)))
{
if (!headers.Add(line))
{
continue;
}
_stringBuilder.AppendLine(line);
}
// Read the body.
_stringBuilder.AppendLine();
var buffer = ArrayPool<char>.Shared.Rent(4096);
try
{
int bytesRead;
while ((bytesRead = await streamReader.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
_stringBuilder.Append(buffer, 0, bytesRead);
}
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
return _stringBuilder.ToString();
}
#endregion
} Usage: // Register HttpClient that is used by Odata API clients
services
.AddHttpClient("OdataHttpClient"))
.AddHttpMessageHandler<UnBugOdataBatchResponse>();
// Register OData clients.
services.AddHttpClient<OdataClient>("OdataHttpClient"); |
Beta Was this translation helpful? Give feedback.
I ended up implementing a HttpMessageHandler to "unbug" the server response.
It still feels a bit bloated, but i feel better about it than catching exceptions.
Implementation: