Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
fix: log field-level execution exceptions with stack traces
Partial-result field failures were caught into GraphQL errors without
calling the schema logger, so Error executing QueryRequest (and the
exception stack) disappeared after the 6.x execution refactor. Log via
ISchemaProvider.LogException from field catch sites and HandleException.

Co-authored-by: Cursor <cursoragent@cursor.com>
  • Loading branch information
alexbirch-xy and cursoragent committed Aug 3, 2026
commit 4261bcef9cf5bbf468dc78a9defc1913ad384c08
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ bin/
.idea/
obj/
.DS_Store
.tools/

# User-specific files
*.suo
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 6.1.5

## Fixes

- Field-level execution exceptions that are returned as GraphQL `errors` (partial results) are logged again via the schema `ILogger` with the original `Exception` and stack trace. In 6.x these failures are caught per field instead of bubbling to `SchemaProvider.HandleException`, so the previous `Error executing QueryRequest` log (with stack) was skipped. `ISchemaProvider.LogException(Exception)` is new for custom schema implementations; the built-in provider uses it from both top-level request handling and field catch sites. Custom `ISchemaProvider` implementations need to add it.

# 6.1.4

## Fixes
Expand Down
2 changes: 1 addition & 1 deletion src/EntityGraphQL.AspNet/EntityGraphQL.AspNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<AssemblyName>EntityGraphQL.AspNet</AssemblyName>
<PackageId>EntityGraphQL.AspNet</PackageId>
<LangVersion>14</LangVersion>
<PackageVersion>6.1.4</PackageVersion>
<PackageVersion>6.1.5</PackageVersion>
<Description>Contains ASP.NET extensions and middleware for EntityGraphQL</Description>
<Authors>Luke Murray</Authors>
<PackageProjectUrl>https://github.com/lukemurray/EntityGraphQL</PackageProjectUrl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,19 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
}
catch (EntityGraphQLFieldException fe)
{
Schema.LogException(fe);
allErrors.Add(new GraphQLError(Schema.AllowedExceptionMessage(fe), expandedField.BuildPath(), null));
}
catch (EntityGraphQLException ve)
{
Schema.LogException(ve);
allErrors.AddRange(Schema.GenerateErrors(ve));
if (expandedField.Field?.ReturnType.TypeNotNullable == false)
result[expandedField.Name] = null;
}
catch (TargetInvocationException tie) when (tie.InnerException != null)
{
Schema.LogException(tie.InnerException);
allErrors.AddRange(Schema.GenerateErrors(tie.InnerException, expandedField.Name));
if (expandedField.Field?.ReturnType.TypeNotNullable == false)
result[expandedField.Name] = null;
Expand All @@ -215,12 +218,16 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
// than wrapped in a TargetInvocationException - flatten it to distinct errors the same way
catch (AggregateException ae)
{
Schema.LogException(ae);
allErrors.AddRange(Schema.GenerateErrors(ae, expandedField.Name));
if (expandedField.Field?.ReturnType.TypeNotNullable == false)
result[expandedField.Name] = null;
}
catch (Exception ex)
{
// Log the original exception so the stack trace points at the resolver failure,
// not the EntityGraphQLException wrapper we build for the GraphQL error payload.
Schema.LogException(ex);
allErrors.AddRange(
Schema.GenerateErrors(
new EntityGraphQLException(
Expand All @@ -246,6 +253,7 @@ public ExecutableGraphQLStatement(ISchemaProvider schema, string? name, Expressi
// building the variables could cause this
catch (EntityGraphQLException ce)
{
Schema.LogException(ce);
allErrors.AddRange(Schema.GenerateErrors(ce));
}

Expand Down
2 changes: 1 addition & 1 deletion src/EntityGraphQL/EntityGraphQL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<LangVersion>14.0</LangVersion>
<AssemblyName>EntityGraphQL</AssemblyName>
<PackageId>EntityGraphQL</PackageId>
<PackageVersion>6.1.4</PackageVersion>
<PackageVersion>6.1.5</PackageVersion>
<Description>A GraphQL library for .NET. Compiles queries into .NET Expressions (LinqProvider) for runtime execution against object graphs. E.g. against an ORM data model (EntityFramework or others) or just an in-memory object.</Description>
<Authors>Luke Murray</Authors>
<PackageProjectUrl>https://github.com/lukemurray/EntityGraphQL</PackageProjectUrl>
Expand Down
7 changes: 7 additions & 0 deletions src/EntityGraphQL/Schema/ISchemaProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,11 @@ void AddMutationsFrom<TType>(SchemaBuilderOptions? options = null)
/// exception type is registered in AllowedExceptions / marked with AllowedExceptionAttribute.
/// </summary>
bool IsAllowedException(Exception exception);

/// <summary>
/// Logs an exception raised while executing a GraphQL request. Field-level failures that are returned as
/// GraphQL errors (partial results) should still call this so the Exception and stack trace are available
/// to the configured <c>ILogger</c>, matching top-level request failure logging.
/// </summary>
void LogException(Exception exception);
}
10 changes: 8 additions & 2 deletions src/EntityGraphQL/Schema/SchemaProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -586,15 +586,21 @@ private async Task<QueryResult> DoExecuteRequestAsync(

private QueryResult HandleException(Exception exception)
{
if (logger != null)
logErrorMessage(logger, exception);
LogException(exception);

var result = new QueryResult();
result.AddErrors(GenerateErrors(exception).Distinct());

return result;
}

/// <inheritdoc />
public void LogException(Exception exception)
{
if (logger != null)
logErrorMessage(logger, exception);
}

public IEnumerable<GraphQLError> GenerateErrors(Exception exception, string? fieldName = null)
{
return exception switch
Expand Down
46 changes: 46 additions & 0 deletions src/tests/EntityGraphQL.Tests/ErrorTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EntityGraphQL.Schema;
using Microsoft.Extensions.Logging;
using Xunit;

namespace EntityGraphQL.Tests;
Expand Down Expand Up @@ -650,4 +652,48 @@ public void QueryExecutionPartialResults_NonNullableField()
Assert.NotNull(error.Path);
Assert.Equal("nonNullableFail", error.Path[0]);
}

[Fact]
public void FieldExecutionException_IsLoggedWithException()
{
// Field failures returned as GraphQL errors (partial-results path) must still be logged with
// the Exception / stack trace, same as top-level HandleException for document failures.
var logger = new CapturingLogger<SchemaProvider<TestDataContext>>();
var schemaProvider = SchemaBuilder.FromObject<TestDataContext>(logger: logger);

var gql = new QueryRequest
{
Query =
@"{
people { error_UnexposedException }
}",
};

var results = schemaProvider.ExecuteRequestWithContext(gql, new TestDataContext().FillWithTestData(), null, null);

Assert.NotNull(results.Errors);
Assert.Contains("You should not see this message outside of Development", results.Errors[0].Message);

var errorLogs = logger.Entries.Where(e => e.Level == LogLevel.Error && e.Exception != null).ToList();
Assert.NotEmpty(errorLogs);
Assert.Contains(errorLogs, e => e.Exception!.ToString().Contains("You should not see this message outside of Development"));
var fieldFailLog = errorLogs.First(e => e.Exception!.ToString().Contains("You should not see this message outside of Development"));
Assert.False(string.IsNullOrEmpty(fieldFailLog.Exception!.GetBaseException().StackTrace));
Assert.Equal("Error executing QueryRequest", fieldFailLog.Message);
}

private sealed class CapturingLogger<T> : ILogger<T>
{
public List<(LogLevel Level, EventId EventId, Exception? Exception, string Message)> Entries { get; } = [];

public IDisposable? BeginScope<TState>(TState state)
where TState : notnull => null;

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
Entries.Add((logLevel, eventId, exception, formatter(state, exception)));
}
}
}