-
-
Notifications
You must be signed in to change notification settings - Fork 29
Issue 129 Every Page Returned HTTP 500
Reported by tjedelhauser Β· Fixed in #1457/#1458
You pulled the latest main, rebuilt, and the whole product was gone. Not degraded β gone. Every URL answered with HTTP 500 and a zero-byte body: the analyst login, the self-service portal, the landing page, the API. Nothing rendered, nothing was logged, and there was no message anywhere to act on.
GET / 500 0 bytes
GET /auth/login.php 500 0 bytes
GET /self-service/login.php 500 0 bytes
The underlying error, which you had to go and find for yourself:
PHP Fatal error: Uncaught Error: Call to undefined function dbConnectionOptions()
in /var/www/html/includes/functions.php:59
Update #1446 pinned every database connection to UTC. It did so by adding one function:
function dbConnectionOptions(): array
{
return [ PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '+00:00'" ];
}...and changing all eleven places in the product that open a PDO connection to pass it.
The function was put in config.php.
That is the entire bug. config.php is the operator's file:
- It ships as a template, with a developer's own
C:\wamp64\db_config.phppath in it. Every install must edit it before the product runs at all. - Having edited it, operators keep their copy across upgrades. That is the documented and sensible thing to do.
- The Docker image does not even leave it to chance β the
Dockerfilecopiesdocker/config.phpstraight over the top of it:
COPY docker/config.php /var/www/html/config.phpSo upgrading delivered the eleven callers and left the definition behind. docker/config.php was never updated in #1446, which is why Docker broke 100% of the time; a hand-installed site broke the moment it preserved its own config.php, which is every hand-installed site that had ever been configured.
PHP resolves a function name when it reaches the call, so config.php parsed fine, functions.php parsed fine, and the process died at the first attempt to open a database connection β which is to say, on every request the product can serve.
The development machine's config.php is the repository's config.php. On the one install in the world where that file is not customised, the function was present and everything worked.
Nothing the application has to execute may live in
config.php.That file is for values the operator chooses β credentials, paths, switches. Behaviour lives in
includes/, which upgrades with the product.
A function defined in config.php is invisible to every install that kept its own copy.
The definition moved to a new file, includes/db.php, which upgrades with the product like everything else under includes/. The rule above is written at the top of it, so the next person tempted to reach for config.php reads why not.
It is declared behind a guard:
if (!function_exists('dbConnectionOptions')) {
function dbConnectionOptions(): array { ... }
}That is an upgrade path, not defensiveness. An install coming from #1446 still has the copy in its own config.php, and every caller loads config.php first. Theirs wins; this fills the hole for everybody else. Without the guard, those installs would trade a fatal undefined function for a fatal cannot redeclare β the same outage with a different message.
includes/functions.php requires it, which covers seven of the eleven callers. The four sign-in and password-reset paths that deliberately do not load functions.php require it directly.
includes/ssl.php β which defines sslApplyCurl(), the helper every outbound HTTPS request in the product calls β is also reached only through config.php. It has 43 callers and only five of them guard with function_exists(). An operator whose config.php predates the SSL work is one identical fatal away from the same outage.
functions.php now requires that too, so it no longer depends on the operator's file either.
Diagnosing this meant reading our source, because docker compose logs showed a 500 in the access log and not one word about the error.
The image sets no value for log_errors, and PHP's built-in default is off. So in the Docker image, no PHP error has ever been written anywhere at all.
log_errors = On ; goes to stderr, where Docker collects it
display_errors = Off ; and stays out of the browserdisplay_errors was already switched off β but in docker/config.php, with ini_set(), which only takes effect once config.php has run. A failure earlier than that would have printed file paths and a stack trace into a visitor's page. Setting it in php.ini covers the whole request. This particular fatal did not leak, as it happens: it was in functions.php, after config.php had run. A parse error in config.php itself would have.
| File | Change |
|---|---|
includes/db.php |
New. Canonical dbConnectionOptions(), guarded, with the rule documented |
config.php |
Definition removed; replaced by a comment saying where it went and why |
includes/functions.php |
Requires db.php and ssl.php
|
api/auth/request_password_reset.php |
Requires db.php
|
api/auth/reset_password.php |
Requires db.php
|
auth/oauth_callback.php |
Requires db.php
|
auth/google_oauth_callback.php |
Requires db.php
|
docker/php.ini |
log_errors = On, display_errors = Off
|
| 11 call sites | Comment repointed from config.php to includes/db.php
|
docker/config.php was deliberately not given the function. Adding it there would have fixed Docker and left every hand-installed site broken β the same mistake in the other direction.
Reproduced first, against the real container, before anything was changed:
GET / 500 0 bytes
GET /auth/login.php 500 0 bytes
GET /self-service/login.php 500 0 bytes
require "config.php"; var_dump(function_exists("dbConnectionOptions"));
bool(false)
After the fix, on a rebuilt image:
302 / 200 /setup/index.php
200 /auth/login.php 200 /api/auth/reset_password.php
200 /self-service/login.php 200 /auth/oauth_callback.php
302 /self-service/register.php 200 /auth/google_oauth_callback.php
PHP errors logged during the sweep: none
A fix that quietly dropped the UTC pinning would test exactly like a fix that kept it β every page would load either way. So it was asserted directly, against a server whose own clock is not pinned:
session time_zone : +00:00 <- ours
global time_zone : SYSTEM <- the server's
NOW() : 2026-09-03 23:02:35
UTC_TIMESTAMP() : 2026-09-03 23:02:35
The same on the hand-installed development machine, which no longer has the function in its config.php at all.
| Starting point | Result |
|---|---|
Fresh Docker (no function in config.php) |
includes/db.php supplies it β
|
Hand install with the new config.php
|
includes/db.php supplies it β
|
Upgrading from #1446, old config.php still defines it |
Guard holds, no redeclare β |
A file calling an undefined function was dropped into the container on purpose. Before: HTTP 200, the error printed into the response body, nothing in the log. After: HTTP 500, empty body, and the fatal in docker compose logs. An ini setting that reads On is not evidence that anything is actually written.
Replace your files and it is fixed. There is nothing to edit.
- Keeping your own
config.php, as you should: the function now arrives with the product. - Already carrying the #1446 copy in your
config.php: it keeps working. You may delete it if you like; you do not have to. - Docker: rebuild the image.
Nothing was written to the database while this was happening, because nothing could open a connection. There is no data to repair.
19 September 2026. A user running 1.9.0 connected a Microsoft 365 mailbox and got:
Fatal error: Uncaught Error: Call to undefined function sslApplyCurl()
in auth/oauth_callback.php:119
Same rule broken, different function. sslApplyCurl() lives in includes/ssl.php, and auth/oauth_callback.php loaded config.php, includes/db.php and includes/encryption.php - but never includes/ssl.php. It had been reaching the definition only because the operator's config.php requires that file. On an install whose config.php does not, there is no definition to reach.
His config.php is missing the whole block, not just the require. The debug tool D006 reported SSL_CA_BUNDLE undefined, and config.php defines that constant two lines below the require - so a file missing only the require would fatal inside config.php on every page instead, and his application worked. The block has been in the shipped template since 22 July 2026, before the 1.0.0 tag, so no released version lacks it; his copy is hand-assembled or predates 1.0.0.
Section 6 of this page already named the two files. It listed auth/oauth_callback.php and auth/google_oauth_callback.php among "the four that deliberately do not load functions.php", gave them db.php, and never gave them ssl.php. The risk was identified, written down, and left open for two weeks.
A noted risk is not a fixed one.
He added require_once __DIR__ . '/ssl.php'; to includes/mailbox_graph.php, and his Microsoft sync started working. Two problems:
-
includes/mailbox_graph.phpis one of ours. His edit disappears on his next upgrade, taking his working mailbox with it. - It only worked by include order -
oauth_callback.phprequiresmailbox_graph.phpat line 14, before line 119 runs.auth/google_oauth_callback.phpnever requiresmailbox_graph.phpat all, so Gmail produced the identical fatal and would have stayed broken.
- Both callbacks now
require_onceincludes/ssl.phpthemselves. - Both definitions in
includes/ssl.phpgained thefunction_exists()guardincludes/db.phphas had since #1446, for the same upgrade-path reason given in section 4. -
sslApplyCurl()attached a CA bundle only whenSSL_CA_BUNDLEwas defined - again the operator's constant. With the fatal cleared, the reproduction's very next error was unable to get local issuer certificate. It now falls back tosslResolveCaBundle(), the same resolverconfig.phpwould have called. An operator who setsSSL_CA_BUNDLEstill wins.
tests/config-not-load-bearing.php asked "does this function have a home under includes/?" - and its probe required includes/ssl.php itself. It never asked the question that matters: "does each caller load that home?" The guard had the same blind spot as the bug.
It now walks the include graph of every directly-requestable caller with config.php's own edges cut out. That detail is the whole test: an earlier hand audit let paths run through config.php and therefore cleared auth/oauth_callback.php, the one file already known to be broken. If the operator's file is what carries you to the definition, you have proved the bug, not its absence. It uses PHP's tokeniser rather than a regex, because sslApplyCurl() appears inside a description string in the debug-tool registry and a regex audit reported that file as a broken caller.
Proved by reintroducing the fault: with the two require_once lines removed the suite fails and names both callbacks.
Worse than not catching it, the diagnostic said everything was fine. D006 printed:
includes/ssl.php loaded : YES
...
β Working. Certificate verification is on and succeeded against 6 of 6 services.
on the same machine, at the same time, as the fatal. It computed that line from function_exists('sslApplyCurl') having itself required includes/functions.php, which requires includes/ssl.php - so the line could only ever say YES. It measured its own include path, not the one that was broken.
D006 now has a Where sslApplyCurl() comes from section that asks the structural question, reports whether your config.php still carries the SSL block, prints the lines to add when it does not, and refuses to show an unqualified tick when it has found something the live requests cannot exercise.
The general lesson, for any diagnostic: a check whose answer cannot come back negative is not a check. Ask what would have to be true for this line to print NO, and if nothing would, the line is decoration.
After dbConnectionOptions() and sslApplyCurl() there was one thing left that the app could not run without and that only config.php provided: BASE_URL, the app's web path (/freeitsm-app/, /). It is used in 300+ places with no fallback, and on PHP 8 an undefined constant is a thrown Error, so a config.php without the block would take down every page at once - the same blast radius as #129.
Nobody has hit it. Every released config.php has the block (it predates v1.0.0). This is insurance against a hand-assembled or very old copy, fixed before it happened rather than after.
The fallback. The detection moved into includes/base_url.php, which ships with the app:
if (!defined('BASE_URL')) {
define('BASE_URL', appBaseUrlDetect()); // where the app sits under DOCUMENT_ROOT
}includes/functions.php loads it first, and the four entry points that do not load functions.php (the two OAuth callbacks, problem-management/new/index.php, lms/native-player.php) load it themselves. Both templates now just require_once it. Your own value always wins - set define('BASE_URL', '/helpdesk/'); above that line, or anywhere in config.php, and the fallback stands aside.
The guard. tests/config-not-load-bearing.php section 7 uses the same include walker as section 5, with config.php's edges cut: every directly-requestable file that uses BASE_URL must reach includes/base_url.php without going through config.php (228 entry points). Positive controls: a config.php with no BASE_URL still gets the right path, and an operator's own value is not overridden. Proved live too: with the line removed from a real config.php, 13 pages including the four special files loaded with correct links.
The other half of the problem: an upgrade never adds a line to your config.php, and until now nothing told you whether your copy was missing something. System β Debug Tools β D017 does, setting by setting - needed, has a default, or optional - with the line to add for each.
Three decisions shaped it:
-
It never prints a value.
docker/config.phpdefines a realDB_PASSWORD. The file is read with the tokeniser for the names itdefine()s (so a commented-out line does not count) and the functions it declares - nothing it assigns is echoed. -
It does not diff against
docker/config.php. That file defines database credentials andTRUST_PROXY_HTTPS, which a hand install must not copy. -
No second copy to keep in sync. The list lives in
includes/config_requirements.php- per setting: needed / has a default / optional, the file that copes when it is absent, what happens without it, and the line to add. Section 8 of the test holds it to both templates: every constant either template defines must be on the list,docker/config.phpmust define every "needed" one, and every "has a default" entry must really have adefined('NAME')check in the shipped file it names. Add a constant to a template and forget the list, and the test fails.
Positive controls in the test: a planted config with an unknown constant and a function is reported as both; a commented-out define is ignored; planted values never appear in the report. The live tool is fetched over HTTP as an administrator and the real database password is asserted absent from its output; without a session it returns 403.
-
Timezones and Time Handling β what
dbConnectionOptions()is for - Issue #126 β Notes were stamped with the server's clock β the fix that introduced this
- The portal was down for everyone who had signed in β the same shape three weeks earlier: a function that existed, in a file the caller had not loaded
- Installation
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96