diff --git a/api/Hmcr.Chris/Api.cs b/api/Hmcr.Chris/Api.cs index b701f803..e4995ae5 100644 --- a/api/Hmcr.Chris/Api.cs +++ b/api/Hmcr.Chris/Api.cs @@ -73,38 +73,45 @@ public async Task GetWithRetry(HttpClient client, string pa public async Task PostWithRetry(HttpClient client, string path, string body) { - var response - = await client.PostAsync(path, new StringContent(body, Encoding.UTF8)); + using var content = new StringContent(body, Encoding.UTF8, "application/xml"); - if (!response.IsSuccessStatusCode) - { - for (var attempt = 2; attempt <= maxAttempt; attempt++) - { - await Task.Delay(100 * attempt); + HttpResponseMessage response = null; + Exception lastException = null; - response = await client.PostAsync(path, new StringContent(body, Encoding.UTF8)); + for (int attempt = 1; attempt <= maxAttempt; attempt++) + { + try + { + response = await client.PostAsync(path, content); if (response.IsSuccessStatusCode) { - break; + return response; } - else if (attempt == maxAttempt) - { - string message = ""; + } + catch (Exception ex) + { + lastException = ex; + } - if (response.Content != null) - { - var bytes = await response.Content.ReadAsByteArrayAsync(); - message = Encoding.UTF8.GetString(bytes); - } + if (attempt < maxAttempt) + { + await Task.Delay(100 * attempt); + } + } - throw new Exception($"Status Code: {response.StatusCode}" + Environment.NewLine + message); - } - } + // Final failure + string message = ""; + + if (response?.Content != null) + { + var bytes = await response.Content.ReadAsByteArrayAsync(); + message = Encoding.UTF8.GetString(bytes); } - return response; + throw new Exception($"Failed POST to {path}. " + + (response != null ? $"Status Code: {response.StatusCode}" : "") + + Environment.NewLine + message, lastException); } - } } diff --git a/api/Hmcr.Chris/OasApi.cs b/api/Hmcr.Chris/OasApi.cs index 1589b2de..d3fdeced 100644 --- a/api/Hmcr.Chris/OasApi.cs +++ b/api/Hmcr.Chris/OasApi.cs @@ -3,9 +3,13 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Net.Http; +using System.Text; using System.Text.Json; using System.Threading.Tasks; +using System.Xml.Linq; namespace Hmcr.Chris { @@ -74,29 +78,75 @@ public OasApi(HttpClient client, IApi api, IConfiguration config, ILogger IsPointOnRfiSegmentAsync(int tolerance, Point point, string rfiSegment) { - var body = ""; - var content = ""; + string Clean(string input) => input?.Replace("\0", "") ?? ""; - try + string rfiSegmentClean = Clean(rfiSegment); + string lon = Clean(point.Longitude.ToString(CultureInfo.InvariantCulture)); + string lat = Clean(point.Latitude.ToString(CultureInfo.InvariantCulture)); + + string body = string.Format(_queries.PointOnRfiSegQuery, tolerance, lon, lat, rfiSegmentClean); + string content = ""; + Exception lastException = null; + + if (body.Contains('\0', StringComparison.Ordinal)) { - body = string.Format(_queries.PointOnRfiSegQuery, tolerance, point.Longitude, point.Latitude, rfiSegment); + _logger.LogError("XML body contains NULL character prior to transmission."); - content = await (await _api.PostWithRetry(_client, _path, body)).Content.ReadAsStringAsync(); + var bytes = Encoding.UTF8.GetBytes(body); + var dumpPath = Path.Combine("/tmp", $"corrupted_body_{DateTime.UtcNow:yyyyMMdd_HHmmss}.bin"); + await File.WriteAllBytesAsync(dumpPath, bytes); - var features = JsonSerializer.Deserialize>(content); + _logger.LogError($"Corrupted XML body written to {dumpPath} for inspection."); + throw new Exception("Aborting request: XML contains illegal null character."); + } - return features.numberMatched > 0; + try + { + _ = XDocument.Parse(body); } catch (Exception ex) { - _logger.LogError($"Exception - IsPointOnRfiSegmentAsync: {body} - {content}"); - throw ex; + _logger.LogError(ex, "XML body failed validation before transmission."); + throw; + } + + _logger.LogDebug($"IsPointOnRfiSegmentAsync - body: {body}"); + _logger.LogInformation($"IsPointOnRfiSegmentAsync - rfiSegment: {rfiSegmentClean}, point: ({lon}, {lat}), tolerance: {tolerance}"); + + for (int i = 0; i < 5; i++) + { + try + { + var response = await _api.PostWithRetry(_client, _path, body); + + if (response.Content.Headers.ContentType?.MediaType != "application/json") + { + content = await response.Content.ReadAsStringAsync(); + _logger.LogWarning($"Non-JSON response (attempt {i + 1}): {content}"); + throw new Exception("Invalid content type"); + } + + content = await response.Content.ReadAsStringAsync(); + var features = JsonSerializer.Deserialize>(content); + + return features?.numberMatched > 0; + } + catch (Exception ex) + { + lastException = ex; + _logger.LogWarning(ex, $"Retry {i + 1} failed for IsPointOnRfiSegmentAsync with body: {body}"); + await Task.Delay((int)Math.Pow(2, i) * 500); // 500ms, 1000ms, 2000ms, 4000ms, 8000ms + } } + + _logger.LogError($"All retries failed for IsPointOnRfiSegmentAsync: {body} - {content}"); + throw new Exception("Failed to determine if point is on RFI segment after multiple attempts.", lastException); } + + public async Task> GetLineFromOffsetMeasureOnRfiSegmentAsync(string rfiSegment, decimal start, decimal end) { var query = ""; diff --git a/api/Hmcr.Domain/Services/SpatialService.cs b/api/Hmcr.Domain/Services/SpatialService.cs index 00aa6457..cf4a2744 100644 --- a/api/Hmcr.Domain/Services/SpatialService.cs +++ b/api/Hmcr.Domain/Services/SpatialService.cs @@ -2,6 +2,7 @@ using Hmcr.Chris.Models; using Hmcr.Model; using Hmcr.Model.Utils; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -36,6 +37,7 @@ public class SpatialService : ISpatialService private IFieldValidatorService _validator; private ILookupCodeService _lookupService; + private ILogger _logger; private IEnumerable _nonSpHighwayUniques = null; private IEnumerable NonSpHighwayUniques => _nonSpHighwayUniques ??= _validator.CodeLookup.Where(x => x.CodeSet == CodeSet.NonSpHighwayUnique).Select(x => x.CodeValue).ToArray().ToLowercase(); @@ -59,7 +61,22 @@ public SpatialService(IOasApi oasApi, IFieldValidatorService validator, ILookupC var threshold = _lookupService.GetThresholdLevel(thresholdLevel); - var isOnRfi = await _oasApi.IsPointOnRfiSegmentAsync(threshold.Error, point, rfiSegment); + bool isOnRfi; + + try + { + isOnRfi = await _oasApi.IsPointOnRfiSegmentAsync(threshold.Error, point, rfiSegment); + } + catch (Exception ex) + { + // Log detailed error + _logger.LogError(ex, $"Exception during WFS check for RFI segment '{rfiSegment}' at point [{point.Latitude}, {point.Longitude}]"); + + // Surface error to validation errors + errors.AddItem("WFS Error", $"Failed to validate GPS point against RFI segment [{rfiSegment}]. Details: {ex.Message}"); + + return (SpValidationResult.Fail, null, rfiResult.segment); + } if (!isOnRfi) {