Fix conditional GET across the API - #5662
Conversation
johnsimons
left a comment
There was a problem hiding this comment.
LGTM
But I noticed the monitoring instance does not use etags 😞
| { | ||
| internal static string ReadEtag(HttpResponseHeaders headers) | ||
| { | ||
| // Read raw rather than through headers.ETag. An instance predating the quoted validator |
There was a problem hiding this comment.
Does this matter too much?
Once the instance updates a new read will happen and the etag will get updated to the quoted version. It seems like bothering with re-implementing this read and handling quoted/unquoted values is a marginal optimisation at best especially if it was being silently ignored previously anyway.
There was a problem hiding this comment.
It protects a small window when an error instance is upgraded and an audit has not. Without this check a 304 would be sent to the client rather than a 200 even if audit has new events it needs to share. It can probably be cleaned up in a couple releases time i think, but adding it is harmless.
ServicePulse polls data from ServiceControl as often as 5s. ServiceControl was built to answer "nothing has changed", but the
ETagit sends was a bare token and RFC 9110 requires a quoted string.NotModifiedStatusHttpHandlercompares typed header values, an unquoted token fails to parse asEntityTagHeaderValueand yieldsnull, so the comparison never matched and every conditional request got a full payload.What changed
WithEtagquotes the validator, inServiceControland inServiceControl.Audit, which has its own copy of the same code and the same defect. All 37 ETag call sites funnel through it.WithEtagemits no header when there is nothing to validate. An empty validator quoted becomesETag: "", which is well formed and therefore matches itself, so quoting alone would have answered304to a client holding an unrelated payload.WithEtaginstead of assigningResponse.Headers.ETagdirectly. Bodies are the largest payloads on the API and immutable once written.ScatterGatherApireads a remote instance's ETag from the raw header. The typed accessor returnsnullfor an unquoted value, so every remote validator was being discarded.string.Emptyfor a body in metadata and a freshGuid.NewGuid()per fetch for one in an attachment. Both can never match. It now uses the body id.Impact assessment
Tests
ConditionalGetTests, 7 tests inServiceControl.UnitTestsand 7 inServiceControl.Audit.UnitTests: the304round trip, the mismatch guard, both validator shapes, and the empty case.When_a_request_is_repeated_with_its_etag, 3 cases:/api/customchecks,GET /api/redirectsandHEAD /api/redirectend to end through Kestrel, including thatTotal-Countsurvives the304. The HEAD verb is covered nowhere else.When_a_message_body_is_requested_twice: the audit body endpoint