Repository navigation
Set the client-side cache headers, with a weak ETag that is the same in every process and none after errors - #59
Draft
jwrosewell wants to merge 12 commits into
Draft
jwrosewell wants to merge 12 commits into
jwrosewell wants to merge 12 commits into
Conversation
…very process and none after errors
This was referenced Nov 7, 2026
…ch only when it is "*" or a list of entity tags
…ays should be configurable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
The cache header detail of the web integration specification lists the headers the client-side JavaScript endpoint (
/51Degrees.core.js) and the JSON endpoint (/json) MUST send, beingCache-Control: max-age=1800 private,Vary,ETag,Content-TypeandContent-Length, and says a request whoseIf-None-Matchmatches the ETag is not sent the content again.Python has no web integration package. On
rcthe client-side endpoints are written in each Flask example, and none of them sets any cache header, so a browser fetches the script and the JSON in full on every page. The places that build them are these./jsonand/51Degrees.core.jsindevice-detection/.../cloud/gettingstarted_web/app.pyand in the on-premise copy beside it./51Degrees.core.jsand/jsonin the pipeline's mixed cloud example,.../cloud/mixed/gettingstarted_web/app.py./jsoninlocation/examples/cloud/web.pyand inpipeline/fiftyone_pipeline_core/examples/client_side_evidence_custom_flow_element.py.The one shared module is
fiftyone_pipeline_core/web.py, which the examples already use forwebevidenceandset_response_header.What the specification and RFC 9110 require
RFC 9110 (HTTP Semantics, STD 97) is current, as its RFC Editor record lists nothing that obsoletes or updates it.
W/"<hash of ALL the Evidence values in Evidence filter key>".hash()of a string afresh in each process, so it is not used.If-None-Matchis compared weakly. RFC 9110 section 13.1.2 says "A recipient MUST use the weak comparison function when comparing entity tags for If-None-Match", and the weak comparison of section 8.8.3.2 matches two tags "if their opaque-tags match character-by-character, regardless of either or both being tagged as "weak"". The field is"*" / #entity-tag, and step 3 of the evaluation in section 13.1.2 says that a value which is neither"*"nor a list of entity tags leaves the condition true, so it matches nothing. A list allows optional spaces and tabs around its commas and empty elements (section 5.6.1.2), the opaque tag holds no space or double quote (etagcin section 8.8.3), andW/is case sensitive. Section 13.1.2 also says a list holding"*"and other values "is syntactically invalid".Cache-Control,ETagandVarya 200 would carry. The specification gives the same answer, and the open draft specifications#81 adds the 15.4.5 headers.Cache-Control: private, no-cacheand no ETag, when processing recorded errors in the Flow Data errors collection (specification lines 248 to 261), so a browser has nothing to revalidate the faulty response with and asks again in full once processing works.The change
fiftyone_pipeline_core.web(web.py) gainsjavascript_response(pipeline, request, max_age_seconds=1800)andjson_response(pipeline, request, max_age_seconds=1800), which process the request and answer with the JavaScriptBuilder's JavaScript or the JSONBundler's JSON, and the constantCLIENT_SIDE_MAX_AGE_SECONDS. Everything else is private to the module.W/"<32 hex digits>", the first 128 bits of a SHA-256 hash of the name and value of each piece of the request's Evidence that one of the pipeline's elements accepts, in name order. It is worked out from whatwebevidence()read from the request, before processing.If-None-Match,If-Modified-Since,If-Match,If-Unmodified-Since,If-Range,Cache-Control,Pragma) and the web request valueserver.http-requestare left out of the ETag and ofVary.FlowElement's default filter accepts every key, so an element that does not set its own filter would otherwise putIf-None-Matchinto the ETag, and a revalidation would never match.If-None-Matchis read by a small parser that follows the grammar above, and matches when the field is"*"or when one tag in a valid list has the ETag's opaque tag.Cache-Control,VaryandETagand noContent-Type, and any other method, being the POST to the JSON endpoint, gets 412 Precondition Failed with no content and no ETag. So a browser holding a good copy keeps it while processing fails. Python's endpoints set no CORS headers on a 200, so the 304 has none either.Content-Type(application/x-javascriptorapplication/json),Content-Lengthfrom Flask, andCache-Control: max-age=1800, privatewith the ETag, orCache-Control: private, no-cacheand no ETag whenflowdata.errorsis not empty.Varynames the request's headers that one of the pipeline's elements accepts, for exampleHost, User-Agent.The five example routes call these two functions, and their comments say what the call does.
The other languages follow the same rules. The PHP draft 51degrees-php#48 adds the same functions to
Utils, and Node onrcsends the weak ETag fromhelpers.js.For the release notes
Cache-Control,Vary, a weakETag,Content-TypeandContent-Length, so a browser keeps the script for 30 minutes and revalidates it with a 304 rather than fetching it in full on every page.fiftyone_pipeline_core.webgainsjavascript_response()andjson_response(), which answer the client-side endpoints of a Flask application with these headers. The max age is the optionalmax_age_secondsargument.If-None-Matchmatches gets a 304 to GET or HEAD and a 412 to any other method, as RFC 9110 section 13.1.2 requires. A response to a request whose processing recorded errors carriesCache-Control: private, no-cacheand no ETag.Tests
pipeline/fiftyone_pipeline_core/tests/test_web_client_side.pyhas 16 tests andpipeline/fiftyone_pipeline_cloudrequestengine/examples/tests/test_cloud_mixed_web_offline.pyruns the mixed example against a stand-in for the cloud service. Neither needs a key or the network. Between them they cover the following.W/"<32 hex digits>", on the JavaScript and the JSON.PYTHONHASHSEEDset to 1 and to 2.If-None-Matchvalues on a GET and on a POST, being the tag as sent, its strong form,*, lists, a comma inside a tag, a tab or no space around a comma, empty list elements, a tag then a non-tag, a non-tag then a tag, two tags with no comma, a space inside the quotes,w/in lower case,W/followed by a space,*inside a list, an unquoted tag and an unterminated one, and twoIf-None-Matchfields read as one list.ETag,Cache-ControlandVaryof the good response, and 412 to POST with no content and no ETag.private, no-cacheand no ETag on both endpoints, a matching tag answered while processing fails (304 to the script, 412 to the JSON), and a failed response replaced once processing works.max_age_secondsargument on both endpoints.The tests were committed before each change, so each pair shows red then green. e2cabee holds the weak tag and
If-None-Matchtests and a7e05a4 the change, then 83c4722 the max age test and bef273b the change.rcat a474f04 was merged in first, at ccd87d7. e0799ee then makes the session id test read the minified script.Test results
The suites have not yet run on this head, because the machine this was written on had too little free memory to run them safely, so the results of the CI build on this head are to be added here. These commands run the tests above from a clone of this branch.
Earlier runs
These ran on older heads of this branch, before the weak ETag and the stricter
If-None-Matchreading.rcat 0507eef with only the first tests added, and green run 42395023618 at b1a6754, 24 passed in the core tests and 18 in the examples on Ubuntu and Windows with Python 3.12.Full build
The full build and test matrix passed every job other than macOS on head e0799ee in run 42695092282 on 9 November 2026, 11 jobs succeeded, run as a dry run so nothing merged. macOS was left out of this draft's full build under the 9 November decision. The rc head is built with macOS before it merges into main.