Skip to content

fix(api): refuse structured run parameters at the boundary (ADR D72) - #265

Merged
thiagoluga merged 2 commits into
masterfrom
fix/reject-complex-run-parameters
Aug 5, 2026
Merged

fix(api): refuse structured run parameters at the boundary (ADR D72)#265
thiagoluga merged 2 commits into
masterfrom
fix/reject-complex-run-parameters

Conversation

@thiagoluga

Copy link
Copy Markdown
Owner

The last item of docs/STATUS-AND-BACKLOG.md §6 that did not need a maintainer decision.

The defect

Array/object parameter values were documented out of scope for v1, but nothing rejected them — and what happened next depended on which backend ran the job:

Path What the source received
sync, in-memory jobs a JsonElement — the very type an ADO provider cannot bind
Hangfire raw JSON text, after a round-trip through JobParameters

Either way the caller learned about the limit as a driver error partway through a run, attributed to the source rather than to the request that caused it.

The fix

POST /reports/{name}/run answers 400 naming the offending parameter. That makes the documented limit real and identical on every backend, and moves the failure to the moment the caller can act on it.

Scalars are untouched. null explicitly still binds — an optional parameter is an ordinary thing to send, and there's a test pinning that the guard didn't over-reach into rejecting it.

The check is deliberately not applied to source property bags, which travel the same object?-valued shape and go through the same normalizer. Those are a provider's own configuration surface rather than a value bound into a query, and nothing in the audit showed them failing this way — so widening the guard there would be speculation, not a fix.

Tests

Five added; three verified to fail against the unfixed code:

  • Array parameter refused, covered as a [Theory] over both run modes — the divergence between them was the defect, so covering only one would miss half of it.
  • Object parameter refused.
  • null still accepted.

Each rejection test also asserts the source captured nothing, i.e. the request was refused before anything ran.

AspNetCore.IntegrationTests: 159/159, 0 failures.


With this merged, §6's remaining entries are only the ones I flagged as needing your call — chiefly the ⚠️ S3 key template not guarding caller-controlled parameters, which is breaking to fix for anyone passing a hierarchy fragment as a run parameter.

Array/object parameter values were documented out of scope for v1, but nothing rejected
them, and what happened next depended on which backend ran the job: the sync and
in-memory paths handed the source a JsonElement — the very type an ADO provider cannot
bind — while Hangfire round-tripped the bag and handed over raw JSON text. Either way
the caller learned about the limit as a driver error partway through a run, attributed
to the source rather than to the request that caused it.

POST /reports/{name}/run now answers 400 naming the offending parameter, so the
documented limit is real and identical on every backend, and the failure lands where the
caller can act on it. Scalars are untouched — null explicitly still binds, since an
optional parameter is an ordinary thing to send.

Deliberately not applied to source property bags, which travel the same object?-valued
shape: those are a provider's own configuration surface rather than a value bound into a
query, and nothing in the audit showed them failing this way.

Five tests, three of them verified to fail against the unfixed code. Both run modes are
covered because the divergence between them was the defect, and the null case pins that
the guard did not over-reach.

This closes the last item of docs/STATUS-AND-BACKLOG.md §6 that did not need a
maintainer decision.
…eries they are

CodeQL flagged two loops that filter their sequence implicitly (cs/linq/missed-where,
alerts 293 and 294). Both are mine: 294 is this PR's parameter check, and 293 is the
Link-header loop on master — #264 cleared cs/linq/missed-select there and CodeQL
immediately raised missed-where on the same loop, so the guard-on-the-way-in shape
traded one alert for another rather than fixing the cause.

Splitting the parse of a single RFC 8288 link-value into its own method leaves
ParseLinkHeaderNext reading as what it actually is — the first rel="next" target, if
any — and removes the whole class of finding instead of moving it. The parameter check
becomes the FirstOrDefault it always was; a default KeyValuePair has a null Key, which
is precisely the "no complex parameter" answer.

Behaviour is unchanged: Http 60/60, AspNetCore 159/159.

The explanatory comment moved from between `=>` and the expression into the XML doc —
dotnet format rejects a comment in that position, and it belongs with the rest of the
method's documentation anyway.
@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@thiagoluga
thiagoluga merged commit 8063125 into master Aug 5, 2026
5 checks passed
@thiagoluga
thiagoluga deleted the fix/reject-complex-run-parameters branch August 5, 2026 10:46
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.

2 participants