diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index 59e62d1b56f3..4e44ae480dc4 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -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) { diff --git a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs index ab4408dd7561..06ca5f330c07 100644 --- a/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -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"; @@ -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."; @@ -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."; diff --git a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs index 995655ba2947..81e4abee0635 100644 --- a/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Abstractions/test/ProblemDetailsJsonConverterTest.cs @@ -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"; @@ -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"; @@ -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."; @@ -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", @@ -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}}}"; diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index ed2fbb631a9b..2e682a849df8 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index 997641d42f44..371b94d4ea59 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -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() @@ -100,7 +100,7 @@ public async Task WriteAsync_Applies_Defaults() var problemDetails = await JsonSerializer.DeserializeAsync(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); } @@ -133,13 +133,19 @@ await writer.WriteAsync(new ProblemDetailsContext() var problemDetails = await JsonSerializer.DeserializeAsync(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(); @@ -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(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] diff --git a/src/Http/Http.Results/test/JsonResultTests.cs b/src/Http/Http.Results/test/JsonResultTests.cs index af600b795ae3..9ee546119010 100644 --- a/src/Http/Http.Results/test/JsonResultTests.cs +++ b/src/Http/Http.Results/test/JsonResultTests.cs @@ -131,7 +131,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode); stream.Position = 0; var responseDetails = JsonSerializer.Deserialize(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); } @@ -160,11 +160,43 @@ public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); stream.Position = 0; var responseDetails = JsonSerializer.Deserialize(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(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() { diff --git a/src/Http/Http.Results/test/OkResultTests.cs b/src/Http/Http.Results/test/OkResultTests.cs index 4d3add27c9ec..ab3e9e8f2398 100644 --- a/src/Http/Http.Results/test/OkResultTests.cs +++ b/src/Http/Http.Results/test/OkResultTests.cs @@ -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; @@ -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(); var context = new EndpointMetadataContext(((Delegate)MyApi).GetMethodInfo(), metadata, EmptyServiceProvider.Instance); diff --git a/src/Http/Http.Results/test/ProblemResultTests.cs b/src/Http/Http.Results/test/ProblemResultTests.cs index d8a262efd105..4862638f27ed 100644 --- a/src/Http/Http.Results/test/ProblemResultTests.cs +++ b/src/Http/Http.Results/test/ProblemResultTests.cs @@ -35,7 +35,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode); stream.Position = 0; var responseDetails = JsonSerializer.Deserialize(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); } @@ -64,11 +64,43 @@ public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); stream.Position = 0; var responseDetails = JsonSerializer.Deserialize(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(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() { diff --git a/src/Http/Http.Results/test/ResultsTests.cs b/src/Http/Http.Results/test/ResultsTests.cs index 7d07c3337523..2afbf5f39e24 100644 --- a/src/Http/Http.Results/test/ResultsTests.cs +++ b/src/Http/Http.Results/test/ResultsTests.cs @@ -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() { @@ -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); } diff --git a/src/Http/Http.Results/test/TypedResultsTests.cs b/src/Http/Http.Results/test/TypedResultsTests.cs index 4b93724d04ad..f4d567d15bf8 100644 --- a/src/Http/Http.Results/test/TypedResultsTests.cs +++ b/src/Http/Http.Results/test/TypedResultsTests.cs @@ -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() { @@ -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); } diff --git a/src/Http/Http.Results/test/ValidationProblemResultTests.cs b/src/Http/Http.Results/test/ValidationProblemResultTests.cs index 55e672bb311e..06b1f984d68a 100644 --- a/src/Http/Http.Results/test/ValidationProblemResultTests.cs +++ b/src/Http/Http.Results/test/ValidationProblemResultTests.cs @@ -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; @@ -39,7 +37,7 @@ public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() Assert.Equal(details, result.ProblemDetails); stream.Position = 0; var responseDetails = JsonSerializer.Deserialize(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); } diff --git a/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs b/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs index 3b6020a52dcb..50eae2a1225e 100644 --- a/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs +++ b/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs @@ -2313,7 +2313,7 @@ public void ValidationProblemDetails_Works() Assert.Equal(400, badRequestResult.StatusCode); Assert.Equal(400, problemDetails.Status); Assert.Equal("One or more validation errors occurred.", problemDetails.Title); - 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("some-trace", problemDetails.Extensions["traceId"]); Assert.Equal(new[] { "error1" }, problemDetails.Errors["key1"]); } @@ -2411,7 +2411,7 @@ public void ProblemDetails_Works() Assert.Equal(500, actionResult.StatusCode); Assert.Equal(500, problemDetails.Status); Assert.Equal("An error occurred while processing your request.", problemDetails.Title); - 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("some-trace", problemDetails.Extensions["traceId"]); } @@ -2437,7 +2437,7 @@ public void ProblemDetails_UsesPassedInValues() Assert.Equal(500, actionResult.StatusCode); Assert.Equal(500, problemDetails.Status); Assert.Equal(title, problemDetails.Title); - 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(detail, problemDetails.Detail); } @@ -2473,7 +2473,7 @@ private static ApiBehaviorOptions GetApiBehaviorOptions() [400] = new ClientErrorData { Title = "One or more validation errors occurred.", - Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + Link = "https://tools.ietf.org/html/rfc9110#section-15.5.1" }, [422] = new ClientErrorData { @@ -2483,7 +2483,7 @@ private static ApiBehaviorOptions GetApiBehaviorOptions() [500] = new ClientErrorData { Title = "An error occurred while processing your request.", - Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1" + Link = "https://tools.ietf.org/html/rfc9110#section-15.6.1" } } }; diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs index d1e7be87785f..e7e8f597b532 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs @@ -16,7 +16,7 @@ public class ApiBehaviorOptionsSetupTest public void Configure_AddsClientErrorMappings() { // Arrange - var expected = new[] { 400, 401, 403, 404, 405, 406, 409, 415, 422, 500, }; + var expected = new[] { 400, 401, 403, 404, 405, 406, 408, 409, 412, 415, 422, 426, 500, 502, 503, 504 }; var optionsSetup = new ApiBehaviorOptionsSetup(); var options = new ApiBehaviorOptions(); @@ -44,7 +44,7 @@ public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblem var problemDetails = Assert.IsType(badRequest.Value); Assert.Equal(400, problemDetails.Status); Assert.Equal("One or more validation errors occurred.", problemDetails.Title); - 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); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs index e918bcf5c49e..4d18639c4633 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs @@ -28,7 +28,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() diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs index 122f2e2893c4..2695b167ff25 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs @@ -21,7 +21,7 @@ public void CreateProblemDetails_DefaultValues() // Assert Assert.Equal(500, problemDetails.Status); Assert.Equal("An error occurred while processing your request.", problemDetails.Title); - 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.Null(problemDetails.Instance); Assert.Null(problemDetails.Detail); Assert.Collection( @@ -42,7 +42,7 @@ public void CreateProblemDetails_WithStatusCode() // Assert Assert.Equal(406, problemDetails.Status); Assert.Equal("Not Acceptable", problemDetails.Title); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type); + Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.7", problemDetails.Type); Assert.Null(problemDetails.Instance); Assert.Null(problemDetails.Detail); Assert.Collection( @@ -65,7 +65,7 @@ public void CreateProblemDetails_WithDetailAndTitle() // Assert Assert.Equal(406, problemDetails.Status); Assert.Equal(title, problemDetails.Title); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type); + Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.7", problemDetails.Type); Assert.Null(problemDetails.Instance); Assert.Equal(detail, problemDetails.Detail); Assert.Collection( @@ -88,7 +88,7 @@ public void CreateValidationProblemDetails_DefaultValues() // Assert Assert.Equal(400, problemDetails.Status); Assert.Equal("One or more validation errors occurred.", problemDetails.Title); - 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.Null(problemDetails.Instance); Assert.Null(problemDetails.Detail); Assert.Collection( @@ -150,7 +150,7 @@ public void CreateValidationProblemDetails_WithTitleAndInstance() // Assert Assert.Equal(400, problemDetails.Status); Assert.Equal(title, problemDetails.Title); - 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(instance, problemDetails.Instance); Assert.Null(problemDetails.Detail); Assert.Collection( diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs index 5abdf6f37068..b7b877bf38f0 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ValidationProblemDetailsJsonConverterTest.cs @@ -14,7 +14,7 @@ public class ValidationProblemDetailsJsonConverterTest 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"; @@ -59,7 +59,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."; @@ -100,7 +100,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."; diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs index 98c6cb955587..a090a853a018 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs @@ -54,7 +54,7 @@ await response.Content.ReadAsStringAsync(), }); Assert.Equal("One or more validation errors occurred.", problemDetails.Title); - 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.Collection( problemDetails.Errors.OrderBy(kvp => kvp.Key), diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs index cb6c44c72723..2cb2a5b490d4 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs @@ -177,7 +177,7 @@ public virtual async Task Formatting_ProblemDetails() await response.AssertStatusCodeAsync(HttpStatusCode.NotFound); var obj = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.4", obj.Value("type")); + Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.5", obj.Value("type")); Assert.Equal("Not Found", obj.Value("title")); Assert.Equal("404", obj.Value("status")); Assert.NotNull(obj.Value("traceId")); diff --git a/src/Mvc/test/Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs b/src/Mvc/test/Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs index 27fe12a7448e..e5eb5d3eaf1f 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs @@ -228,7 +228,7 @@ public async Task ProblemDetails_IsSerialized() var root = XDocument.Parse(content).Root; Assert.Equal("404", root.Element(root.Name.Namespace.GetName("status"))?.Value); Assert.Equal("Not Found", root.Element(root.Name.Namespace.GetName("title"))?.Value); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.4", root.Element(root.Name.Namespace.GetName("type"))?.Value); + Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.5", root.Element(root.Name.Namespace.GetName("type"))?.Value); // Activity is not null Assert.NotNull(root.Element(root.Name.Namespace.GetName("traceId"))?.Value); } diff --git a/src/Mvc/test/Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs b/src/Mvc/test/Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs index c97617c0b774..b8f1f1578c49 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs @@ -197,7 +197,7 @@ public async Task ProblemDetails_IsSerialized() var expected = "" + "404" + "Not Found" + - "https://tools.ietf.org/html/rfc7231#section-6.5.4" + + "https://tools.ietf.org/html/rfc9110#section-15.5.5" + $"{Activity.Current.Id}" + ""; @@ -210,7 +210,7 @@ public async Task ProblemDetails_IsSerialized() var root = XDocument.Parse(content).Root; Assert.Equal("404", root.Element(root.Name.Namespace.GetName("status"))?.Value); Assert.Equal("Not Found", root.Element(root.Name.Namespace.GetName("title"))?.Value); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.4", root.Element(root.Name.Namespace.GetName("type"))?.Value); + Assert.Equal("https://tools.ietf.org/html/rfc9110#section-15.5.5", root.Element(root.Name.Namespace.GetName("type"))?.Value); // Activity is not null Assert.NotNull(root.Element(root.Name.Namespace.GetName("traceId"))?.Value); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 87998e408288..1a5a908bd2ed 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -442,7 +442,7 @@ private void OnAuthorityFormTarget(HttpMethod method, Span target) } // The authority-form of request-target is only used for CONNECT - // requests (https://tools.ietf.org/html/rfc7231#section-4.3.6). + // requests (https://tools.ietf.org/html/rfc9110#section-9.3.6). if (method != HttpMethod.Connect) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.ConnectMethodRequired); @@ -486,7 +486,7 @@ private void OnAsteriskFormTarget(HttpMethod method) _requestTargetForm = HttpRequestTarget.AsteriskForm; // The asterisk-form of request-target is only used for a server-wide - // OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7). + // OPTIONS request (https://tools.ietf.org/html/rfc9110#section-9.3.7). if (method != HttpMethod.Options) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.OptionsMethodRequired); diff --git a/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs index 0a4ad2c2c74d..f02a32dcc202 100644 --- a/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs +++ b/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; namespace Microsoft.AspNetCore.Http; @@ -11,49 +12,61 @@ internal static class ProblemDetailsDefaults { [400] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.1", + "https://tools.ietf.org/html/rfc9110#section-15.5.1", "Bad Request" ), [401] = ( - "https://tools.ietf.org/html/rfc7235#section-3.1", + "https://tools.ietf.org/html/rfc9110#section-15.5.2", "Unauthorized" ), [403] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.3", + "https://tools.ietf.org/html/rfc9110#section-15.5.4", "Forbidden" ), [404] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.4", + "https://tools.ietf.org/html/rfc9110#section-15.5.5", "Not Found" ), [405] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.5", + "https://tools.ietf.org/html/rfc9110#section-15.5.6", "Method Not Allowed" ), [406] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.6", + "https://tools.ietf.org/html/rfc9110#section-15.5.7", "Not Acceptable" ), + [408] = + ( + "https://tools.ietf.org/html/rfc9110#section-15.5.9", + "Request Timeout" + ), + [409] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.8", + "https://tools.ietf.org/html/rfc9110#section-15.5.10", "Conflict" ), + [412] = + ( + "https://tools.ietf.org/html/rfc9110#section-15.5.13", + "Precondition Failed" + ), + [415] = ( - "https://tools.ietf.org/html/rfc7231#section-6.5.13", + "https://tools.ietf.org/html/rfc9110#section-15.5.16", "Unsupported Media Type" ), @@ -63,11 +76,35 @@ internal static class ProblemDetailsDefaults "Unprocessable Entity" ), + [426] = + ( + "https://tools.ietf.org/html/rfc9110#section-15.5.22", + "Upgrade Required" + ), + [500] = ( - "https://tools.ietf.org/html/rfc7231#section-6.6.1", + "https://tools.ietf.org/html/rfc9110#section-15.6.1", "An error occurred while processing your request." ), + + [502] = + ( + "https://tools.ietf.org/html/rfc9110#section-15.6.3", + "Bad Gateway" + ), + + [503] = + ( + "https://tools.ietf.org/html/rfc9110#section-15.6.4", + "Service Unavailable" + ), + + [504] = + ( + "https://tools.ietf.org/html/rfc9110#section-15.6.5", + "Gateway Timeout" + ), }; public static void Apply(ProblemDetails problemDetails, int? statusCode) @@ -89,10 +126,19 @@ public static void Apply(ProblemDetails problemDetails, int? statusCode) } } - if (Defaults.TryGetValue(problemDetails.Status.Value, out var defaults)) + var status = problemDetails.Status.GetValueOrDefault(); + if (Defaults.TryGetValue(status, out var defaults)) { problemDetails.Title ??= defaults.Title; problemDetails.Type ??= defaults.Type; } + else if (problemDetails.Title is null) + { + var reasonPhrase = ReasonPhrases.GetReasonPhrase(status); + if (!string.IsNullOrEmpty(reasonPhrase)) + { + problemDetails.Title = reasonPhrase; + } + } } }