Skip to content

Fix SRD monster and spell filters broken by Express 5 query parser change - #352

Open
HarmlessHarm wants to merge 3 commits into
developfrom
claude/srd-monster-filter-bug-c1ds35
Open

Fix SRD monster and spell filters broken by Express 5 query parser change#352
HarmlessHarm wants to merge 3 commits into
developfrom
claude/srd-monster-filter-bug-c1ds35

Conversation

@HarmlessHarm

Copy link
Copy Markdown
Collaborator

Refs #351

Fixes the SRD monster filter, where only filtering by name had any effect and type, size, alignment, environment and CR were silently ignored.

Root cause

The API was upgraded to Express 5, which changed the default query parser from extended (qs) to simple (Node's querystring). The simple parser does not interpret the [] suffix, so type[]=Beast becomes the literal key "type[]" and the filter is dropped. Confirmed against the live API: type=Beast works, type[]=Beast is ignored.

name and source are single plain keys, which is why filtering by name kept working.

query parser is application-wide, so /spells broke identically (school[], classes[], level[]). /items and /conditions only send name= and are unaffected.

Repeated plain keys parse to an array under both the simple and extended parsers, so this change is correct whether or not the API also restores app.set("query parser", "extended"). See #351 for the API-side follow-up.

Changes

1. Send multi-value filters as repeated plain keyssrc/services/api/monsters.js, src/services/api/spells.js

before:  type[]=Beast&environment[]=forest&challenge_rating[]=1
after:   type=Beast&environment=forest&challenge_rating=1

Multiple selections repeat the key: type=Beast&type=Dragon.

2. Fix the challenge rating range expansionsrc/services/api/monsters.js, src/utils/generalConstants.js, src/components/hk-components/hk-filter.vue

A separate, real bug that was masked by the above. CRs were expanded with lodash.range(min, max + 1), which steps by 1 from min — only correct when min is a whole number. Since 2.41.0 the slider stores real CR values (0, 1/8, 1/4, 1/2, 1-30) rather than slider positions, so a range starting at a fraction produced 0.125, 1.125, 2.125, …, matching nothing. The params were being dropped anyway, but this would have surfaced the moment the param names were fixed.

CRs are now sliced out of the canonical CR list, which also removes the min === 0 special case that force-added 1/8, 1/4 and 1/2 to any range starting at 0 (so a 0 – 0 range returned those too).

The CR list and its fraction labels moved to generalConstants.js so hk-filter and the monster service share one definition — that drift is what caused this in the first place.

3. URL-encode query values — both services

Values were interpolated raw, so multi-word options ("Swarm of tiny beasts", and nearly every alignment such as "Lawful good") and searches containing & or + corrupted the query string.

4. Minorhk-filter drops challenge_ratings from the filter when the slider covers the full range, so it stops sending every CR as a parameter and stops showing a phantom count on the active-filter badge.

Verification

The API host is not reachable from the dev environment (egress policy), so query-string generation was verified in isolation rather than against live responses:

Filter Generated query
Type = Beast type=Beast
Type = Beast, Dragon type=Beast&type=Dragon
Environment = forest, hill + Size = Large size=Large&environment=forest&environment=hill
Type = "Swarm of tiny beasts" type=Swarm%20of%20tiny%20beasts
CR 1/2 – 2 challenge_rating=0.5&challenge_rating=1&challenge_rating=2
CR 0 – 0 challenge_rating=0 (previously also returned 1/8, 1/4, 1/2)

hk-filter.vue was compiled with the project's vue-template-compiler to confirm the template still builds. Worth a manual pass over the encounter builder, the public compendium and the spell filter before merging.

Spec: .planning/fix-srd-monster-filter.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01JsnXZN9Gbx5a6aCieuW6cR


Generated by Claude Code

claude added 3 commits July 26, 2026 22:17
The CR range was expanded with lodash range(min, max + 1), which steps by 1
from min. That only yields valid CRs when min is a whole number. Since the CR
slider started storing real CR values (0, 1/8, 1/4, 1/2, 1-30) instead of
slider positions, a range starting at 1/8, 1/4 or 1/2 produced 0.125, 1.125,
2.125, ... which matches no monster, so the query came back empty.

Because the filter object is spread into every request and persists until
"Clear filter" is pressed, one poisoned challenge_ratings entry also made
type, size, alignment and environment appear broken, while a plain name
search done before opening the dialog still worked.

Pick the CRs out of the canonical CR list instead. This also drops the
min === 0 special case, which force-added 1/8, 1/4 and 1/2 to any range
starting at 0, so a 0-0 range returned those as well.

Also:
- Move the CR list and fraction labels to generalConstants so hk-filter and
  the monster service share one definition.
- Drop challenge_ratings from the filter when the slider covers the full
  range, so it stops counting towards the active filter badge.
- URL-encode query values in the monster and spell services. Multi-word
  options ("Swarm of tiny beasts", "Lawful good") and searches containing
  & or + were corrupting the query string.
The API no longer accepts the bracket form for multi-value filters:
type=Beast works, type[]=Beast is ignored. Every multi-value monster
filter used brackets - type[], size[], environment[], alignment[] and
challenge_rating[] - so all of them were silently dropped and results
came back unfiltered. name and source are single plain keys, which is
why filtering by name kept working.

Multiple selections are now sent as the key repeated
(type=Beast&type=Dragon).

/spells still sends school[], classes[] and level[]. If the API change
was global rather than limited to /monsters those need the same
treatment, but that is unverified, so they are left alone rather than
guessed at.
Express 5 changed the API's default query parser from extended (qs) to
simple (Node querystring), which does not interpret the [] suffix. That
setting is application-wide, so /spells broke the same way /monsters
did: school[], classes[] and level[] were all ignored.

/items and /conditions only send name=, so they are unaffected.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
27.8% Duplication on New Code (required ≤ 5%)

See analysis details on SonarQube Cloud

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