jpsingleton/Huxley2

Issues with new serviceID format

Closed this issue · 1 comments

Describe the bug
Huxley 2 appears to be unable to interpret the new-format Darwin Service IDs - as such, all queries that hit a new Darwin server (which now appears to be the majority?) will error out

To Reproduce
Query any previously valid route

Expected behaviour
Information Returned

Actual Behaviour

  • Error 500 returned
    In logs:
fail: Huxley2.Controllers.DeparturesController[0]
      Open LDB DepartureBoard API call failed
      System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
         at System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength)
         at System.Convert.FromBase64String(String s)
         at ServiceIdGuidGetter(Object )
         at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer)
         at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.Converters.ArrayConverter`2.OnWriteResume(Utf8JsonWriter writer, TElement[] array, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer)
         at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.JsonSerializer.WriteCore[TValue](JsonConverter jsonConverter, Utf8JsonWriter writer, TValue& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.JsonSerializer.WriteStream[TValue](Stream utf8Json, TValue& value, JsonTypeInfo jsonTypeInfo)
         at System.Text.Json.JsonSerializer.Serialize(Stream utf8Json, Object value, Type inputType, JsonSerializerOptions options)
         at Huxley2.Services.ChecksumGenerator.GenerateChecksumObj(Object obj) in /app/Services/ChecksumGenerator.cs:line 57
         at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
         at Huxley2.Services.ChecksumGenerator.GenerateChecksumImpl(Object response) in /app/Services/ChecksumGenerator.cs:line 43
         at Huxley2.Services.ChecksumGenerator.GenerateChecksum(BaseStationBoard board) in /app/Services/ChecksumGenerator.cs:line 14
         at Huxley2.Services.StationBoardService.GenerateChecksum(BaseStationBoard board) in /app/Services/StationBoardService.cs:line 103
         at Huxley2.Controllers.DeparturesController.Get(StationBoardRequest request) in /app/Controllers/DeparturesController.cs:line 50

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "XXXXXXXXXXXXX", Request id "XXXXXXXXXXXXX:00000005": An unhandled exception was thrown by the application.
      System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
         at System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength)
         at System.Convert.FromBase64String(String s)
         at ServiceIdGuidGetter(Object )
         at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer)
         at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.Converters.ArrayConverter`2.OnWriteResume(Utf8JsonWriter writer, TElement[] array, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer)
         at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.Serialization.JsonConverter`1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.JsonSerializer.WriteCore[TValue](JsonConverter jsonConverter, Utf8JsonWriter writer, TValue& value, JsonSerializerOptions options, WriteStack& state)
         at System.Text.Json.JsonSerializer.WriteStream[TValue](Stream utf8Json, TValue& value, JsonTypeInfo jsonTypeInfo)
         at System.Text.Json.JsonSerializer.Serialize(Stream utf8Json, Object value, Type inputType, JsonSerializerOptions options)
         at Huxley2.Services.ChecksumGenerator.GenerateChecksumObj(Object obj) in /app/Services/ChecksumGenerator.cs:line 57
         at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
         at Huxley2.Services.ChecksumGenerator.GenerateChecksumImpl(Object response) in /app/Services/ChecksumGenerator.cs:line 43
         at Huxley2.Services.ChecksumGenerator.GenerateChecksum(BaseStationBoard board) in /app/Services/ChecksumGenerator.cs:line 14
         at Huxley2.Services.StationBoardService.GenerateChecksum(BaseStationBoard board) in /app/Services/StationBoardService.cs:line 103
         at Huxley2.Controllers.DeparturesController.Get(StationBoardRequest request) in /app/Controllers/DeparturesController.cs:line 50
         at lambda_method18(Closure , Object )
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware.Invoke(HttpContext httpContext)
         at ETagMiddleware.InvokeAsync(HttpContext context) in /app/ETagMiddleware.cs:line 31
         at Microsoft.AspNetCore.ResponseCompression.ResponseCompressionMiddleware.InvokeCore(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

(Version 2.03)

Additional context
Open Rail Data discussion

In the short term, it appears this can be fixed with the following changes to BaseServiceItem.cs and ServiceDetailsService.cs:

--- a/Huxley2/Connected Services/OpenLDBWS/BaseServiceItem.cs
+++ b/Huxley2/Connected Services/OpenLDBWS/BaseServiceItem.cs
@@ -11,9 +11,9 @@ namespace OpenLDBWS
 {
     public partial class BaseServiceItem
     {
         public string ServiceIdPercentEncoded => WebUtility.UrlEncode(serviceIDField);

-        public Guid ServiceIdGuid => new Guid(Convert.FromBase64String(serviceIDField));
+        // public Guid ServiceIdGuid => new Guid(serviceIDField);

         [SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "Not a URL")]
-        public string ServiceIdUrlSafe => WebEncoders.Base64UrlEncode(Convert.FromBase64String(serviceIDField));
+        public string ServiceIdUrlSafe => serviceIDField; // underscores are URL safe
     }
 }
--- a/Huxley2/Services/ServiceDetailsService.cs
+++ b/Huxley2/Services/ServiceDetailsService.cs
@@ -55,17 +55,27 @@ namespace Huxley2.Services

             // If ID looks like a RID (15 decimal digit long base 10 integer) then use the staff API if configured
             // This appears to be the date and the UID (with the first character in decimal representation)
-            if (request.ServiceId.Length == 15 && long.TryParse(request.ServiceId, out _)
-                && _accessTokenService.TryMakeStaffAccessToken(out var staffToken))
-            {
-                _logger.LogInformation($"Calling staff service details SOAP endpoint for {request.ServiceId}");
-                var staffService = await _staffSoapClient.GetServiceDetailsByRIDAsync(
-                    new GetServiceDetailsByRIDRequest
+            if (request.ServiceId.Length == 15) {
+                if (long.TryParse(request.ServiceId, out _)
+                    && _accessTokenService.TryMakeStaffAccessToken(out var staffToken)) {
+                    _logger.LogInformation($"Calling staff service details SOAP endpoint for {request.ServiceId}");
+                    var staffService = await _staffSoapClient.GetServiceDetailsByRIDAsync(
+                        new GetServiceDetailsByRIDRequest
+                        {
+                            AccessToken = staffToken,
+                            rid = request.ServiceId,
+                        });
+                    return staffService.GetServiceDetailsResult;
+                } else if (request.ServiceId.EndsWith("_")) {
+                    _logger.LogInformation($"Calling service details SOAP endpoint for {request.ServiceId}");
+                    var s = await _soapClient.GetServiceDetailsAsync(new GetServiceDetailsRequest
                     {
-                        AccessToken = staffToken,
-                        rid = request.ServiceId,
+                        AccessToken = _accessTokenService.MakeAccessToken(request),
+                        serviceID = request.ServiceId,
                     });
-                return staffService.GetServiceDetailsResult;
+                    return s.GetServiceDetailsResult;
+
+                }
             }

However, this will break GUID support and percent encoding, so a more elegant overall solution is likely needed