Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c79a61c
Do not leak exception details in production
elzik Nov 1, 2025
86f0986
Fix unauthorised scenario asserts
elzik Nov 1, 2025
a07c208
Fix whitespace issue
elzik Nov 1, 2025
d4309f8
Fix whitespace
elzik Nov 1, 2025
e765be2
Fix whitespace
elzik Nov 1, 2025
4b34d4b
Ensure tests cover exception handler as well as development excetion …
elzik Nov 1, 2025
9571f3f
Merge branch 'improve-api-error-handling' of https://github.com/elzik…
elzik Nov 1, 2025
a0d1d9f
Revert "Ensure tests cover exception handler as well as development e…
elzik Nov 1, 2025
040c833
Use additional class to ensure tests cover exception handler as well …
elzik Nov 1, 2025
12871ac
Use multiple WebApplicationFactories to cover production and deveopme…
elzik Nov 1, 2025
3ef8ba8
Use TheoryData as return type to provide better type safety
elzik Nov 1, 2025
6867e19
Explicit;ly configure exception handler
elzik Nov 1, 2025
dcde89c
Ensure WebApplicationFactory and client get disposed
elzik Nov 1, 2025
22ce3e7
Add http sample file to solution
elzik Nov 1, 2025
e7f7505
Upgrade to .NET 10 (#172)
elzik Nov 22, 2025
c22f15b
Do not leak exception details in production
elzik Nov 1, 2025
65a7f54
Fix unauthorised scenario asserts
elzik Nov 1, 2025
980b709
Fix whitespace
elzik Nov 1, 2025
11c2d94
Fix whitespace
elzik Nov 1, 2025
a717e05
Ensure tests cover exception handler as well as development excetion …
elzik Nov 1, 2025
7a85bde
Fix whitespace issue
elzik Nov 1, 2025
27eaba5
Revert "Ensure tests cover exception handler as well as development e…
elzik Nov 1, 2025
fa9df67
Use additional class to ensure tests cover exception handler as well …
elzik Nov 1, 2025
2f47678
Use multiple WebApplicationFactories to cover production and deveopme…
elzik Nov 1, 2025
829f415
Use TheoryData as return type to provide better type safety
elzik Nov 1, 2025
539d294
Explicit;ly configure exception handler
elzik Nov 1, 2025
9a806d5
Ensure WebApplicationFactory and client get disposed
elzik Nov 1, 2025
5adc8ed
Add http sample file to solution
elzik Nov 1, 2025
52641f1
Implement ProblemDetails for only caller fixable extensions
elzik Nov 23, 2025
bc484ca
Use extension method for exception handling
elzik Nov 23, 2025
3de67a7
Don't end Problem Deatils titles with full-stops
elzik Nov 23, 2025
89c82cf
Refactor deprecated ConfigureWebHost override pattern
elzik Nov 23, 2025
48fe13c
Treat all ICallerFixableException instaces as a Status400BadRequest
elzik Nov 24, 2025
5b08a15
Add funcitonal Problem Details tests
elzik Nov 24, 2025
fd38a49
Remove unnecessary null-conditional operator
elzik Nov 24, 2025
329fe4a
Don't perform seperate dev and prod health tests - there is no value
elzik Nov 24, 2025
9193756
Make namespaces clearer
elzik Nov 24, 2025
64344d8
Remove unused WebApplicationFactories
elzik Nov 24, 2025
6448dac
Remove unecessary launch profile
elzik Nov 24, 2025
f0862a4
Add trace ID to Problem Details and log
elzik Nov 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Elzik.Breef.sln
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestData", "TestData", "{7F
ProjectSection(SolutionItems) = preProject
tests\TestData\BbcNewsPage-ExpectedContent.txt = tests\TestData\BbcNewsPage-ExpectedContent.txt
tests\TestData\BbcNewsPage.html = tests\TestData\BbcNewsPage.html
tests\TestData\SampleRedditPost-1kqiwzc.json = tests\TestData\SampleRedditPost-1kqiwzc.json
tests\TestData\StaticTestPage.html = tests\TestData\StaticTestPage.html
tests\TestData\TestHtmlPage-ExpectedContent.txt = tests\TestData\TestHtmlPage-ExpectedContent.txt
tests\TestData\TestHtmlPage.html = tests\TestData\TestHtmlPage.html
Expand Down
5 changes: 3 additions & 2 deletions src/Elzik.Breef.Api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080


# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Elzik.Breef.Api/Elzik.Breef.Api.csproj", "Elzik.Breef.Api/"]
Expand All @@ -26,4 +26,5 @@ RUN dotnet publish "./Elzik.Breef.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENV ASPNETCORE_ENVIRONMENT=Production
ENTRYPOINT ["dotnet", "Elzik.Breef.Api.dll"]
7 changes: 4 additions & 3 deletions src/Elzik.Breef.Api/Elzik.Breef.Api.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
Expand All @@ -10,14 +10,15 @@

<ItemGroup>
<PackageReference Include="AspNetCore.Authentication.ApiKey" Version="9.0.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.65.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.67.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Enrichers.AspNetCore" Version="1.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Elzik.Breef.Domain;
using Microsoft.AspNetCore.Diagnostics;
using Serilog.Context;
using System.Diagnostics;

namespace Elzik.Breef.Api.ExceptionHandling;

public static class ExceptionHandlingExtensions
{
public static void AddExceptionHandling(this IServiceCollection services)
{
services.AddProblemDetails();
}

public static void UseExceptionHandling(this WebApplication app)
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
var exception = exceptionHandlerFeature?.Error;
int statusCode;
string title;
string detail;

if (exception is ICallerFixableException)
{
if (string.IsNullOrWhiteSpace(exception.Message))
{
throw new InvalidOperationException(
"Caller-fixable exception must have a non-empty message for the caller to fix.",
exception);
}
statusCode = StatusCodes.Status400BadRequest;
title = "There was a problem with your request";
detail = exception.Message;
}
else
{
statusCode = StatusCodes.Status500InternalServerError;
title = "An error occurred while processing your request";
detail = "Contact your Breef administrator for a solution.";
}

var problemDetails = new Microsoft.AspNetCore.Mvc.ProblemDetails
{
Status = statusCode,
Title = title,
Detail = detail
};

if(Activity.Current != null)
{
problemDetails.Extensions["traceId"] = Activity.Current.TraceId.ToString();
}

context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(problemDetails);
});
});
}
}
14 changes: 12 additions & 2 deletions src/Elzik.Breef.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Elzik.Breef.Api.Auth;
using Elzik.Breef.Api.ExceptionHandling;
using Elzik.Breef.Api.Presentation;
using Elzik.Breef.Application;
using Elzik.Breef.Domain;
Expand All @@ -9,6 +10,8 @@
using Elzik.Breef.Infrastructure.ContentExtractors.Reddit.Client;
using Elzik.Breef.Infrastructure.ContentExtractors.Reddit.Client.Raw;
using Elzik.Breef.Infrastructure.Wallabag;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using Refit;
using Serilog;
Expand All @@ -32,7 +35,7 @@
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} [{TraceId}]{NewLine}{Exception}")
.ReadFrom.Configuration(configuration)
.CreateLogger();
builder.Host.UseSerilog();
Expand All @@ -45,11 +48,13 @@
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

builder.Services.AddExceptionHandling();

builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin()

Check warning on line 57 in src/Elzik.Breef.Api/Program.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Make sure this permissive CORS policy is safe here. (https://rules.sonarsource.com/csharp/RSPEC-5122)

Check warning on line 57 in src/Elzik.Breef.Api/Program.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Make sure this permissive CORS policy is safe here. (https://rules.sonarsource.com/csharp/RSPEC-5122)

Check warning on line 57 in src/Elzik.Breef.Api/Program.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Make sure this permissive CORS policy is safe here. (https://rules.sonarsource.com/csharp/RSPEC-5122)

Check warning on line 57 in src/Elzik.Breef.Api/Program.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Make sure this permissive CORS policy is safe here. (https://rules.sonarsource.com/csharp/RSPEC-5122)
.AllowAnyMethod()
.AllowAnyHeader();
});
Expand Down Expand Up @@ -137,11 +142,16 @@
builder.Services.AddTransient<IBreefGenerator, BreefGenerator>();

var app = builder.Build();

app.UseStatusCodePages();

app.UseExceptionHandling();

app.UseCors();
app.UseAuth();

app.MapGet("/health", () => Results.Ok(new { status = "Healthy" }))
.AllowAnonymous();
.AllowAnonymous();

app.AddBreefEndpoints();

Expand Down
3 changes: 0 additions & 3 deletions src/Elzik.Breef.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"profiles": {
"Local": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5079"
},
Expand Down
2 changes: 1 addition & 1 deletion src/Elzik.Breef.Application/Elzik.Breef.Application.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Elzik.Breef.Domain/Elzik.Breef.Domain.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="Microsoft.SemanticKernel.Abstractions" Version="1.66.0" />
<PackageReference Include="Microsoft.SemanticKernel.Abstractions" Version="1.67.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
9 changes: 9 additions & 0 deletions src/Elzik.Breef.Domain/ICallerFixableException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Elzik.Breef.Domain
{
/// <summary>
/// Marker interface for exceptions that can be fixed by the requester/user.
/// </summary>
public interface ICallerFixableException
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Elzik.Breef.Domain;

namespace Elzik.Breef.Infrastructure
{
public class CallerFixableHttpRequestException : HttpRequestException, ICallerFixableException
{
public CallerFixableHttpRequestException(string message, Exception? innerException = null)
: base(message, innerException)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Elzik.Breef.Domain;
using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Elzik.Breef.Infrastructure;

namespace Elzik.Breef.Infrastructure.ContentExtractors;

Expand All @@ -13,7 +15,15 @@ public override bool CanHandle(string webPageUrl)
protected override async Task<UntypedExtract> CreateUntypedExtractAsync(string webPageUrl)
{
var httpClient = httpClientFactory.CreateClient("BreefDownloader");
var html = await httpClient.GetStringAsync(webPageUrl);
string html;
try
{
html = await httpClient.GetStringAsync(webPageUrl);
}
catch (HttpRequestException ex)
{
throw new CallerFixableHttpRequestException($"Failed to download content for URL: {webPageUrl}", ex);
}
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(html);

Expand Down
17 changes: 8 additions & 9 deletions src/Elzik.Breef.Infrastructure/Elzik.Breef.Infrastructure.csproj
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.10" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="1.66.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="1.67.1" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.51.0-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.67.1" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
Expand All @@ -22,14 +22,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<ProjectReference Include="..\Elzik.Breef.Domain\Elzik.Breef.Domain.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="SummarisationInstructions\**\*.md"
Link="SummarisationInstructions\%(RecursiveDir)%(Filename)%(Extension)">
<None Include="SummarisationInstructions\**\*.md" Link="SummarisationInstructions\%(RecursiveDir)%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using Elzik.Breef.Api.Presentation;
using Elzik.Breef.Infrastructure.Wallabag;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Shouldly;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;

namespace Elzik.Breef.Api.Tests.Functional
namespace Elzik.Breef.Api.Tests.Functional.Breefs
{
public abstract class BreefTestsBase
{
Expand Down Expand Up @@ -78,8 +79,15 @@ public async Task Unauthorised()
challenge.Scheme.ShouldBe("ApiKey");
challenge.Parameter.ShouldNotBeNullOrEmpty();
challenge.Parameter.ShouldContain("BREEF-API-KEY");

var responseString = await response.Content.ReadAsStringAsync();
responseString.ShouldBeEmpty();
responseString.ShouldNotBeNullOrEmpty();
var problemDetails = JsonSerializer
.Deserialize<ProblemDetails>(responseString, JsonSerializerOptions);
problemDetails.ShouldNotBeNull();
problemDetails.Status.ShouldBe(401);
problemDetails.Title.ShouldBe("Unauthorized");
problemDetails.Type.ShouldNotBeNullOrWhiteSpace();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Diagnostics;
using Xunit.Abstractions;

namespace Elzik.Breef.Api.Tests.Functional;
namespace Elzik.Breef.Api.Tests.Functional.Breefs;

public class BreefTestsDocker : BreefTestsBase, IAsyncLifetime
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.Testing;

namespace Elzik.Breef.Api.Tests.Functional;
namespace Elzik.Breef.Api.Tests.Functional.Breefs;

public class BreefTestsNative : BreefTestsBase, IClassFixture<WebApplicationFactory<Program>>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand All @@ -17,18 +17,18 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.21" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.4.13" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.66.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.5.4" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.67.1" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Testcontainers" Version="4.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.analyzers" Version="1.24.0">
<PackageReference Include="xunit.analyzers" Version="1.25.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading