Skip to content

Make the API's optional file extension actually optional - #811

Merged
blaipr merged 1 commit into
mainfrom
fix/an-optional-api-parameter-is-optional
Aug 17, 2026
Merged

Make the API's optional file extension actually optional#811
blaipr merged 1 commit into
mainfrom
fix/an-optional-api-parameter-is-optional

Conversation

@blaipr

@blaipr blaipr commented Aug 17, 2026

Copy link
Copy Markdown
Member

accountFile/upload declares extension optional in its own help. Omitting it answered HTTP 500:

{"error":{"message":"Integrity constraint",
          "detail":"SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'extension' cannot be null"}}

AccountFile.extension is varchar(10) NOT NULL with no default, the repository does not filter nulls out of the insert, and a null model property overrides a column's default — so using the endpoint exactly as documented handed an API client an internal database error.

Why nothing caught it

Two independent reasons, both worth knowing:

  • Every one of the sixteen existing tests passes an extension. The optional path had never been exercised, which is the pattern that keeps producing findings here: prefer an untested surface over an uncovered branch in a tested one.
  • ApiHelpMatchesControllersTest passes, and is right to. It checks that each declared parameter is one its controller reads, with the same required flag — and the help and the controller do agree with each other. They both disagree with the database, which that test has no way to see.

The fix

The extension is derived from the file name when the caller does not give one, which is what the web upload has always done (mb_strtoupper(pathinfo($fileName, PATHINFO_EXTENSION))). The same file arriving by either route is now recorded the same way, instead of one route storing TXT and the other refusing the insert.

A name carrying no extension stores an empty string. That is a fact about the file — plenty of real attachments have none — where null is an insert the server rejects.

Tests

Three cases: no extension given (taken from the name), a name with no extension at all (stored empty), and an extension the caller does give (kept, and normalised the way the web normalises it).

Each reads the stored row rather than the upload's response, which echoes back only the id and the name and so cannot show what was persisted — the thing that was actually wrong.

Reverting the fix fails all three, two of them with the same HTTP 500.

3965 unit + 961 integration pass; PHPStan level 6 on src and PHPCS clean.

`accountFile/upload` declares `extension` optional in its own help. Omitting it
answered HTTP 500:

    {"error":{"message":"Integrity constraint",
              "detail":"SQLSTATE[23000]: ... Column 'extension' cannot be null"}}

`AccountFile.extension` is `varchar(10) NOT NULL` with no default, the
repository does not filter nulls out of the insert, and a null model property
overrides a column's default — so using the endpoint exactly as documented
handed an API client a raw SQLSTATE.

Two things kept it invisible. Every one of the sixteen existing tests passes an
`extension`, so the optional path was never exercised. And
`ApiHelpMatchesControllersTest` passes, correctly: the help and the controller
agree with each other about the parameter being optional. They both disagree
with the database.

The extension is now derived from the file name when it is not given, which is
what the web upload has always done (`mb_strtoupper(pathinfo(...))`), so the
same file arriving by either route is recorded the same way. A name carrying no
extension stores an empty string — a fact about the file — rather than a null
the database refuses.

The new tests read the stored row rather than the upload's own response, which
echoes back only the id and the name and so cannot show what was persisted.
@blaipr
blaipr merged commit 2e9944e into main Aug 17, 2026
8 checks passed
@blaipr
blaipr deleted the fix/an-optional-api-parameter-is-optional branch August 17, 2026 18:38
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