fix: Catch mysqli_sql_exception in DB fallback handlers for fresh Docker installs by jekkos · Pull Request #4525 · opensourcepos/opensourcepos · GitHub
Skip to content

fix: Catch mysqli_sql_exception in DB fallback handlers for fresh Docker installs#4525

Merged
jekkos merged 2 commits into
masterfrom
fix/docker-500-fresh-install
Apr 22, 2026
Merged

fix: Catch mysqli_sql_exception in DB fallback handlers for fresh Docker installs#4525
jekkos merged 2 commits into
masterfrom
fix/docker-500-fresh-install

Conversation

@jekkos

@jekkos jekkos commented Apr 21, 2026

Copy link
Copy Markdown
Member

Summary

  • Fixes HTTP 500 error on fresh Docker installs by catching mysqli_sql_exception (not just DatabaseException) in three database fallback handlers
  • Removes unused suppliers LEFT JOIN in Item::get_item_id() per review feedback
  • Narrows catch blocks from \Throwable to \Exception per review feedback
  • Allows the login/migration page to render on first access so the initial schema migration can run

Problem

On a fresh docker compose up, the database starts completely empty (no tables). The first HTTP request triggers this chain:

  1. CSRF filter → calls service('security') → calls service('session')
  2. Session → tries DatabaseHandler::read() on ospos_sessions table
  3. CRASH: MySQLi throws mysqli_sql_exception: Table 'ospos.ospos_sessions' doesn't exist
  4. Result: HTTP 500 — the login/migration page is never reached

The existing fallback code from PR #4467 catches DatabaseException, but MySQLi throws mysqli_sql_exception which extends RuntimeException, not DatabaseException. So the catch block never fires and the exception goes unhandled.

Changes

1. Session.php — Catch \Exception instead of DatabaseException

When sessions can't use the database (no tables), fall back to FileHandler so the app can still serve the login/migration page.

2. OSPOS.php — Catch \Exception instead of DatabaseException

When app_config table doesn't exist, fall back to empty default settings.

3. MY_Migration.php — Catch \Exception in two places

When checking current schema version on an empty database, return 0/false gracefully instead of crashing.

4. Item.php — Remove unused suppliers LEFT JOIN

The get_item_id() method LEFT JOINs suppliers but selects nothing from it and the WHERE clause doesn't reference it. This is pure overhead on a hot lookup path (called via sale_lib->get_item_id()). Per CodeRabbit review on PR #4520.

Why \Exception instead of \Throwable

Per review feedback from CodeRabbit: mysqli_sql_exception extends RuntimeException → Exception, so \Exception is sufficient. Using \Throwable would unnecessarily mask programming errors like TypeError and ValueError that should surface during debugging.

Testing

Reproduced the error with a fresh docker compose up:

ERROR - mysqli_sql_exception: Table 'ospos.ospos_sessions' doesn't exist
CRITICAL - Call to a member function getRow() on bool

Verified that the Session fallback to FileHandler correctly triggers with the \Exception catch, allowing the login page to render (HTTP 200).

Note: The current jekkos/opensourcepos:master Docker image doesn't include the PR #4467 Session fallback at all (127 lines vs 150), so it needs to be rebuilt. This fix will take effect once the image is updated.

Related

…ker installs

On a fresh Docker install with an empty database, the ospos_sessions
table doesn't exist yet. The CSRF filter triggers session initialization
before the login/migration page can be reached.

The existing code in Session.php, OSPOS.php, and MY_Migration.php
catches DatabaseException, but the MySQLi driver throws
mysqli_sql_exception (which extends RuntimeException, not
DatabaseException) when the table doesn't exist. This causes an
unhandled exception resulting in HTTP 500.

Fix: Change all three catch blocks from  to
 so that mysqli_sql_exception and any other unexpected
database errors are caught, allowing the app to fall back gracefully:

- Session.php: Falls back to FileHandler so sessions work without DB
- OSPOS.php: Falls back to empty settings so config loads work
- MY_Migration.php: Falls back to version 0 / false so the migration
  check passes gracefully

This allows the login page with migration UI to be served on first
access, so the initial schema migration can run.

Fixes #4524
@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/Config/OSPOS.php (1)

43-52: Narrow the exception catch to avoid masking non-database errors.

catch (\Exception $e) is sufficient and will still handle mysqli_sql_exception (which inherits from RuntimeException → Exception). Using \Throwable is unnecessarily broad and could silence TypeError or ValueError issues that indicate actual coding problems.

♻️ Proposed change
-            } catch (\Throwable $e) {
+            } catch (\Exception $e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Config/OSPOS.php` around lines 43 - 52, The catch block currently uses
catch (\Throwable $e) which is too broad; change it to catch (\Exception $e) in
the same try/catch around the settings load so database-related errors
(including mysqli_sql_exception) are still handled but programmer errors like
TypeError/ValueError are not masked; keep the existing fallback assignment to
$this->settings ('language','language_code','company') and the existing comment
about migrations so behavior is unchanged except for narrowing the exception
type.
app/Libraries/MY_Migration.php (1)

46-49: Narrow these migration fallbacks to \Exception.

Returning 0/false on DB exceptions is appropriate for fresh installs, but catching \Throwable masks all errors, including unrelated PHP Errors and TypeErrors in the migration code itself. In PHP 8.1+, mysqli_sql_exception extends RuntimeException which extends Exception, so catch (\Exception $e) will still catch database exceptions while allowing programming errors to surface and fail the migration appropriately.

♻️ Proposed narrowing
-        } catch (\Throwable $e) {
+        } catch (\Exception $e) {
             // Database not available yet (e.g. fresh install before schema).
             // Catches mysqli_sql_exception which is not a DatabaseException.
             return 0;
@@
-        } catch (\Throwable $e) {
+        } catch (\Exception $e) {
             // Database not available yet (e.g. fresh install before schema).
             // Catches mysqli_sql_exception which is not a DatabaseException.
         }

Also applies to: 80-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Libraries/MY_Migration.php` around lines 46 - 49, The catch blocks in
MY_Migration that currently use catch(\Throwable $e) should be narrowed to
catch(\Exception $e) so programming Errors/TypeErrors are not swallowed; update
the two occurrences (the block returning 0/false for DB-not-ready fallbacks and
the other similar fallback around lines 80–83) to catch \Exception instead,
preserving the existing return 0/false behavior and logging if present so
database-related exceptions (like mysqli_sql_exception) are still handled but
actual PHP errors surface.
app/Config/Session.php (1)

141-147: Prefer catching \Exception to avoid masking unrelated PHP errors.

In PHP 8.2+, mysqli_sql_exception extends RuntimeException which extends Exception, so catch (\Exception $e) will still catch all database exceptions. However, catch (\Throwable $e) unnecessarily catches Error and its subclasses (such as TypeError, ParseError), which could hide legitimate PHP errors. Narrow the catch to \Exception to be explicit about what this code handles.

♻️ Proposed narrowing
-            } catch (\Throwable $e) {
+            } catch (\Exception $e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Config/Session.php` around lines 141 - 147, The catch block in the
Session class currently catches \Throwable which can mask PHP Engine errors;
update the catch to catch \Exception instead so only exceptions are handled. In
the catch that assigns $this->driver = FileHandler::class and $this->savePath =
WRITEPATH . 'session' (inside the Session class initialization where the
DB-backed session setup occurs), replace catch (\Throwable $e) with catch
(\Exception $e) and keep the existing fallback logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/Config/OSPOS.php`:
- Around line 43-52: The catch block currently uses catch (\Throwable $e) which
is too broad; change it to catch (\Exception $e) in the same try/catch around
the settings load so database-related errors (including mysqli_sql_exception)
are still handled but programmer errors like TypeError/ValueError are not
masked; keep the existing fallback assignment to $this->settings
('language','language_code','company') and the existing comment about migrations
so behavior is unchanged except for narrowing the exception type.

In `@app/Config/Session.php`:
- Around line 141-147: The catch block in the Session class currently catches
\Throwable which can mask PHP Engine errors; update the catch to catch
\Exception instead so only exceptions are handled. In the catch that assigns
$this->driver = FileHandler::class and $this->savePath = WRITEPATH . 'session'
(inside the Session class initialization where the DB-backed session setup
occurs), replace catch (\Throwable $e) with catch (\Exception $e) and keep the
existing fallback logic unchanged.

In `@app/Libraries/MY_Migration.php`:
- Around line 46-49: The catch blocks in MY_Migration that currently use
catch(\Throwable $e) should be narrowed to catch(\Exception $e) so programming
Errors/TypeErrors are not swallowed; update the two occurrences (the block
returning 0/false for DB-not-ready fallbacks and the other similar fallback
around lines 80–83) to catch \Exception instead, preserving the existing return
0/false behavior and logging if present so database-related exceptions (like
mysqli_sql_exception) are still handled but actual PHP errors surface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 340cb147-7aab-407e-86a5-3fde499deb4f

📥 Commits

Reviewing files that changed from the base of the PR and between ff7a8d2 and 4b2f06c.

📒 Files selected for processing (3)
  • app/Config/OSPOS.php
  • app/Config/Session.php
  • app/Libraries/MY_Migration.php

…nused JOIN

1. Narrow catch blocks from Throwable to Exception per review feedback.
   mysqli_sql_exception extends RuntimeException which extends Exception,
   so Exception is sufficient and doesn't mask programming errors like
   TypeError or ValueError that should surface.

2. Remove unused suppliers JOIN in get_item_id() per review feedback.
   The method selects nothing from the suppliers table and the WHERE
   clause doesn't reference it, making the LEFT JOIN pure overhead
   on a hot lookup path (called via sale_lib->get_item_id()).
@jekkos jekkos merged commit f1c6fe2 into master Apr 22, 2026
14 checks passed
@jekkos jekkos deleted the fix/docker-500-fresh-install branch April 22, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant