Skip to content

fix(http-server): validate types and lengths for join and create quiz input - #234

Merged
TheCodeHeist-Coder merged 1 commit into
TheCodeHeist-Coder:mainfrom
Anshuu2004:fix/issue-231-input-validation
Nov 5, 2026
Merged

TheCodeHeist-Coder merged 1 commit into
TheCodeHeist-Coder:mainfrom
Anshuu2004:fix/issue-231-input-validation

Conversation

@Anshuu2004

Copy link
Copy Markdown
Contributor

What does this PR do?

joinQuizController and createQuizController only checked that the fields were truthy, so an object, array or number went straight into prisma and came back as a generic 500. There was also no length limit. Now both controllers check the body first and answer through errorResponse(res, 400, ...), same setup as parseCreateQuestion and parseRegisterInput.

  • New apps/http-server/src/utils/quizInput.ts with parseJoinQuiz and parseCreateQuiz (zod, one message for the 400):
    • code: string
    • username: string, trimmed, 1 to 30 characters
    • title: string, 1 to 120 characters
    • description: string, 1 to 2000 characters
  • A request with no JSON body has no req.body at all, which was a 500 from the destructure on both routes. That is a 400 now too (same handling as parseRegisterInput).
  • Nothing touches the database until the body is valid.

Messages:

case response error
missing, null or blank Code and username are required / Title and description are required (same text as before)
wrong type username must be a string, code must be a string, title must be a string, description must be a string
too long username must be at most 30 characters, title must be at most 120 characters, description must be at most 2000 characters

Related issue

Closes #231

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance
  • Tests
  • Build / CI / deployment

Which services does this touch?

  • apps/frontend
  • apps/http-server
  • apps/ws-server
  • apps/genAI
  • packages/db (schema or Prisma changes)
  • Docker / CI

How did you test this?

Baseline is clean main (19334fe). Node 24 here (CI uses 22). I generated the Prisma client first with pnpm --filter @repo/db run db:generate.

In apps/http-server:

clean main this branch
pnpm test (vitest) 8 files, 80 tests pass 10 files, 146 tests pass
pnpm check-types passes passes
pnpm build (tsc -b) passes passes
lint no lint script, turbo runs 0 tasks no lint script, turbo runs 0 tasks

From the repo root:

clean main this branch
pnpm run check-types 9 of 9 tasks pass 9 of 9 tasks pass
pnpm run build 7 of 7 tasks pass 7 of 7 tasks pass
pnpm run lint fails on frontend#lint only, 20 problems (18 errors, 2 warnings) same, 20 problems (18 errors, 2 warnings)

For the root build I set VITE_ADMIN_API_URL on both sides, the vite config in apps/admin throws without it. The frontend lint errors are the known ones from CI-CD.md, this PR doesn't touch the frontend.

Tests:

  • New utils/quizInput.test.ts: object, array, number and boolean for every field, empty, blank and null, the limits (30, 120, 2000 pass, 31, 121, 2001 fail), trim happens before the length check, and a missing or non-object body.
  • New controllers/createQuiz.test.ts: 400 for the bad bodies and prisma.quiz.create is not called, a valid quiz still gives 201.
  • controllers/sessionController.test.ts extended: same bad-body cases, findUnique and $transaction are not called, and the participant is created with the trimmed username.

As a sanity check I put the old controllers back with the new tests and 18 of them fail. With the new controllers all 146 pass.

I also started the built server with a DATABASE_URL that points at nothing and sent real requests with curl:

request clean main this branch
join code: {} / ["ABC123"] 500, PrismaClientValidationError in the log 400 code must be a string
join username: {} / ["ada"] / 42 500 (see below) 400 username must be a string
join 31 character username 500 (see below) 400 username must be at most 30 characters
join with no body 500, TypeError on the destructure 400 Code and username are required
quiz title: {} / ["t"] 500, PrismaClientValidationError in the log 400 title must be a string
quiz description: 42 500, PrismaClientValidationError in the log 400 description must be a string
quiz 121 character title / 2001 character description 500 (see below) 400 at most 120 / 2000 characters
quiz with no body 500, TypeError on the destructure 400 Title and description are required

With no database the join username rows on main stop at findUnique with a connection error, not at the username, and the too long rows on main only fail because there is no database. So those 500s don't prove anything on their own. For the username I called prisma.participant.create directly with {}, ["ada"] and 42, and all three throw PrismaClientValidationError before any connection is made, which matches what the issue describes. On this branch a valid join and a valid quiz get past the check and only fail on the missing database.

I did not run this against a real Postgres or with Docker, and I did not try it in the browser. The valid paths (join storing the trimmed username, quiz create) are covered with mocks only.

Checklist

  • I ran pnpm run lint
  • I ran pnpm run check-types
  • I ran pnpm run build
  • The app still starts with docker-compose up
  • I updated documentation if this changes behaviour or setup
  • I did not commit any .env file, API key or other secret

Lint is unticked because the root command fails on the frontend errors above, same as main. docker-compose up was not run.

Notes

  • Update path: there isn't one. routes/quiz.ts has no PUT or PATCH for a quiz, only PUT /:quizId/questions/:questionId, which already goes through parseCreateQuestion. The other quiz writes in the repo (generateAccessCodeController, endQuizSession.ts in ws-server, routes/sessions.ts in admin-server) don't write title or description. So there was nothing else to cover.
  • I kept the two existing "are required" messages as they were (fix: send join 400 through errorResponse so the frontend can read it #229 pinned the join one in a test and JoinQuiz.tsx shows it). The new messages follow the <field> must be ... wording from registerInput.ts and questionInput.ts.
  • If several fields are wrong you get the first one only, same as parseCreateQuestion.
  • Small behaviour changes: a whitespace-only username, title or description used to be accepted and is now a 400, and a whitespace-only code was a 404 and is now a 400. username is stored trimmed. JoinQuiz.tsx already trims, so nothing changes for the app there.
  • title and description are returned as sent, not trimmed, like parseRegisterInput does, so their limits count any surrounding spaces. The issue only asked for the trim on username.
  • Lengths are JS string length, so an emoji can count as 2.
  • This only checks new joins and new quizzes. Rows already over the limits stay as they are, and the schema did not change.
  • Frontend not touched. JoinQuiz.tsx shows the server message, so a long nickname gets the real reason. Dashboard.tsx shows a generic "Failed to create the quiz" for any failure, so someone with a 121 character title won't see why. Neither form has a maxLength. I can add those in a follow-up if you want.

code, username, title and description were only checked for being truthy,
so an object, array or number got through to prisma and came back as a 500.
A request with no body was a 500 too.

parseJoinQuiz and parseCreateQuiz in utils/quizInput.ts now check the type
and length (username 30 after trim, title 120, description 2000) and the
controllers answer with errorResponse 400.

Closes TheCodeHeist-Coder#231
@TheCodeHeist-Coder
TheCodeHeist-Coder merged commit eaf4086 into TheCodeHeist-Coder:main Nov 5, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: validate types and lengths for join username and quiz title/description (500 on non-string input)

2 participants