6 Commits

Author SHA1 Message Date
a924e32291 Bump version to 2.4.0-beta in ReC.API.csproj
Updated the project version, assembly version, file version, and informational version from 2.3.0-beta to 2.4.0-beta in the ReC.API.csproj file. This prepares the project for the next beta release.
2026-04-17 15:14:32 +02:00
28a4146069 Auto-detect Content-Type and Accept headers if enabled
Add AutoDetectHeaders option to RecAction config and handler. When enabled, automatically set Content-Type based on body content (JSON or XML) and default Accept to application/json if missing. Log warnings when headers are auto-detected. Improves robustness and makes header detection configurable.
2026-04-17 15:13:33 +02:00
17d40817f2 Update version to 2.3.0-beta
Bump Version, AssemblyVersion, FileVersion, and InformationalVersion to 2.3.0-beta in ReC.API.csproj.
2026-04-17 12:42:04 +02:00
330443d2c9 Add config options for EF Core logging and error details
EnableSensitiveDataLogging and EnableDetailedErrors are now configurable via appsettings.json under the "EfCore" section. Program.cs reads these values from configuration instead of hardcoding them, allowing runtime control of EF Core logging and error detail behavior.
2026-04-17 12:41:45 +02:00
6ca876c762 Improve HTTP header handling and error resilience
Enhance InvokeRecActionViewCommandHandler to robustly handle invalid or non-strict HTTP header values. "Content-Type" and "Authorization" headers are now set using strict parsing with fallback to TryAddWithoutValidation on failure, logging warnings for easier debugging. General headers and API key headers also use this strict-then-fallback approach, making HTTP request construction more tolerant of malformed values and reducing runtime errors.
2026-04-17 12:17:41 +02:00
e89af1cbcd Improve HTTP header handling and add logging to handler
Inject optional ILogger into InvokeRecActionViewCommandHandler for enhanced logging. When adding HTTP headers, catch FormatException and log a warning with relevant context, then fall back to TryAddWithoutValidation. This increases robustness and observability for malformed or non-standard headers.
2026-04-17 12:01:15 +02:00
5 changed files with 75 additions and 14 deletions

View File

@@ -48,10 +48,13 @@ try
?? throw new InvalidOperationException("Connection string is not found."); ?? throw new InvalidOperationException("Connection string is not found.");
var logger = provider.GetRequiredService<ILogger<RecDbContext>>(); var logger = provider.GetRequiredService<ILogger<RecDbContext>>();
var enableSensitiveDataLogging = config.GetValue("EfCore:EnableSensitiveDataLogging", true);
var enableDetailedErrors = config.GetValue("EfCore:EnableDetailedErrors", false);
opt.UseSqlServer(cnnStr) opt.UseSqlServer(cnnStr)
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace) .LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
.EnableSensitiveDataLogging() .EnableSensitiveDataLogging(enableSensitiveDataLogging)
.EnableDetailedErrors(); .EnableDetailedErrors(enableDetailedErrors);
}); });
}); });

View File

@@ -10,10 +10,10 @@
<Product>ReC.API</Product> <Product>ReC.API</Product>
<PackageIcon>Assets\icon.ico</PackageIcon> <PackageIcon>Assets\icon.ico</PackageIcon>
<PackageTags>digital data rest-caller rec api</PackageTags> <PackageTags>digital data rest-caller rec api</PackageTags>
<Version>2.2.1-beta</Version> <Version>2.4.0-beta</Version>
<AssemblyVersion>2.2.1.0</AssemblyVersion> <AssemblyVersion>2.4.0.0</AssemblyVersion>
<FileVersion>2.2.1.0</FileVersion> <FileVersion>2.4.0.0</FileVersion>
<InformationalVersion>2.2.0-beta</InformationalVersion> <InformationalVersion>2.4.0-beta</InformationalVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright> <Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>

View File

@@ -5,9 +5,13 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"LuckyPennySoftwareLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg", "LuckyPennySoftwareLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
"EfCore": {
"EnableSensitiveDataLogging": true,
"EnableDetailedErrors": false
},
"RecAction": { "RecAction": {
"AddedWho": "ReC.API", "UseHttp1ForNtlm": false,
"UseHttp1ForNtlm": false "AutoDetectHeaders": false
}, },
// Bad request SqlException numbers numbers can be updated at runtime; no restart required. // Bad request SqlException numbers numbers can be updated at runtime; no restart required.
"SqlException": { "SqlException": {

View File

@@ -3,4 +3,5 @@
public class RecActionOptions public class RecActionOptions
{ {
public bool UseHttp1ForNtlm { get; set; } = false; public bool UseHttp1ForNtlm { get; set; } = false;
public bool AutoDetectHeaders { get; set; } = false;
} }

View File

@@ -1,5 +1,6 @@
using MediatR; using MediatR;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ReC.Application.Common.Constants; using ReC.Application.Common.Constants;
using ReC.Application.Common.Dto; using ReC.Application.Common.Dto;
@@ -35,7 +36,8 @@ public class InvokeRecActionViewCommandHandler(
IOptions<RecActionOptions> options, IOptions<RecActionOptions> options,
ISender sender, ISender sender,
IHttpClientFactory clientFactory, IHttpClientFactory clientFactory,
IConfiguration? config = null IConfiguration? config = null,
ILogger<InvokeRecActionViewCommandHandler>? logger = null
) : IRequestHandler<InvokeRecActionViewCommand> ) : IRequestHandler<InvokeRecActionViewCommand>
{ {
private readonly RecActionOptions _options = options.Value; private readonly RecActionOptions _options = options.Value;
@@ -57,11 +59,47 @@ public class InvokeRecActionViewCommandHandler(
using var httpReq = CreateHttpRequestMessage(restType, action.EndpointUri); using var httpReq = CreateHttpRequestMessage(restType, action.EndpointUri);
if (action.Body is not null) if (action.Body is not null)
{
httpReq.Content = new StringContent(action.Body); httpReq.Content = new StringContent(action.Body);
var contentType = action.Headers?.FirstOrDefault(h => h.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
if (contentType is not null && !string.IsNullOrWhiteSpace(contentType.Value.Value))
try { httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType.Value.Value); }
catch (FormatException ex)
{
logger?.LogWarning(ex, "Content-Type '{Value}' could not be parsed with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", contentType.Value.Value, action.Id, action.ProfileId);
httpReq.Content.Headers.TryAddWithoutValidation("Content-Type", contentType.Value.Value);
}
else if (_options.AutoDetectHeaders)
{
var body = action.Body.TrimStart();
if (body.StartsWith('{') || body.StartsWith('['))
{
httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" };
logger?.LogWarning("Content-Type header was not specified. Auto-detected 'application/json; charset=utf-8' based on body content. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
}
else if (body.StartsWith('<'))
{
httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml") { CharSet = "utf-8" };
logger?.LogWarning("Content-Type header was not specified. Auto-detected 'application/xml; charset=utf-8' based on body content. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
}
}
}
if (action.Headers is not null) if (action.Headers is not null)
foreach (var header in action.Headers) foreach (var header in action.Headers.Where(h => !h.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)))
httpReq.Headers.Add(header.Key, header.Value); try { httpReq.Headers.Add(header.Key, header.Value); }
catch (FormatException ex)
{
logger?.LogWarning(ex, "Header '{Key}' could not be added with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", header.Key, action.Id, action.ProfileId);
httpReq.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (_options.AutoDetectHeaders && !httpReq.Headers.Contains("Accept"))
{
httpReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
logger?.LogWarning("Accept header was not specified. Defaulting to 'application/json'. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
}
switch (action.EndpointAuthType) switch (action.EndpointAuthType)
{ {
@@ -74,7 +112,12 @@ public class InvokeRecActionViewCommandHandler(
switch (action.EndpointAuthApiKeyAddTo) switch (action.EndpointAuthApiKeyAddTo)
{ {
case ApiKeyLocation.Header: case ApiKeyLocation.Header:
httpReq.Headers.Add(apiKey, apiValue); try { httpReq.Headers.Add(apiKey, apiValue); }
catch (FormatException ex)
{
logger?.LogWarning(ex, "ApiKey header '{Key}' could not be added with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", apiKey, action.Id, action.ProfileId);
httpReq.Headers.TryAddWithoutValidation(apiKey, apiValue);
}
break; break;
case ApiKeyLocation.Query: case ApiKeyLocation.Query:
var uriBuilder = new UriBuilder(httpReq.RequestUri!); var uriBuilder = new UriBuilder(httpReq.RequestUri!);
@@ -97,14 +140,24 @@ public class InvokeRecActionViewCommandHandler(
case EndpointAuthType.JwtBearer: case EndpointAuthType.JwtBearer:
case EndpointAuthType.OAuth2: case EndpointAuthType.OAuth2:
if (action.EndpointAuthToken is string authToken) if (action.EndpointAuthToken is string authToken)
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); try { httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); }
catch (FormatException ex)
{
logger?.LogWarning(ex, "Bearer token could not be set with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
httpReq.Headers.TryAddWithoutValidation("Authorization", $"Bearer {authToken}");
}
break; break;
case EndpointAuthType.BasicAuth: case EndpointAuthType.BasicAuth:
if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword) if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword)
{ {
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}")); var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}"));
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth); try { httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth); }
catch (FormatException ex)
{
logger?.LogWarning(ex, "Basic auth could not be set with strict validation, falling back to TryAddWithoutValidation. ActionId: {ActionId}, ProfileId: {ProfileId}", action.Id, action.ProfileId);
httpReq.Headers.TryAddWithoutValidation("Authorization", $"Basic {basicAuth}");
}
} }
break; break;