Skip to content

Commit

Permalink
feat: add response headers to HttpException (#658)
Browse files Browse the repository at this point in the history
* feat: (WIP) add response headers to HttpException

* chore: (WIP) reformat test code

* feat: (WIP) makes response headers accessible from WriteErrorEvent

* chore: reformat code in recent changes.

* docs: adds examples of handling HTTP Headers in errors.

* chore: tweak wait polling in new example.

* docs: add API doc and update Examples/README.md

* docs: tweaking new example.

* docs: update CHANGELOG.md

* chore: change Headers from raw field to AutoProperty

* chore: explicitly set nullable context for Headers.

---------

Co-authored-by: Jakub Bednář <jakub.bednar@gmail.com>
  • Loading branch information
karel-rehor and bednar authored Sep 2, 2024
1 parent fd2ea47 commit f212951
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 4.18.0 [unreleased]

### Features:
1. [#658](https://github.com/influxdata/influxdb-client-csharp/pull/658): Add HttpHeaders as `IEnumerable<RestSharp.HttpParameter>` to `HttpException` and facilitate access in `WriteErrorEvent`. Includes new example `HttpErrorHandling`.

### Dependencies
Update dependencies:

Expand Down
9 changes: 9 additions & 0 deletions Client.Core/Exceptions/InfluxException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using InfluxDB.Client.Core.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -56,6 +57,13 @@ public HttpException(string message, int status, Exception exception = null) : b
/// </summary>
public int? RetryAfter { get; set; }

#nullable enable
/// <summary>
/// The response headers
/// </summary>
public IEnumerable<HeaderParameter>? Headers { get; private set; }
#nullable disable

public static HttpException Create(RestResponse requestResult, object body)
{
Arguments.CheckNotNull(requestResult, nameof(requestResult));
Expand Down Expand Up @@ -162,6 +170,7 @@ public static HttpException Create(object content, IEnumerable<HeaderParameter>

err.ErrorBody = errorBody;
err.RetryAfter = retryAfter;
err.Headers = headers;

return err;
}
Expand Down
46 changes: 46 additions & 0 deletions Client.Test/InfluxExceptionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using InfluxDB.Client.Core.Exceptions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using RestSharp;

namespace InfluxDB.Client.Test
{
[TestFixture]
public class InfluxExceptionTest
{
[Test]
public void ExceptionHeadersTest()
{
try
{
throw HttpException.Create(
JObject.Parse("{\"callId\": \"123456789\", \"message\": \"error in content object\"}"),
new List<HeaderParameter>
{
new HeaderParameter("Trace-Id", "123456789ABCDEF0"),
new HeaderParameter("X-Influx-Version", "1.0.0"),
new HeaderParameter("X-Platform-Error-Code", "unavailable"),
new HeaderParameter("Retry-After", "60000")
},
null,
HttpStatusCode.ServiceUnavailable);
}
catch (HttpException he)
{
Assert.AreEqual("error in content object", he?.Message);

Assert.AreEqual(4, he?.Headers.Count());
var headers = new Dictionary<string, string>();
foreach (var header in he?.Headers) headers.Add(header.Name, header.Value);
Assert.AreEqual("123456789ABCDEF0", headers["Trace-Id"]);
Assert.AreEqual("1.0.0", headers["X-Influx-Version"]);
Assert.AreEqual("unavailable", headers["X-Platform-Error-Code"]);
Assert.AreEqual("60000", headers["Retry-After"]);
}
}
}
}
103 changes: 103 additions & 0 deletions Client.Test/ItErrorEventsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using NUnit.Framework;
using System.Collections.Generic;
using System.Threading.Tasks;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Writes;

namespace InfluxDB.Client.Test
{
[TestFixture]
public class ItErrorEventsTest : AbstractItClientTest
{
private Organization _org;
private Bucket _bucket;
private string _token;
private InfluxDBClientOptions _options;

[SetUp]
public new async Task SetUp()
{
_org = await FindMyOrg();
_bucket = await Client.GetBucketsApi()
.CreateBucketAsync(GenerateName("fliers"), null, _org);

//
// Add Permissions to read and write to the Bucket
//
var resource = new PermissionResource(PermissionResource.TypeBuckets, _bucket.Id, null,
_org.Id);

var readBucket = new Permission(Permission.ActionEnum.Read, resource);
var writeBucket = new Permission(Permission.ActionEnum.Write, resource);

var loggedUser = await Client.GetUsersApi().MeAsync();
Assert.IsNotNull(loggedUser);

var authorization = await Client.GetAuthorizationsApi()
.CreateAuthorizationAsync(_org,
new List<Permission> { readBucket, writeBucket });

_token = authorization.Token;

Client.Dispose();

_options = new InfluxDBClientOptions(InfluxDbUrl)
{
Token = _token,
Org = _org.Id,
Bucket = _bucket.Id
};

Client = new InfluxDBClient(_options);
}


[Test]
public void HandleEvents()
{
using (var writeApi = Client.GetWriteApi())
{
writeApi.EventHandler += (sender, eventArgs) =>
{
switch (eventArgs)
{
case WriteSuccessEvent successEvent:
Assert.Fail("Call should not succeed");
break;
case WriteErrorEvent errorEvent:
Assert.AreEqual("unable to parse 'velocity,unit=C3PO mps=': missing field value",
errorEvent.Exception.Message);
var eventHeaders = errorEvent.GetHeaders();
if (eventHeaders == null)
{
Assert.Fail("WriteErrorEvent must return headers.");
}

var headers = new Dictionary<string, string> { };
foreach (var hp in eventHeaders)
{
Console.WriteLine("DEBUG {0}: {1}", hp.Name, hp.Value);
headers.Add(hp.Name, hp.Value);
}

Assert.AreEqual(4, headers.Count);
Assert.AreEqual("OSS", headers["X-Influxdb-Build"]);
Assert.True(headers["X-Influxdb-Version"].StartsWith('v'));
Assert.AreEqual("invalid", headers["X-Platform-Error-Code"]);
Assert.AreNotEqual("missing", headers.GetValueOrDefault("Date", "missing"));
break;
case WriteRetriableErrorEvent retriableErrorEvent:
Assert.Fail("Call should not be retriable.");
break;
case WriteRuntimeExceptionEvent runtimeExceptionEvent:
Assert.Fail("Call should not result in runtime exception. {0}", runtimeExceptionEvent);
break;
}
};

writeApi.WriteRecord("velocity,unit=C3PO mps=", WritePrecision.S, _bucket.Name, _org.Name);
}
}
}
}
25 changes: 25 additions & 0 deletions Client.Test/ItWriteApiAsyncTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Core;
using InfluxDB.Client.Core.Exceptions;
using InfluxDB.Client.Core.Flux.Domain;
using InfluxDB.Client.Core.Test;
using InfluxDB.Client.Writes;
Expand Down Expand Up @@ -185,5 +187,28 @@ public async Task WriteULongValues()

Assert.AreEqual(ulong.MaxValue, query[0].Records[0].GetValue());
}

[Test]
public async Task WriteWithError()
{
try
{
await _writeApi.WriteRecordAsync("h2o,location=fox_hollow water_level=");
Assert.Fail("Call should fail");
}
catch (HttpException exception)
{
Assert.AreEqual("unable to parse 'h2o,location=fox_hollow water_level=': missing field value",
exception.Message);
Assert.AreEqual(400, exception.Status);
Assert.GreaterOrEqual(4, exception.Headers.Count());
var headers = new Dictionary<string, string>();
foreach (var header in exception?.Headers) headers.Add(header.Name, header.Value);
Assert.AreEqual("OSS", headers["X-Influxdb-Build"]);
Assert.AreEqual("invalid", headers["X-Platform-Error-Code"]);
Assert.IsTrue(headers["X-Influxdb-Version"].StartsWith('v'));
Assert.NotNull(headers["Date"]);
}
}
}
}
57 changes: 57 additions & 0 deletions Client.Test/WriteApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using System.Threading;
using Castle.Core.Smtp;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Core;
using InfluxDB.Client.Core.Exceptions;
Expand Down Expand Up @@ -506,6 +507,62 @@ public void WriteRuntimeException()
Assert.AreEqual(0, MockServer.LogEntries.Count());
}

[Test]
public void WriteExceptionWithHeaders()
{
var localWriteApi = _client.GetWriteApi(new WriteOptions { RetryInterval = 1_000 });

var traceId = Guid.NewGuid().ToString();
const string buildName = "TestBuild";
const string version = "v99.9.9";

localWriteApi.EventHandler += (sender, eventArgs) =>
{
switch (eventArgs)
{
case WriteErrorEvent errorEvent:
Assert.AreEqual("just a test", errorEvent.Exception.Message);
var errHeaders = errorEvent.GetHeaders();
var headers = new Dictionary<string, string>();
foreach (var h in errHeaders)
headers.Add(h.Name, h.Value);
Assert.AreEqual(6, headers.Count);
Assert.AreEqual(traceId, headers["Trace-Id"]);
Assert.AreEqual(buildName, headers["X-Influxdb-Build"]);
Assert.AreEqual(version, headers["X-Influxdb-Version"]);
break;
default:
Assert.Fail("Expect only WriteErrorEvents but got {0}", eventArgs.GetType());
break;
}
};
MockServer
.Given(Request.Create().WithPath("/api/v2/write").UsingPost())
.RespondWith(
CreateResponse("{ \"message\": \"just a test\", \"status-code\": \"Bad Request\"}")
.WithStatusCode(400)
.WithHeaders(new Dictionary<string, string>()
{
{ "Content-Type", "application/json" },
{ "Trace-Id", traceId },
{ "X-Influxdb-Build", buildName },
{ "X-Influxdb-Version", version }
})
);


var measurement = new SimpleModel
{
Time = new DateTime(2024, 09, 01, 6, 15, 00),
Device = "R2D2",
Value = 1976
};

localWriteApi.WriteMeasurement(measurement, WritePrecision.S, "b1", "org1");

localWriteApi.Dispose();
}

[Test]
public void RequiredOrgBucketWriteApi()
{
Expand Down
11 changes: 11 additions & 0 deletions Client/Writes/Events.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Core;
using InfluxDB.Client.Core.Exceptions;
using RestSharp;

namespace InfluxDB.Client.Writes
{
Expand Down Expand Up @@ -42,6 +45,14 @@ internal override void LogEvent()
{
Trace.TraceError($"The error occurred during writing of data: {Exception.Message}");
}

/// <summary>
/// Get headers from the nested exception.
/// </summary>
public IEnumerable<HeaderParameter> GetHeaders()
{
return ((HttpException)Exception)?.Headers;
}
}

/// <summary>
Expand Down
Loading

0 comments on commit f212951

Please sign in to comment.