Skip to content
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

Add UseIgnoreDisconnections helper #63

Merged
merged 1 commit into from
Aug 19, 2024
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,26 @@ are rejected over HTTP GET connections. Derive from `GraphQLHttpMiddleware` and

As would be expected, subscription requests are only allowed over WebSocket channels.

### Excessive `OperationCanceledException`s

When hosting a WebSockets endpoint, it may be common for clients to simply disconnect rather
than gracefully terminating the connection — most specifically when the client is a web browser.
If you log exceptions, you may notice an `OperationCanceledException` logged any time this occurs.

In some scenarios you may wish to log these exceptions — for instance, when the GraphQL endpoint is
used in server-to-server communications — but if you wish to ignore these exceptions, simply call
`app.UseIgnoreDisconnections();` immediately after any exception handling or logging configuration calls.
This will consume any `OperationCanceledException`s when `HttpContext.RequestAborted` is signaled — for
a WebSocket request or any other request.

```csharp
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseIgnoreDisconnections();
app.UseWebSockets();
app.UseGraphQL();
```

### Handling form data for POST requests

The GraphQL over HTTP specification does not outline a procedure for transmitting GraphQL requests via
Expand Down
20 changes: 20 additions & 0 deletions src/GraphQL.AspNetCore3/GraphQLHttpApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,24 @@ public static IApplicationBuilder UseGraphQL<TMiddleware>(this IApplicationBuild
context => context.Request.Path.Equals(path),
b => b.UseMiddleware<TMiddleware>(args));
}

/// <summary>
/// Ignores <see cref="OperationCanceledException"/> exceptions when
/// <see cref="HttpContext.RequestAborted"/> is signaled.
/// </summary>
/// <remarks>
/// Place this immediately after exception handling or logging middleware, such as
/// <see cref="DeveloperExceptionPageExtensions.UseDeveloperExceptionPage(IApplicationBuilder)">UseDeveloperExceptionPage</see>.
/// </remarks>
public static IApplicationBuilder UseIgnoreDisconnections(this IApplicationBuilder builder)
{
return builder.Use(static next => {
return async context => {
try {
await next(context);
} catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) {
}
};
});
}
}
1 change: 1 addition & 0 deletions src/Tests.ApiApprovals/GraphQL.AspNetCore3.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ namespace GraphQL.AspNetCore3
where TSchema : GraphQL.Types.ISchema { }
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseGraphQL<TMiddleware>(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder, string path = "/graphql", params object[] args)
where TMiddleware : GraphQL.AspNetCore3.GraphQLHttpMiddleware { }
public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseIgnoreDisconnections(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder) { }
}
public static class GraphQLHttpEndpointRouteBuilderExtensions
{
Expand Down
35 changes: 35 additions & 0 deletions src/Tests/BuilderMethodTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,41 @@ public async Task Basic(string url)
await VerifyAsync(url);
}

[Fact]
public async Task Basic_WithUseIgnoreDisconnections()
{
_hostBuilder.Configure(app => {
app.UseIgnoreDisconnections();
app.UseWebSockets();
app.UseGraphQL();
});
await VerifyAsync();
}

[Fact]
public async Task UseIgnoreDisconnections_Fail()
{
using var cts = new CancellationTokenSource();
RequestDelegate func = next => throw new OperationCanceledException();
var builderMock = new Mock<IApplicationBuilder>();
builderMock.Setup(x => x.Use(It.IsAny<Func<RequestDelegate, RequestDelegate>>())).Returns<Func<RequestDelegate, RequestDelegate>>(
d => {
func = d(func);
return builderMock.Object;
});
builderMock.Object.UseIgnoreDisconnections();
var context = new DefaultHttpContext() {
RequestAborted = cts.Token,
};

// cts is not canceled here so OCE should pass through
await Should.ThrowAsync<OperationCanceledException>(() => func(context));

cts.Cancel();
// cts is canceled so OCE should be consumed
await func(context);
}

[Theory]
[InlineData("/graphql/")]
[InlineData("/graphql/more")]
Expand Down
Loading