Skip to content

Instantly share code, notes, and snippets.

@mattiasnordqvist
Last active November 30, 2023 21:04
Show Gist options
  • Save mattiasnordqvist/8956ee550b5c0e4ba8923542d782578b to your computer and use it in GitHub Desktop.
Save mattiasnordqvist/8956ee550b5c0e4ba8923542d782578b to your computer and use it in GitHub Desktop.
PersistedQueryNotFoundIsNotAnError
// Program.cs
app.UseMiddleware<PersistedQueryNotFoundIsNotAnError.ResponseBodyLoggingMiddleware>();
...
builder.RegisterPersistedQueryNotFoundLogHandling();
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Channel;
/// <summary>
/// Services and middleware to make sure that PersistedQueryNotFound are not logged as failed requests in application insights.
/// They clog the logs and are not really errors, but expected due to usage of autopersisted queries.
/// This was not a problem until a recent update of graphql, which now returns a 400 instead of a 200 in the case of a missing persisted query.
///
/// By the way, we tried prepersisting queries in backend at build time instead, but failed because apollo and graphql/codegen didn't generate the same sha256 hash for the same queries. We couldn't figure out why. :(
///
/// Even though I set request.Success = true at multiple occasions, application insights in VS still logs the request as failed.
/// This is a problem with VS, in actual application insights,it works fine
/// </summary>
public static class PersistedQueryNotFoundIsNotAnError
{
public static void RegisterPersistedQueryNotFoundLogHandling(this WebApplicationBuilder builder)
{
builder.Services.AddSingleton<ITelemetryInitializer, PersistedQueryNotFoundCountsAsSuccess>();
builder.Services.AddTransient<ResponseBodyLoggingMiddleware>();
}
private const string _requestTelemetryPropertyKey = "PersistedQueryNotFound";
private const string _hotChocolateErrorMessage = "{\"errors\":[{\"message\":\"PersistedQueryNotFound\",\"extensions\":{\"code\":\"HC0020\"}}]}";
public class ResponseBodyLoggingMiddleware : IMiddleware
{
private readonly char[] _buffer;
private readonly char[] _cache;
public ResponseBodyLoggingMiddleware()
{
_buffer = new char[_hotChocolateErrorMessage.Length];
_cache = _hotChocolateErrorMessage.ToArray();
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Method == "POST" && (context.Request.Path.Value == "/graphql/"))
{
var originalBodyStream = context.Response.Body;
try
{
// Swap out stream with one that is buffered and suports seeking
using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
// hand over to the next middleware and wait for the call to return
await next(context);
// Read response body from memory stream
memoryStream.Position = 0;
var reader = new StreamReader(memoryStream);
var charactersRead = await reader.ReadBlockAsync(_buffer, 0, _buffer.Length);
// Copy body back to so its available to the user agent
memoryStream.Position = 0;
await memoryStream.CopyToAsync(originalBodyStream);
// Write response body to App Insights
var requestTelemetry = context.Features.Get<RequestTelemetry>();
if (requestTelemetry != null
&& charactersRead == _hotChocolateErrorMessage.Length
&& context.Response.StatusCode == 400)
{
if (_buffer.SequenceEqual(_cache))
{
requestTelemetry!.Properties[_requestTelemetryPropertyKey] = null;
}
}
}
finally
{
context.Response.Body = originalBodyStream;
}
}
else
{
await next(context);
}
}
}
public class PersistedQueryNotFoundCountsAsSuccess : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
if (telemetry is RequestTelemetry request
&& request.Properties.ContainsKey(_requestTelemetryPropertyKey))
{
request.Success = true;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment