Skip to content

html fieldtype with source:"file" broken — ModuleFile endpoint unreachable, response JSON-mangled, failures cached a year #338

Description

@AndyOfLinux

Summary

On the v2025.xx.xx branch, a module settings field of type html with
source: "file" (the mechanism documented in allsky_boilerplate, e.g. its
html1 field / help.html) never renders in the module settings dialog.
The dialog JS (html/js/modules/modules.js) fetches the file via
includes/moduleutil.php?request=ModuleFile&file=<file>&module=<module>,
and that endpoint is broken in three independent ways in
html/includes/moduleutil.php. A caching header then makes each failure
persist in the browser even after it is fixed.

All found on a fresh install while developing a module that uses this
fieldtype; each fix below was applied locally and verified working.

Environment

  • Raspberry Pi 5, Raspberry Pi OS Trixie (64-bit Desktop)
  • Fresh install of v2025.xx.xx (cloned 2026-07-08)
  • Browser: Safari on macOS (caching behaviour also applies to other browsers)

Reproduction

  1. Install a module whose argumentdetails contains an html field with
    source: "file" (e.g. allsky_boilerplate with its help.html), with the
    file present in myFiles/modules/moduledata/data/<module>/.
  2. Open the module's settings dialog and go to the tab containing the field.
  3. The field content does not render. The Network tab shows the ModuleFile
    request returning 404.

Bug 1 — ModuleFile missing from the route allow-list

UTILBASE::dispatch() only invokes endpoints declared in getRoutes().
MODULEUTIL::getRoutes() does not contain ModuleFile, so every request is
rejected with 404 "ModuleFile is not callable." Presumably dropped when
the route allow-list hardening was added.

Fix — add to the getRoutes() array:

'ModuleFile' => ['get'],

Bug 2 — getModuleFile() is private

With the route added, the endpoint still fails: getModuleFile() is declared
private, and the dispatcher lives in the parent class (UTILBASE), which
cannot call a child's private method — the call throws and dispatch returns
500 "Internal error." Every other endpoint method in the class is public.

Fix:

public function getModuleFile() {

Bug 3 — sendResponse() JSON-encodes the returned HTML

getModuleFile() returns the file via sendResponse($fileContents), which
JSON-encodes any string that is not already valid JSON and serves it as
application/json. However, modules.js inserts result.responseText into
the dialog raw. The tab therefore fills with quote-wrapped,
backslash-escaped markup, and any <script> inside the fragment fails to
parse (Safari: SyntaxError: Invalid escape in identifier).

Fix (verified) — return the file content raw:

header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store');
echo $fileContents;
exit;

Aggravating issue — one-year immutable caching on dynamic/error responses

Responses from these endpoints — including the 404s produced by the bugs
above and the anti-scraping 404 from checkXHRRequest() — are served with:

Cache-Control: max-age=31536001, immutable

Browsers cache the failure for a year and never revalidate, so even after
the server is fixed, users keep getting the cached error until they manually
empty the browser cache. During debugging this turned every fix into a
"still broken" result until the cache was cleared by hand.

Suggestion: use no-store (or a short max-age) for dynamic module
endpoints, and never immutable on error responses.

Additional observations - should be verified?

With the three fixes applied, two further behaviours of the settings dialog
affect any html-field fragment that contains a script:

  1. The dialog renders the fragment more than once into the DOM
    (duplicate copies of the same markup, so duplicate element IDs).
  2. Showing a settings tab does not reliably emit shown.bs.tab on
    a[data-toggle="tab"] elements within #module-settings-dialog, so a
    fragment script cannot rely on Bootstrap tab events to know when its tab
    becomes visible.

Fragment scripts therefore need to be duplicate-tolerant (class-based
selectors, initialise only the visible copy) and use their own visibility
detection. Worth either de-duplicating the field rendering or documenting
this in allsky_boilerplate.

Verification

With Bugs 1–3 patched as above, file-based html fields render correctly
and their embedded scripts execute in the settings dialog (tested with an
interactive Konva-based settings widget in a third-party module, and the
behaviour matches the allsky_boilerplate documentation).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions