-
-
Notifications
You must be signed in to change notification settings - Fork 27
Approvals Inbox Said Error
Reported with screenshots by a user Β· Fixed in #1850, #1851 Β· Released in 2.3.1
Forms β Approvals showed the word Error where the list of waiting requests should be, on both pending tabs, for every analyst including administrators.
The part that made it confusing: the counts were right. The sidebar said 2 waiting for me, 2 pending in total, 0 decided by me - correct numbers, beside a list that would not load. And clicking Decided by me, the one tab with nothing in it, produced a perfectly normal empty state.
So the screen looked as though the whole list endpoint was broken, while simultaneously proving it was not. That is the wrong conclusion, and it is the one the screen pushed you towards.
It only happened once a request waiting for approval contained a table question - the field type added in 2.3.0. An install whose pending requests contained no table never saw it at all, which is why it could not be reproduced here until one was deliberately created.
One line, in one function that three different things call.
catalogueAnswerText() turns a stored answer into readable text. Most answers are a plain string; a checkboxes answer is stored as a JSON array, so the function decodes and flattens it:
$decoded = json_decode($val, true); // checkboxes are stored as a JSON array
if (is_array($decoded)) $val = implode(', ', $decoded);A table's answer is also a JSON array - but of objects, not strings. One entry per row, each a map of column id to cell value. implode() cannot render that. It produces the literal text Array, Array and emits a PHP warning for every row.
The warning is the interesting half. On an install with display_errors on, PHP prints it into the response body, ahead of everything the page meant to send:
<br />
<b>Warning</b>: Array to string conversion in .../includes/catalogue_approvals.php on line 380<br />
{"success":true,"items":[...]}
That is no longer JSON. The browser's res.json() threw, the page's catch block ran, and all it had to show was a hard-coded fallback: the word Error.
The page asks for one filter at a time, and every response carries all three counts. "Decided by me" had no rows to flatten, so that request produced no warning, returned clean JSON, and populated the counts. The two tabs that did have rows failed.
Correct counts beside a broken list is therefore not a contradiction - it is the fingerprint of a fault in rendering a row, not in fetching them. Worth remembering, because it points at exactly the opposite of where it looks.
The same function renders two other things:
- the approval notification email sent to the approver
- the
{{submission.fields.*}}merge codes available to workflow rules
Neither is JSON, so neither broke visibly. They simply said Array, Array where the table should have been - including on installs with display_errors off, where nothing appeared wrong at all. Those installs had the bug the whole time and no symptom to report.
π The same mistake was already written up in the code.
FormsService::submitForm()special-cases a grid before its ownimplode(), with a comment saying why: "A grid's rows are OBJECTS, so the generic implode below would render them as 'Array, Array' and emit a PHP warning." The note existed. This copy of the flattener never received it.
A table now renders through FormsService::gridToText() - the renderer that already existed for exactly this - rather than through a third hand-written flattener. It produces one line per row with each cell labelled from the form's own column definitions, so the approver reads "Item: Pencil, Qty: 1" rather than a count or a shrug.
That needed the column definitions, which live in form_fields.config, so the three queries feeding this function now select it.
Two further changes, because the first one only fixes the case we know about:
-
The generic branch can no longer emit that warning at all. Any non-scalar is JSON-encoded before
implode()sees it. A field type nobody has taught this function yet will render awkwardly; it will not take out every endpoint that calls it. -
The endpoint catches
Throwable, notException. A PHPError- aTypeError, a call to something that is not there - is not anException, so it walked straight past the handler, and a fatal still answers HTTP 200. That is the other way this screen ends up with nothing to say.
Even with all of the above, the next unexpected failure would have shown the same bare Error. So the page now reads the response as text first and parses second, keeping the body when the parse fails, and shows a copyable diagnostic box: the screen and tab, the request, the HTTP status, the FreeITSM version, the browser, and the first 800 characters of what the server actually sent.
π Reading the text first is the whole trick.
res.json()throws away the body along with the parse, which is precisely why the original failure had nothing to report. The one piece of evidence that identifies the fault is destroyed by the convenience method.
The server's response is rendered with textContent, never innerHTML - a PHP fatal is an HTML page, and injecting one would turn a diagnostic into a second bug.
| File | What changed |
|---|---|
includes/catalogue_approvals.php |
a table renders through gridToText(); the generic branch is warning-proof; three queries select config
|
api/forms/catalogue_approvals.php |
catches Throwable rather than Exception, and logs |
forms/approvals.php |
reads the response as text first; the diagnostic box |
lang/en,es,de,pl,id/forms.php |
strings for the box |
tests/catalogue-approvals-grid.php |
new |
Not by reading the code - by building the conditions and watching it fail first.
-
Reproduced deliberately: a form with a table question, a submission left pending approval, and the list endpoint called exactly as the page calls it. Two
Array to string conversionwarnings, and a response body thatjson_decodereturnsnullfor. - The new test drives the real endpoint with a forged analyst session, and asserts the thing the bug failed: not "did it return items" but "is the body JSON at all", and that nothing precedes the opening brace.
-
Run against the old code as a control it fails 7 of 14, with the body starting
<br />. -
The diagnostic box was driven with the genuine failing response, lifted out of the page rather than retyped: nine assertions including that the server's
<b>tags arrive as text and that a 5,000-character body is truncated with a count of what was cut.
- Forms - what a table question is
- A table's answers were missing from the PDF - the same field type, a different reader, found the same week
- Bugs resolved
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