Problem/Motivation

Enabling this module breaks cron on a site that has not configured it yet — which is the state the module ships in, on purpose.

$ drush cron
In ReaderFactory.php line 90:
  No reading service configured: set a base URL in
  content_credentials.settings reader.base_url.

Non-zero exit, and no cron work done — not this module's, and not the work of any module queued behind it.

Nothing is wrong with the site. The shipped default is reader.route: service with an empty base_url, deliberately, and the settings form says so in as many words: "No route is chosen for you." hook_requirements exists precisely to describe this state to a site that is in it. So the module simultaneously treats an unconfigured route as a normal condition worth reporting, and cannot survive a cron run in it.

Steps to reproduce

  1. Install Drupal (Standard), require drupal/ai_disclosure:^1.0.0-alpha2 and this module.
  2. drush en content_credentials.
  3. Configure nothing.
  4. drush cron.

No queue item, no participating bundle and no uploaded file are needed. Enabling the module is enough.

Measured on Drupal 11.4.6, PHP 8.5, ai_disclosure 1.0.0-alpha2, provemark/content-credentials v0.15.1.

Cause

The failure is at instantiation, not at reading. Core's Cron::processQueues() calls createInstance() on every queue-worker plugin definition before it claims a single item. CredentialRead::create() asks the container for content_credentials.suggester, which needs content_credentials.inspector, which needs content_credentials.reader — and that service is a factory call to ReaderFactory::get(), which builds the route eagerly and throws when there is none.

In a browser it is worse, and the fix sits behind the breakage

automated_cron is enabled by default in the Standard profile, so the exception is thrown during ordinary page requests and logged as a php Error. Measured: with automated_cron installed, saving the settings form did not take effect and the page ended in "The website encountered an unexpected error"; after drush pm:uninstall automated_cron, the identical browser interaction saved on the first attempt. A site builder following the README therefore cannot configure the module through its own form.

That last paragraph is a measured correlation, not a diagnosed mechanism — why an exception thrown at kernel.terminate prevented a form submit from persisting was not established. The cron failure is the diagnosed defect; the form symptom is evidence of its blast radius.

Why no automated gate caught it

Every queue test constructs the worker against a configured route or a test double, so the container never builds the real reader in the one state the module calls normal. composer check is green, this project's CI is green on two core minors, and the kernel suite is green on a real site — all of them configure a route first. It was found by running the module through a browser for the first time.

Proposed resolution

The reading route must not be built until a file is actually read. Two shapes are on the table and neither is chosen yet:

  • the inspector takes the factory and calls get() at read time;
  • the inspector keeps depending on ReaderInterface and the service is declared lazy: true, so the container hands out a proxy.

Nothing about ReaderFactory::get() itself changes: it must keep throwing. Where it is called is the whole of the fix.

Two things the fix must not do, and both will get their own test:

  • It must not catch the exception somewhere. That would hide a broken route instead of deferring it.
  • It must not swallow a real read failure. With an item queued and the route unusable, the failure must still be reported and the item must stay queued — an unconfigured route is a "might succeed later" condition, and discarding the item would silently lose a read.

Remaining tasks

  1. A test that is watched failing in exactly this state — enabled, unconfigured, empty queue — before any code changes. A test that configures a route first cannot fail for this reason.
  2. The fix.
  3. A test that the do-not-overshoot behaviour above is unchanged.

API changes

Possibly a constructor signature inside the module, depending on which shape is chosen. Nothing a site interacts with.

Data model changes

None.

Who is affected

No released site: the project has no release. Anyone who has installed 1.0.x-dev and enabled the module without configuring it has a site whose cron does not run.

Comments

maurice1969 created an issue. See original summary.

  • maurice1969 committed 414739c0 on main
    Issue #3622757 by maurice1969: SPEC 008 — build the reading route when a...

  • maurice1969 committed 414739c0 on 1.0.x
    Issue #3622757 by maurice1969: SPEC 008 — build the reading route when a...

  • maurice1969 committed 20ceda2f on main
    Issue #3622757 by maurice1969: teach cspell the word SPEC 008 introduced...

  • maurice1969 committed 20ceda2f on 1.0.x
    Issue #3622757 by maurice1969: teach cspell the word SPEC 008 introduced...
maurice1969’s picture

Status: Active » Fixed

Fixed in 414739c, on main and 1.0.x, with a one-line follow-up in 20ceda2.

CredentialInspector now takes a ReaderProviderInterface — one method returning a ReaderInterface, implemented by ReaderFactory — and asks for the route inside inspect(), where having a file to read is what makes a route necessary. content_credentials.reader is gone; the swap seam a site would use is content_credentials.reader_factory, and services.yml says so where the old comment stood.

The lazy-service alternative was rejected on a measurement: Drupal 11 still builds lazy services from a generated proxy class, and this service declared an interface as its class with a factory returning two different concrete readers. Whether that generator copes was never established — which is the point, since the whole subject here is when the route is built. One visible line in inspect() beats container machinery a later reader has to know is load-bearing.

Nothing about ReaderFactory::get() changed. It still throws, and the third criterion pins that: with an item queued and no route, the read still fails and the item stays. Discarding it would silently lose work; swallowing the error would make a misconfigured site look like one reading files that carry nothing.

ADR-0002 was amended rather than left stale — its sentence about depending on ReaderInterface "and nothing narrower" is no longer literally true, while its intent is untouched.

What was measured

  • All three criteria were watched failing before any src/ change. The first one's stack carried the whole diagnosis: Cron.php:146 to QueueWorkerManager::createInstance() to CredentialRead.php:108 to the container to ReaderFactory.php:55. A kernel test reproduced it, so no functional suite was needed.
  • Two of the three tests were wrong on their first run, and both faults are recorded rather than quietly corrected. One asserted a service was "not initialised", which would also have passed once that service stopped existing. The other's file entity pointed at nothing on disk, so the inspector threw UnprocessableItemException before it ever asked for a route — it passed without exercising the criterion at all.
  • On the site that produced the report: the shipped default now exits drush cron with status 0 where it aborted the day before, and a node carrying a signed PNG still records ai_generated_autonomous as pending through the full path. 136 tests, 507 assertions there.
  • CI green on both branches, read job by job rather than by pipeline status — which mattered: the first push reported a green pipeline with a red cspell job inside it, because that job is allow_failure in the shared template. 20ceda2 is the word it did not know.

Found by running the module through a browser for the first time.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.