Skip to content

Send only the Evidence the cloud accepts from the Cloud Request Engine - #89

Merged
jwrosewell merged 36 commits into
rcfrom
fix/cloud-request-engine-sent-evidence
Nov 10, 2026
Merged

jwrosewell merged 36 commits into
rcfrom
fix/cloud-request-engine-sent-evidence

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #86, because the change reads the accepted Evidence keys through the CloudEvidenceKeyFilter that #86 adds, and relies on #86 fetching those keys before any data request is sent. This pull request now targets rc and carries #86's head, 5a6fc84, so until #86 merges it also shows #86's commits, and with them #84's. It merges after #86. The head is b244e5a.

The change

The Cloud Request Engine sends only the Evidence the cloud accepts, and never Evidence named resource, license, licence or values, as the Processing section says.

Before this change, CloudRequestEngineDefault.getFormData copied every item of Evidence in the FlowData into the request, leaving out only the web request (server.http-request). The list of keys from the evidencekeys request was used for the engine's evidence key filter but not to choose what was sent, and Evidence whose key strips to resource, license, licence or values was sent as another form field of that name.

What the specification says

From the Processing section of the Cloud Request Engine page on the specifications' rc, as 51Degrees/specifications#125 left it when it merged as 425a6bd. The text below is from rc at d729b7d.

The Engine processes Flow Data by filtering the full list of Evidence down to just Evidence keys needed by the server, and makes an HTTP request to the server using the filtered Evidence.

The remote server advertises query.values among its accepted Evidence keys, so a value in the Flow Data Evidence could otherwise collide with the Property list the Engine is already sending. The remote server also reads licence as another spelling of license. Evidence whose key strips to values, resource, license or licence, in any letter case, MUST therefore be left out of the request, because sending it would put a second field of the same name, or a second spelling of the same credential, in the request with no way for either side to know which one applied. The rule covers resource, license and licence as well as values, and does not depend on which keys the server advertises.

What changed

  • getFormData now keeps only the Evidence that the new isSent allows, before the precedence order picks one value for each name.
  • isSent allows a key only when the evidencekeys list holds it, compared without regard to case, and only when the name left after the prefix is not one of ENGINE_FIELDS, being resource, license, licence and values, also compared without regard to case. licence is there because the cloud service reads the license key from licence where license is absent. The web request is still never sent, even where the list holds its key. A key with no ., which has no prefix to strip, is not sent either.
  • The request therefore carries the configured resource key once, the configured license key once where one is set, and then the accepted Evidence. getContent is unchanged.
  • A warning about two prefixes giving the same name is now raised only among the Evidence being sent.

The data request is only made once the evidencekeys list is known, which #86 ensures in completeDiscovery, so the list is always there when getFormData runs.

The Evidence keys written in the Java sources were checked against the list that https://cloud.51degrees.com/api/v4/evidencekeys returned on 10 November 2026, at this branch's head b244e5a, so that no example or engine loses Evidence it needs.

  1. Every string literal in the .java files under examples, device-detection, ip-intelligence and location that starts with a known prefix was collected with git grep -h -o -i -E '"(header|query|cookie|server|fiftyone|location|session)\.[A-Za-z0-9_.-]+"' b244e5a -- 'examples/*.java' 'device-detection/*.java' 'ip-intelligence/*.java' 'location/*.java', giving 29 different keys in lower case.
  2. They were compared with the 137 keys in the live list, also in lower case.
  3. 24 are in the list. The other five are not Evidence for the cloud service:
    • header.usage-from, which ShareUsageExample sets for usage sharing.
    • header.irrelevant-header and server.irrelevant-header, which the device detection and IP intelligence ProcessTests add on purpose as Evidence that no engine reads.
    • fiftyone.pipeline.web.mvc and fiftyone.geolocation.web.examples.mvc.controller, which are Java package names in the @ComponentScan of the location MVC example's ExampleMvcConfig, not Evidence keys.

Compared with Go

The Go change, 51Degrees/51degrees-go#85, is merged into Go's rc, with its head fecae24 and the merge commit f23ad2e. It applies the same two rules in resolveEvidence, and its engineFields holds the four names resource, license, licence and values.

For the release notes

  • The Cloud Request Engine now sends only the Evidence whose keys the cloud service lists in its evidencekeys answer. Other Evidence in the FlowData is no longer sent.
  • Evidence whose key, without its prefix, is resource, license, licence or values, in any case, is never sent. The request carries only the resource key and license key the engine was configured with.

How to verify

No new test calls a live service.

mvn -pl pipeline/pipeline.cloudrequestengine -am test -Dtest='CloudRequestEngineTests,CloudRequestEngineDiscoveryTests' -Dsurefire.failIfNoSpecifiedTests=false
Class Test methods at #86's head (5a6fc84) This branch (b244e5a)
CloudRequestEngineTests 19 22

Run 42729045219 runs each set of tests on the code before its fix, in two jobs, and each job fails as expected.

Full build

The full build and test matrix passed every job other than macOS on head b244e5a in run 42800964739 on 10 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.

…and logs once

A resource key can be valid and include no product that holds a cloud engine's properties. The Device Detection, IP Intelligence and Location cloud engines are tested to build with empty property metadata, to log the missing product once, to log and throw nothing when processing responses that hold nothing for them, and to report a property read as missing with ProductNotAccessibleWithResourceKey. The base class tests now expect a missing product to build with empty metadata and log once, keep the failure when the accessible properties are not loaded, and check that a property keyed engine gives no profiles.

The Location cloud module gets Mockito as a test dependency for its first tests.
…nce, and process quietly

A resource key can be valid and include no product that holds a cloud engine's properties. The Device Detection, IP Intelligence and Location cloud engines then left their property metadata null, logged two errors, and threw on every request when getJSONObject found no part of the response for them. The engines built on CloudAspectEngineBase, such as the hardware profile engines, threw when their metadata was read.

CloudAspectEngineBase now has findProduct, which returns the engine's product from the accessible products, or an empty product when the resource key does not include it, logging that once for each engine instance. getElementJson returns the engine's part of the response, or an empty object when the product is missing. The three engines, the base class's own metadata loading and the property keyed base use them, so each builds with empty property metadata, logs one error, and processes a response with nothing for it without logging or throwing. Reading a property then throws PropertyMissingException with ProductNotAccessibleWithResourceKey, as for any property the response does not carry. Accessible products that were never loaded still fail as before.

The Location engine's failure message named the Device Detection engine, and now names the Location engine.
…tries only after a failure that can pass

CloudRequestEngineDiscoveryTests uses a stand-in for the HTTP client that counts the requests to each endpoint. A build fetches the accessible properties and the evidence keys once each and processing fetches neither again. A 4xx answer to either fails the build with a CloudRequestException carrying the status, once both requests are made, also when the other request failed in a way that can pass. A 5xx answer, a body that cannot be read, a refused connection and a timeout each complete the build with one warning. A list that arrived is kept, processing fetches only what is missing and sends no data request until both are known, the Pipeline's Evidence filter sees the keys a retry fetched, and the configured timeout applies to the discovery requests.

The existing tests that expected getPublicProperties and getEvidenceKeyFilter to fetch and throw when the server was unavailable, and a 200 answer holding errors to throw, now expect the build to complete with one warning. The cloud aspect engines are tested to build without logging when the accessible products are not known yet, and to read their metadata once they are.
…se only after a failure that can pass

The engine fetched its accessible properties and accepted evidence keys only when something first asked for them, and a failure surfaced wherever that was. The builder now calls discover, which makes both requests. A 4xx answer to either is a definitive configuration error, and its CloudRequestException fails the build once both requests are made. Any other failure, the service unreachable, a timeout, a 5xx answer or a body that cannot be read, is logged as one warning, the build completes and the list that arrived is kept. The configured timeout now applies to both requests.

getPublicProperties and getEvidenceKeyFilter make no request. The products are null until they are known. The evidence filter is one CloudEvidenceKeyFilter that includes nothing until the keys are known and reads them on each call, and it derives from EvidenceKeyFilterWhitelist so the Pipeline's filter keeps it rather than copying its keys once. Processing fetches what is missing, evidence keys first, before it sends the data request, and a request that could not be made is reported as its IOException, as for the data request.

The Device Detection, IP Intelligence and Location cloud engines, and CloudAspectEngineBase, read their metadata when the products are known rather than logging errors at build and keeping none. A missing product still builds and logs once. The web integration reads a filter that derives from EvidenceKeyFilterWhitelist for the Vary header, as it reads the exact class.
…er one discovered

The specification's Resilience section says an engine SHOULD be buildable from a kept copy of the discovery results, the accepted evidence keys and the accessible properties, which depend only on the resource key, so that a Pipeline can be built offline and a host that starts often need not call the cloud service each time.

CloudDiscoveryState holds the evidence keys and the accessibleproperties answer, and writes and reads them as one JSON document. CloudRequestEngineBuilder.getDiscoveryState returns the copy after a build that knows both lists, and setDiscoveryState makes a later build use it with no discovery request. The tests are in this commit because they use the new class and methods.
…conds, and that the Pipeline reads metadata again once it is known

Each discovery request in the new test waits for the other to start, so they only succeed if they are made at once. A build with no timeout set applies 2 seconds to both, as the page's Configuration options table and .NET give. The Pipeline's map of available properties, built before the Cloud Request Engine knows the accessible properties, holds the cloud aspect engine's properties once they are known.

The shared test client now gives each request a connection of its own, as the requests can be made at the same time.
…ult, and read metadata again until it is loaded

The two discovery requests are now made at once, as the specification allows, so a service that does not answer delays the build by one timeout rather than two. The evidence keys request runs on another thread while the accessible properties request runs on the building thread, and the build waits for both.

The default timeout for each request to the cloud service is now 2 seconds, as the specification's Configuration options table gives and as .NET's CLOUD_REQUEST_TIMEOUT_DEFAULT_SECONDS is. It was 100 seconds.

FlowElement gains hasLoadedProperties, true by default, as .NET's aspect engines have HasLoadedProperties. The Cloud Request Engine returns true once both discovery lists are known, the cloud aspect engines once their metadata is loaded, and a parallel group once all its elements have. PipelineDefault keeps its map of available properties only once every element has loaded its properties, and builds it again on each call until then, as .NET's Pipeline.ElementAvailableProperties does after a PropertiesNotYetLoadedException. The web integration works out the Vary headers again on each response until every element has loaded, as .NET's ClientsidePropertyService.HeadersAffectingJavaScript does after an exception from an element's EvidenceKeyFilter.
…lement has loaded its properties

With every element loaded, the configuration built from the Pipeline's available properties is built once and kept across requests. While an element has not loaded its properties, it is built again on each request, and kept once every element has loaded. Both fail on the code before the fix, where the configuration was built again on every request, because getOrDefault never stored it.
…aded its properties

The SetHeaders element built its configuration from the Pipeline's available properties on every request, because getOrDefault never stored it. It now keeps it once every element of the Pipeline has loaded its properties, and builds it again on each request until then, as after a cloud element could not reach its service when the Pipeline was built. So the configuration gains the cloud's SetHeader properties once a retry fetches them, and is then built once. This is the same rule the Pipeline's map of available properties and the web integration's Vary header follow.
…accepts

The Processing section of the cloud request engine specification says the
engine sends only the Evidence keys the cloud service needs, and never
Evidence whose key strips to resource, license or values. Two new tests
check the posted request. process_SendsOnlyAcceptedEvidence sends Evidence
the service does not list and expects only the listed keys, compared
without regard to case. process_LeavesOutEvidenceNamedResourceLicenseOrValues
sends Evidence named resource, license and values in several cases, with
and without a configured license key, and expects the configured keys once
each and nothing else.

Existing tests that check the request now answer the evidencekeys request
with the keys they send, as the cloud service would.
The Cloud Request Engine sends only the Evidence whose keys the cloud
service accepts, from its evidencekeys request, and never Evidence whose
key strips to resource, license or values, compared without regard to
case, as the Processing section of the cloud request engine specification
says. The request carries the configured resource key, and license key
where one is configured, once each.

Before, the engine sent all Evidence except the web request. Conflicts
between prefixes are now reported only among the Evidence being sent.
thenReturn(null, products) uses the varargs overload, and a generic Map array is an unchecked warning that the build treats as an error, so each stub now chains two single-value thenReturn calls.
…e build

The specification now treats a 408 or a 429 answer to a discovery request as a transient failure, because the same request can succeed later, so 429 moves from the cases that fail the build to those that do not, and 408 cases join it.
… build

A build fails on a 4xx answer because no retry with the same configuration can succeed, which is not so for a 408 or a 429, as RFC 9110 section 15.5.9 and RFC 6585 section 4 say, so those two are logged as a warning and retried on first use like a 5xx.
The Processing section of the cloud request engine specification now
also leaves out Evidence whose key strips to licence, because the cloud
service reads the license key from licence where license is absent.
process_LeavesOutEvidenceNamedLicence lists query.licence and
cookie.LICENCE as accepted, adds Evidence under each, and expects only
the configured keys and User-Agent, with and without a license key. It
is written to fail on the previous commit, which sends both as licence
and LICENCE.
Evidence whose key strips to licence, compared without regard to case,
is now left out of the cloud request along with resource, license and
values, as the Processing section of the cloud request engine
specification says, because the cloud service reads the license key from
licence where license is absent.
…no error

The Start-up activity section of the cloud aspect engine specification on
rc says the engine "MUST log the missing product once per Cloud Aspect
Engine instance, at warning level, naming its Data Key". The Processing
section says "Where the response JSON has no portion for the Engine's
Data Key, the Cloud Aspect Engine leaves the Element Data without values
and raises no error."

The missing product tests now expect one warning and no error.
ProductPresent_ResponseWithoutPart_NoValues, for each of the Device
Detection, IP Intelligence and Location cloud engines, includes the
engine's product and processes a response with no part for it, and
expects no exception and a property without a value.
propertyKeyed_productPresent_noProfiles does the same for a property
keyed engine, with a response that has no part for it and one whose part
has no profiles. All are written to fail on the previous commit, which
logs an error and throws on a missing part when the product is present.
… as no values

The Start-up activity section of the cloud aspect engine specification on
rc says the missing product is logged "once per Cloud Aspect Engine
instance, at warning level, naming its Data Key". findProduct now logs
it with logger.warn.

The Processing section says "Where the response JSON has no portion for
the Engine's Data Key, the Cloud Aspect Engine leaves the Element Data
without values and raises no error." getElementJson now returns an empty
object whenever the response has no part for the engine, whether or not
the product is included, and PropertyKeyedCloudEngineBase reads profiles
with optJSONArray, so a part without profiles gives none. The
productMissing flag and isProductMissing, which only chose between those
two behaviours, are removed.
The specification on rc makes every 4xx answer to a discovery request a
definitive configuration error, so the cases that fail the build now
include 408 beside 429.
A 4xx answer is a definitive configuration error whatever its body holds,
such as an error page from a proxy. The accessibleproperties answer is
read as JSON before its status is used, so a 403 with an HTML body there
fails this test, as the build completes with a warning.
An answer that cannot be read as JSON now raises a CloudRequestException
carrying its HTTP status, as .NET does, so a 4xx answer to a discovery
request fails the build whatever its body holds, as the specification
says.
A comment-only change. The paragraph that describes findProduct left
"returned, so the engine builds" on a short line of its own, so the
paragraph is now wrapped at 80 columns. No code changes.
The one conflict is in CloudRequestEngineDefault.java, where rc adds
getLicenseKeys() in the place where this branch adds
setTimeouts(HttpURLConnection) and renames getCloudProperties to
fetchPublicProperties. Both helpers are kept, and the accessible
properties request keeps this branch's name, fetchPublicProperties,
which every call site uses.
This branch carried the missing product change as it was at 33a13fe. Its
head, 9fced9b, logs a missing product as a warning, treats a missing
response part as no values, and removes isProductMissing.

The conflicts are in the Device Detection and IP Intelligence
ProductMissingTests and in GeoLocationCloudEngineTests. Each takes the
file as it is at 9fced9b and keeps this branch's
DiscoveryNotComplete_LoadsTheMetadataLater test above buildEngine.
@jwrosewell
jwrosewell changed the base branch from fix/cloud-request-engine-eager-discovery to rc November 10, 2026 10:34
The eager discovery branch now has rc merged in, with the license key
masking, and the missing product change at 9fced9b. Both merge into this
branch with no conflict.
@jwrosewell
jwrosewell marked this pull request as ready for review November 10, 2026 13:49
@jwrosewell
jwrosewell marked this pull request as draft November 10, 2026 16:01
@jwrosewell
jwrosewell marked this pull request as ready for review November 10, 2026 16:28
@jwrosewell
jwrosewell merged commit ff6e4c0 into rc Nov 10, 2026
2 checks passed
@jwrosewell
jwrosewell deleted the fix/cloud-request-engine-sent-evidence branch November 10, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant