Set trace id in ProblemDetalsClientErrorFactory

This commit is contained in:
Pranav K 2018-08-28 16:48:45 -07:00
parent b649133eec
commit 82a01a414d
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
9 changed files with 307 additions and 72 deletions

View File

@ -2,12 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
{
private static readonly string TraceIdentifierKey = "traceId";
private readonly ApiBehaviorOptions _options;
public ProblemDetailsClientErrorFactory(IOptions<ApiBehaviorOptions> options)
@ -28,6 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
problemDetails.Title = errorData.Title;
problemDetails.Type = errorData.Link;
SetTraceId(actionContext, problemDetails);
}
return new ObjectResult(problemDetails)
@ -40,5 +44,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
},
};
}
internal static void SetTraceId(ActionContext actionContext, ProblemDetails problemDetails)
{
var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier;
problemDetails.Extensions[TraceIdentifierKey] = traceId;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
@ -128,9 +129,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return result;
}
private static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
internal static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
{
var result = new BadRequestObjectResult(new ValidationProblemDetails(context.ModelState));
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status400BadRequest,
};
ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails);
var result = new BadRequestObjectResult(problemDetails);
result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+xml");

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Xunit;
@ -22,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
}));
// Act
var result = factory.GetClientError(new ActionContext(), clientError);
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
@ -49,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
}));
// Act
var result = factory.GetClientError(new ActionContext(), clientError);
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
@ -61,5 +63,67 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Assert.Null(problemDetails.Detail);
Assert.Null(problemDetails.Instance);
}
[Fact]
public void GetClientError_UsesActivityId_ToSetTraceId()
{
// Arrange
using (new ActivityReplacer())
{
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal(Activity.Current.Id, problemDetails.Extensions["traceId"]);
}
}
[Fact]
public void GetClientError_UsesHttpContext_ToSetTraceIdIfActivityIdIsNotSet()
{
// Arrange
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal("42", problemDetails.Extensions["traceId"]);
}
private static ActionContext GetActionContext()
{
return new ActionContext
{
HttpContext = new DefaultHttpContext
{
TraceIdentifier = "42",
}
};
}
}
}

View File

@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -97,5 +100,64 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
Assert.Same(expected, options.InvalidModelStateResponseFactory);
}
[Fact]
public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails()
{
// Arrange
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
};
// Act
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, badRequest.ContentTypes.OrderBy(c => c));
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
Assert.Equal(400, problemDetails.Status);
}
[Fact]
public void ProblemDetailsInvalidModelStateResponse_SetsTraceId()
{
// Arrange
using (new ActivityReplacer())
{
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
};
// Act
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
Assert.Equal(Activity.Current.Id, problemDetails.Extensions["traceId"]);
}
}
[Fact]
public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull()
{
// Arrange
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
};
// Act
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
Assert.Equal("42", problemDetails.Extensions["traceId"]);
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Mvc
{
public class ActivityReplacer : IDisposable
{
private readonly Activity _activity;
public ActivityReplacer()
{
_activity = new Activity("Test");
_activity.Start();
}
public void Dispose()
{
Debug.Assert(Activity.Current == _activity);
_activity.Stop();
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -35,37 +36,48 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
{
// Arrange
var contactModel = new Contact
using (new ActivityReplacer())
{
Name = "Abc",
City = "Redmond",
State = "WA",
Zip = "Invalid",
};
var contactString = JsonConvert.SerializeObject(contactModel);
// Act
var response = await Client.PostAsJsonAsync("/contact", contactModel);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
Assert.Equal("application/problem+json", response.Content.Headers.ContentType.MediaType);
var problemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(await response.Content.ReadAsStringAsync());
Assert.Collection(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
var contactModel = new Contact
{
Assert.Equal("Name", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Name must be a string with a minimum length of 5 and a maximum length of 30.", error);
},
kvp =>
{
Assert.Equal("Zip", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Zip must match the regular expression '\\d{5}'.", error);
}
);
Name = "Abc",
City = "Redmond",
State = "WA",
Zip = "Invalid",
};
var contactString = JsonConvert.SerializeObject(contactModel);
// Act
var response = await Client.PostAsJsonAsync("/contact", contactModel);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
Assert.Equal("application/problem+json", response.Content.Headers.ContentType.MediaType);
var problemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(await response.Content.ReadAsStringAsync());
Assert.Collection(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("Name", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Name must be a string with a minimum length of 5 and a maximum length of 30.", error);
},
kvp =>
{
Assert.Equal("Zip", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Zip must match the regular expression '\\d{5}'.", error);
}
);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("traceId", kvp.Key);
Assert.Equal(Activity.Current.Id, kvp.Value);
});
}
}
[Fact]
@ -253,21 +265,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
[Fact]
public async Task ClientErrorResultFilterExecutesForStatusCodeResults()
{
// Act
var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult");
using (new ActivityReplacer())
{
// Act
var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
Assert.Equal(404, problemDetails.Status);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
Assert.Equal(404, problemDetails.Status);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("traceId", kvp.Key);
Assert.Equal(Activity.Current.Id, kvp.Value);
});
}
}
[Fact]
public async Task SerializingProblemDetails_IgnoresNullValuedProperties()
{
// Arrange
var expected = new[] { "status", "title", "type" };
var expected = new[] { "status", "title", "traceId", "type" };
// Act
var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult");
@ -310,7 +332,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Error", validationProblemDetails.Title);
Assert.Equal(400, validationProblemDetails.Status);
Assert.Equal("27", validationProblemDetails.Extensions["tracking-id"]);
Assert.Collection(
validationProblemDetails.Extensions,
kvp =>
{
Assert.Equal("tracking-id", kvp.Key);
Assert.Equal("27", kvp.Value);
});
Assert.Collection(
validationProblemDetails.Errors,
kvp =>

View File

@ -10,6 +10,7 @@
<ItemGroup>
<Compile Include="..\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test\XmlAssert.cs" />
<Compile Include="..\Microsoft.AspNetCore.Mvc.Core.TestCommon\ActivityReplacer.cs" />
<EmbeddedResource Include="compiler\resources\**\*" />
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@ -213,15 +214,23 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ProblemDetails_IsSerialized()
{
// Arrange
var expected = @"<ProblemDetails><Status>404</Status><Title>Not Found</Title><Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type></ProblemDetails>";
using (new ActivityReplacer())
{
var expected = "<ProblemDetails>" +
"<Status>404</Status>" +
"<Title>Not Found</Title>" +
"<Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"</ProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningClientErrorStatusCodeResult");
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningClientErrorStatusCodeResult");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]
@ -244,16 +253,25 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ValidationProblemDetails_IsSerialized()
{
// Arrange
var expected = @"<ValidationProblemDetails><Status>400</Status><Title>One or more validation errors occurred.</Title>
<MVC-Errors><State>The State field is required.</State></MVC-Errors></ValidationProblemDetails>";
using (new ActivityReplacer())
{
var expected = "<ValidationProblemDetails>" +
"<Status>400</Status>" +
"<Title>One or more validation errors occurred.</Title>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"<MVC-Errors>" +
"<State>The State field is required.</State>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationProblem");
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationProblem");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@ -188,15 +189,23 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ProblemDetails_IsSerialized()
{
// Arrange
var expected = @"<ProblemDetails><Status>404</Status><Title>Not Found</Title><Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type></ProblemDetails>";
using (new ActivityReplacer())
{
var expected = "<ProblemDetails>" +
"<Status>404</Status>" +
"<Title>Not Found</Title>" +
"<Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"</ProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningClientErrorStatusCodeResult");
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningClientErrorStatusCodeResult");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]
@ -219,16 +228,25 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ValidationProblemDetails_IsSerialized()
{
// Arrange
var expected = @"<ValidationProblemDetails><Status>400</Status><Title>One or more validation errors occurred.</Title>
<MVC-Errors><State>The State field is required.</State></MVC-Errors></ValidationProblemDetails>";
using (new ActivityReplacer())
{
var expected = "<ValidationProblemDetails>" +
"<Status>400</Status>" +
"<Title>One or more validation errors occurred.</Title>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"<MVC-Errors>" +
"<State>The State field is required.</State>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationProblem");
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationProblem");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]