Skip to content

Support more ProblemDetails Titles out-of-the-box #43232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/Http/Headers/src/HeaderUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,8 @@ public static bool TryParseNonNegativeInt64(StringSegment value, out long result
return long.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result);
}

// Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation)
// See https://tools.ietf.org/html/rfc7231#section-5.3.1
// Strict and fast RFC9110 12.4.2 Quality value parser (and without memory allocation)
// See https://tools.ietf.org/html/rfc9110#section-12.4.2
// Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails).
internal static bool TryParseQualityDouble(StringSegment input, int startIndex, out double quality, out int length)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class HttpValidationProblemDetailsJsonConverterTest
public void Read_Works()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5";
var title = "Not found";
var status = 404;
var detail = "Product not found";
Expand Down Expand Up @@ -60,7 +60,7 @@ public void Read_Works()
public void Read_WithSomeMissingValues_Works()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5";
var title = "Not found";
var status = 404;
var traceId = "|37dd3dd5-4a9619f953c40a16.";
Expand Down Expand Up @@ -101,7 +101,7 @@ public void Read_WithSomeMissingValues_Works()
public void ReadUsingJsonSerializerWorks()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5";
var title = "Not found";
var status = 404;
var traceId = "|37dd3dd5-4a9619f953c40a16.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void Read_ThrowsIfJsonIsIncomplete()
public void Read_Works()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5";
var title = "Not found";
var status = 404;
var detail = "Product not found";
Expand Down Expand Up @@ -65,7 +65,7 @@ public void Read_Works()
public void Read_UsingJsonSerializerWorks()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5";
var title = "Not found";
var status = 404;
var detail = "Product not found";
Expand Down Expand Up @@ -96,7 +96,7 @@ public void Read_UsingJsonSerializerWorks()
public void Read_WithSomeMissingValues_Works()
{
// Arrange
var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4";
var type = "https://tools.ietf.org/html/rfc9110#section-15.5.5";
var title = "Not found";
var status = 404;
var traceId = "|37dd3dd5-4a9619f953c40a16.";
Expand Down Expand Up @@ -129,7 +129,7 @@ public void Write_Works()
var value = new ProblemDetails
{
Title = "Not found",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Type = "https://tools.ietf.org/html/rfc9110#section-15.5.5",
Status = 404,
Detail = "Product not found",
Instance = "http://example.com/products/14",
Expand Down Expand Up @@ -161,7 +161,7 @@ public void Write_WithSomeMissingContent_Works()
var value = new ProblemDetails
{
Title = "Not found",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Type = "https://tools.ietf.org/html/rfc9110#section-15.5.5",
Status = 404,
};
var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status}}}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as suggested by #36417 (comment)

<Reference Include="Microsoft.Net.Http.Headers" />
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async Task WriteAsync_Works()
Detail = "Custom Bad Request",
Instance = "Custom Bad Request",
Status = StatusCodes.Status400BadRequest,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1-custom",
Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom",
Title = "Custom Bad Request",
};
var problemDetailsContext = new ProblemDetailsContext()
Expand Down Expand Up @@ -100,7 +100,7 @@ public async Task WriteAsync_Applies_Defaults()
var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream);
Assert.NotNull(problemDetails);
Assert.Equal(StatusCodes.Status500InternalServerError, problemDetails.Status);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.6.1", problemDetails.Type);
Assert.Equal("An error occurred while processing your request.", problemDetails.Title);
}

Expand Down Expand Up @@ -133,13 +133,19 @@ await writer.WriteAsync(new ProblemDetailsContext()
var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream);
Assert.NotNull(problemDetails);
Assert.Equal(StatusCodes.Status406NotAcceptable, problemDetails.Status);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.1", problemDetails.Type);
Assert.Equal("Custom Title", problemDetails.Title);
Assert.Contains("new-extension", problemDetails.Extensions);
}

[Fact]
public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified()
[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
[InlineData(499, null, null)]
public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified(
int statusCode,
string title,
string type)
{
// Arrange
var writer = GetWriter();
Expand All @@ -150,16 +156,16 @@ public async Task WriteAsync_UsesStatusCode_FromProblemDetails_WhenSpecified()
await writer.WriteAsync(new ProblemDetailsContext()
{
HttpContext = context,
ProblemDetails = { Status = StatusCodes.Status400BadRequest }
ProblemDetails = { Status = statusCode }
});

//Assert
stream.Position = 0;
var problemDetails = await JsonSerializer.DeserializeAsync<ProblemDetails>(stream);
Assert.NotNull(problemDetails);
Assert.Equal(StatusCodes.Status400BadRequest, problemDetails.Status);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
Assert.Equal("Bad Request", problemDetails.Title);
Assert.Equal(statusCode, problemDetails.Status);
Assert.Equal(type, problemDetails.Type);
Assert.Equal(title, problemDetails.Title);
}

[Theory]
Expand Down
36 changes: 34 additions & 2 deletions src/Http/Http.Results/test/JsonResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails()
Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<ProblemDetails>(stream);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", responseDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.6.1", responseDetails.Type);
Assert.Equal("An error occurred while processing your request.", responseDetails.Title);
Assert.Equal(StatusCodes.Status500InternalServerError, responseDetails.Status);
}
Expand Down Expand Up @@ -160,11 +160,43 @@ public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails()
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<HttpValidationProblemDetails>(stream);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", responseDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.1", responseDetails.Type);
Assert.Equal("One or more validation errors occurred.", responseDetails.Title);
Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status);
}

[Fact]
public async Task ExecuteAsync_UsesDefaults_HttpStatusCodesWithoutTypes()
{
// Arrange
var details = new ProblemDetails()
{
Status = StatusCodes.Status418ImATeapot,
};

var result = new ProblemHttpResult(details);
var stream = new MemoryStream();
var httpContext = new DefaultHttpContext()
{
RequestServices = CreateServices(),
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.Equal(StatusCodes.Status418ImATeapot, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<HttpValidationProblemDetails>(stream);
Assert.Null(responseDetails.Type);
Assert.Equal("I'm a teapot", responseDetails.Title);
Assert.Equal(StatusCodes.Status418ImATeapot, responseDetails.Status);
}

[Fact]
public async Task ExecuteAsync_SetsProblemDetailsStatus_ForValidationProblemDetails()
{
Expand Down
3 changes: 1 addition & 2 deletions src/Http/Http.Results/test/OkResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -43,7 +42,7 @@ public async Task OkObjectResult_ExecuteAsync_SetsStatusCode()
public void PopulateMetadata_AddsResponseTypeMetadata()
{
// Arrange
Ok MyApi() { throw new NotImplementedException(); }
static Ok MyApi() { throw new NotImplementedException(); }
var metadata = new List<object>();
var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance);

Expand Down
36 changes: 34 additions & 2 deletions src/Http/Http.Results/test/ProblemResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails()
Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<ProblemDetails>(stream);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", responseDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.6.1", responseDetails.Type);
Assert.Equal("An error occurred while processing your request.", responseDetails.Title);
Assert.Equal(StatusCodes.Status500InternalServerError, responseDetails.Status);
}
Expand Down Expand Up @@ -64,11 +64,43 @@ public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails()
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<HttpValidationProblemDetails>(stream);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", responseDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.1", responseDetails.Type);
Assert.Equal("One or more validation errors occurred.", responseDetails.Title);
Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status);
}

[Fact]
public async Task ExecuteAsync_SetsTitleFromReasonPhrases_WhenNotInDefaults()
{
// Arrange
var details = new ProblemDetails()
{
Status = StatusCodes.Status418ImATeapot,
};

var result = new ProblemHttpResult(details);
var stream = new MemoryStream();
var httpContext = new DefaultHttpContext()
{
RequestServices = CreateServices(),
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.Equal(StatusCodes.Status418ImATeapot, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<HttpValidationProblemDetails>(stream);
Assert.Null(responseDetails.Type);
Assert.Equal("I'm a teapot", responseDetails.Title);
Assert.Equal(StatusCodes.Status418ImATeapot, responseDetails.Status);
}

[Fact]
public async Task ExecuteAsync_IncludeErrors_ForValidationProblemDetails()
{
Expand Down
25 changes: 24 additions & 1 deletion src/Http/Http.Results/test/ResultsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,29 @@ public void Problem_WithArgs_ResultHasCorrectValues()
Assert.Equal(extensions, result.ProblemDetails.Extensions);
}

[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
[InlineData(499, null, null)]
public void Problem_WithOnlyHttpStatus_ResultHasCorrectValues(
int statusCode,
string title,
string type)
{
// Act
var result = Results.Problem(statusCode: statusCode) as ProblemHttpResult;

// Assert
Assert.Null(result.ProblemDetails.Detail);
Assert.Null(result.ProblemDetails.Instance);
Assert.Equal("application/problem+json", result.ContentType);
Assert.Equal(statusCode, result.StatusCode);
Assert.Equal(title, result.ProblemDetails.Title);
Assert.Equal(type, result.ProblemDetails.Type);
Assert.NotNull(result.ProblemDetails.Extensions);
Assert.Empty(result.ProblemDetails.Extensions);
}

[Fact]
public void Problem_WithNoArgs_ResultHasCorrectValues()
{
Expand All @@ -943,7 +966,7 @@ public void Problem_WithNoArgs_ResultHasCorrectValues()
Assert.Equal("application/problem+json", result.ContentType);
Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode);
Assert.Equal("An error occurred while processing your request.", result.ProblemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", result.ProblemDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.6.1", result.ProblemDetails.Type);
Assert.Empty(result.ProblemDetails.Extensions);
}

Expand Down
24 changes: 23 additions & 1 deletion src/Http/Http.Results/test/TypedResultsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,28 @@ public void Problem_WithArgs_ResultHasCorrectValues()
Assert.Equal(extensions, result.ProblemDetails.Extensions);
}

[Theory]
[InlineData(StatusCodes.Status400BadRequest, "Bad Request", "https://tools.ietf.org/html/rfc9110#section-15.5.1")]
[InlineData(StatusCodes.Status418ImATeapot, "I'm a teapot", null)]
public void Problem_WithOnlyHttpStatus_ResultHasCorrectValues(
int statusCode,
string title,
string type)
{
// Act
var result = TypedResults.Problem(statusCode: statusCode);

// Assert
Assert.Null(result.ProblemDetails.Detail);
Assert.Null(result.ProblemDetails.Instance);
Assert.Equal("application/problem+json", result.ContentType);
Assert.Equal(statusCode, result.StatusCode);
Assert.Equal(title, result.ProblemDetails.Title);
Assert.Equal(type, result.ProblemDetails.Type);
Assert.NotNull(result.ProblemDetails.Extensions);
Assert.Empty(result.ProblemDetails.Extensions);
}

[Fact]
public void Problem_WithNoArgs_ResultHasCorrectValues()
{
Expand All @@ -885,7 +907,7 @@ public void Problem_WithNoArgs_ResultHasCorrectValues()
Assert.Equal("application/problem+json", result.ContentType);
Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode);
Assert.Equal("An error occurred while processing your request.", result.ProblemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", result.ProblemDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.6.1", result.ProblemDetails.Type);
Assert.Empty(result.ProblemDetails.Extensions);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@

using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json.Linq;

namespace Microsoft.AspNetCore.Http.HttpResults;

Expand Down Expand Up @@ -39,7 +37,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails()
Assert.Equal(details, result.ProblemDetails);
stream.Position = 0;
var responseDetails = JsonSerializer.Deserialize<ProblemDetails>(stream);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", responseDetails.Type);
Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.1", responseDetails.Type);
Assert.Equal("One or more validation errors occurred.", responseDetails.Title);
Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status);
}
Expand Down
Loading