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
- 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>/.
- Open the module's settings dialog and go to the tab containing the field.
- 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:
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:
- The dialog renders the fragment more than once into the DOM
(duplicate copies of the same markup, so duplicate element IDs).
- 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).
Summary
On the
v2025.xx.xxbranch, a module settings field of typehtmlwithsource: "file"(the mechanism documented inallsky_boilerplate, e.g. itshtml1field /help.html) never renders in the module settings dialog.The dialog JS (
html/js/modules/modules.js) fetches the file viaincludes/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 failurepersist 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
v2025.xx.xx(cloned 2026-07-08)Reproduction
argumentdetailscontains anhtmlfield withsource: "file"(e.g.allsky_boilerplatewith itshelp.html), with thefile present in
myFiles/modules/moduledata/data/<module>/.ModuleFilerequest returning 404.
Bug 1 —
ModuleFilemissing from the route allow-listUTILBASE::dispatch()only invokes endpoints declared ingetRoutes().MODULEUTIL::getRoutes()does not containModuleFile, so every request isrejected with 404 "ModuleFile is not callable." Presumably dropped when
the route allow-list hardening was added.
Fix — add to the
getRoutes()array:Bug 2 —
getModuleFile()isprivateWith the route added, the endpoint still fails:
getModuleFile()is declaredprivate, and the dispatcher lives in the parent class (UTILBASE), whichcannot 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:
Bug 3 —
sendResponse()JSON-encodes the returned HTMLgetModuleFile()returns the file viasendResponse($fileContents), whichJSON-encodes any string that is not already valid JSON and serves it as
application/json. However,modules.jsinsertsresult.responseTextintothe dialog raw. The tab therefore fills with quote-wrapped,
backslash-escaped markup, and any
<script>inside the fragment fails toparse (Safari:
SyntaxError: Invalid escape in identifier).Fix (verified) — return the file content raw:
Aggravating issue — one-year
immutablecaching on dynamic/error responsesResponses from these endpoints — including the 404s produced by the bugs
above and the anti-scraping 404 from
checkXHRRequest()— are served with: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 moduleendpoints, and never
immutableon 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:
(duplicate copies of the same markup, so duplicate element IDs).
shown.bs.tabona[data-toggle="tab"]elements within#module-settings-dialog, so afragment 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
htmlfields render correctlyand 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_boilerplatedocumentation).