diff --git a/CHANGELOG.md b/CHANGELOG.md index 67311444c..60bff6e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 4.18.0 [unreleased] +### Features: +1. [#658](https://github.com/influxdata/influxdb-client-csharp/pull/658): Add HttpHeaders as `IEnumerable` to `HttpException` and facilitate access in `WriteErrorEvent`. Includes new example `HttpErrorHandling`. + ### Dependencies Update dependencies: diff --git a/Client.Core/Exceptions/InfluxException.cs b/Client.Core/Exceptions/InfluxException.cs index d413f25c5..ef679c58b 100644 --- a/Client.Core/Exceptions/InfluxException.cs +++ b/Client.Core/Exceptions/InfluxException.cs @@ -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; @@ -56,6 +57,13 @@ public HttpException(string message, int status, Exception exception = null) : b /// public int? RetryAfter { get; set; } +#nullable enable + /// + /// The response headers + /// + public IEnumerable? Headers { get; private set; } +#nullable disable + public static HttpException Create(RestResponse requestResult, object body) { Arguments.CheckNotNull(requestResult, nameof(requestResult)); @@ -162,6 +170,7 @@ public static HttpException Create(object content, IEnumerable err.ErrorBody = errorBody; err.RetryAfter = retryAfter; + err.Headers = headers; return err; } diff --git a/Client.Test/InfluxExceptionTest.cs b/Client.Test/InfluxExceptionTest.cs new file mode 100644 index 000000000..2c7e6a73c --- /dev/null +++ b/Client.Test/InfluxExceptionTest.cs @@ -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 + { + 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(); + 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"]); + } + } + } +} \ No newline at end of file diff --git a/Client.Test/ItErrorEventsTest.cs b/Client.Test/ItErrorEventsTest.cs new file mode 100644 index 000000000..e604200a5 --- /dev/null +++ b/Client.Test/ItErrorEventsTest.cs @@ -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 { 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 { }; + 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); + } + } + } +} \ No newline at end of file diff --git a/Client.Test/ItWriteApiAsyncTest.cs b/Client.Test/ItWriteApiAsyncTest.cs index 6cfb6323e..a9aba7798 100644 --- a/Client.Test/ItWriteApiAsyncTest.cs +++ b/Client.Test/ItWriteApiAsyncTest.cs @@ -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; @@ -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(); + 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"]); + } + } } } \ No newline at end of file diff --git a/Client.Test/WriteApiTest.cs b/Client.Test/WriteApiTest.cs index ec2b3fc3a..29d3212b4 100644 --- a/Client.Test/WriteApiTest.cs +++ b/Client.Test/WriteApiTest.cs @@ -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; @@ -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(); + 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() + { + { "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() { diff --git a/Client/Writes/Events.cs b/Client/Writes/Events.cs index 14ae98db8..dd8b32227 100644 --- a/Client/Writes/Events.cs +++ b/Client/Writes/Events.cs @@ -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 { @@ -42,6 +45,14 @@ internal override void LogEvent() { Trace.TraceError($"The error occurred during writing of data: {Exception.Message}"); } + + /// + /// Get headers from the nested exception. + /// + public IEnumerable GetHeaders() + { + return ((HttpException)Exception)?.Headers; + } } /// diff --git a/Examples/HttpErrorHandling.cs b/Examples/HttpErrorHandling.cs new file mode 100644 index 000000000..3c1f3fc80 --- /dev/null +++ b/Examples/HttpErrorHandling.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using InfluxDB.Client; +using InfluxDB.Client.Api.Domain; +using InfluxDB.Client.Core.Exceptions; +using InfluxDB.Client.Writes; +using RestSharp; + +namespace Examples +{ + public class HttpErrorHandling + { + private static InfluxDBClient _client; + private static List _lpRecords; + + private static void Setup() + { + _client = new InfluxDBClient("http://localhost:9999", + "my-user", "my-password"); + var nowMillis = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + _lpRecords = new List() + { + $"temperature,location=north value=42 {nowMillis}", + $"temperature,location=north value=17 {nowMillis - 10000}", + $"temperature,location=north value= {nowMillis - 20000}", // one flaky record + $"temperature,location=north value=5 {nowMillis - 30000}" + }; + } + + private static void TearDown() + { + _client.Dispose(); + } + + private static Dictionary Headers2Dictionary(IEnumerable headers) + { + var result = new Dictionary(); + foreach (var hp in headers) + result.Add(hp.Name, hp.Value); + return result; + } + + private static async Task WriteRecordsAsync() + { + Console.WriteLine("Write records async with one invalid record."); + + // + // Write Data + // + var writeApiAsync = _client.GetWriteApiAsync(); + + try + { + await writeApiAsync.WriteRecordsAsync(_lpRecords, WritePrecision.Ms, + "my-bucket", "my-org"); + } + catch (HttpException he) + { + Console.WriteLine(" WARNING write failed"); + var headersDix = Headers2Dictionary(he.Headers); + Console.WriteLine(" Caught Exception({0}) \"{1}\"", + he.GetType(), + he.Message); + Console.WriteLine(" Response Status: {0}", he.Status); + Console.WriteLine(" Headers:"); + foreach (var key in headersDix.Keys) + Console.WriteLine($" {key}: {headersDix[key]}"); + } + finally + { + Console.WriteLine(" ===================="); + } + } + + private static void WriteRecordsWithErrorEvent() + { + Console.WriteLine("Write records with error event."); + + var caughtError = false; + using (var writeApi = _client.GetWriteApi()) + { + writeApi.EventHandler += (sender, eventArgs) => + { + switch (eventArgs) + { + case WriteErrorEvent wee: + Console.WriteLine(" WARNING write failed"); + Console.WriteLine(" Received event WriteErrorEvent with:"); + Console.WriteLine(" Status: {0}", ((HttpException)wee.Exception).Status); + Console.WriteLine(" Exception: {0}", wee.Exception.GetType()); + Console.WriteLine(" Message: {0}", wee.Exception.Message); + Console.WriteLine(" Headers:"); + var headersDix = Headers2Dictionary(wee.GetHeaders()); + foreach (var key in headersDix.Keys) + Console.WriteLine($" {key}: {headersDix[key]}"); + caughtError = true; + break; + default: + throw new Exception("Should only receive WriteErrorEvent"); + } + }; + Console.WriteLine("Trying the records list"); + writeApi.WriteRecords(_lpRecords, WritePrecision.Ms, "my-bucket", "my-org"); + var slept = 0; + while (!caughtError && slept < 3001) + { + Thread.Sleep(1000); + slept += 1000; + } + + if (!caughtError) + { + Console.WriteLine("WARN, did not encounter expected error"); + } + + + // manually retry the bad record + Console.WriteLine("Manually retrying the bad record."); + writeApi.WriteRecord(_lpRecords[2], WritePrecision.Ms, "my-bucket", "my-org"); + caughtError = false; + slept = 0; + while (!caughtError && slept < 3001) + { + Thread.Sleep(1000); + slept += 1000; + } + + if (!caughtError) + { + Console.WriteLine("WARN, did not encounter expected error"); + } + } + } + + public static async Task Main() + { + Console.WriteLine("Main()"); + Setup(); + await WriteRecordsAsync(); + WriteRecordsWithErrorEvent(); + TearDown(); + } + } +} \ No newline at end of file diff --git a/Examples/README.md b/Examples/README.md index 6eca76b7a..e8681b3f4 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -2,6 +2,7 @@ ## Writes - [WriteEventHandlerExample.cs](WriteEventHandlerExample.cs) - How to use WriteAPI with batch options and how to handle events +- [HttpErrorHandling.cs](HttpErrorHandling.cs) - How to handle errors on write and retrieve response headers. ## Others - [InvokableScripts.cs](InvokableScripts.cs) - How to use Invokable scripts Cloud API to create custom endpoints that query data diff --git a/Examples/RunExamples.cs b/Examples/RunExamples.cs index 91d5e59f8..a40688819 100644 --- a/Examples/RunExamples.cs +++ b/Examples/RunExamples.cs @@ -72,6 +72,9 @@ public static async Task Main(string[] args) case "RecordRowExample": await RecordRowExample.Main(); break; + case "HttpErrorHandling": + await HttpErrorHandling.Main(); + break; } } else pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy