Skip to content

Public profile routes register nested app.get() handlers at request time, causing production 500s #309

@Ridanshi

Description

@Ridanshi

Summary

Two major public-facing routes in public.ts register nested app.get() handlers inside existing route handlers.

This causes Fastify to attempt route registration at request time after the server has already started, which throws runtime errors and makes the endpoints unusable in production.

Affected endpoints:

  • GET /api/u/:username
  • GET /api/u/:username/card/:cardId

The QR endpoint (/:username/qr) is not affected because it uses the correct single-handler pattern.


Affected File

apps/backend/src/routes/public.ts

Root Cause

The outer routes are registered correctly during initialization:

app.get('/:username', {
  config: {
    rateLimit: {
      max: 100,
      timeWindow: '1 minute',
    },
  },
}, async (request, reply) => {

However, inside the handler body, another app.get() is registered:

app.get('/:username', async (request, reply) => {
  // actual logic
});

This means Fastify attempts to add routes during request execution rather than during server startup.

Fastify rejects this with runtime errors similar to:

Cannot add new route to the server after it has started

The same nested-registration pattern also exists for:

/:username/card/:cardId

Why This Is Difficult To Detect

The structure visually resembles a refactor where rate-limit config was added around an existing route.

Because TypeScript allows app.get() calls inside async functions, this passes compilation without warnings.

Additionally, current tests only cover:

GET /:username/qr

which uses the correct implementation pattern and therefore hides the failure entirely.


Production Impact

The primary public profile routes become unusable:

  • public DevCard profile pages fail,
  • shared card pages fail,
  • follow/view state logic never executes,
  • public profile retrieval is inaccessible.

Expected:

GET /api/u/testuser
→ 200 OK

Actual:

GET /api/u/testuser
→ 500 Internal Server Error

Reproduction

Attempt:

curl https://api.devcard.app/api/u/testuser

Result:

500 Internal Server Error

Meanwhile:

curl https://api.devcard.app/api/u/testuser/qr

continues working because it does not use nested route registration.


Proposed Fix

Collapse the nested handlers into a single handler and move all route logic into the outer registered route.

Example:

app.get('/:username', {
  config: {
    rateLimit: {
      max: 100,
      timeWindow: '1 minute',
    },
  },
}, async (request, reply) => {
  const { username } = request.params;

  // existing logic here directly
});

Apply the same fix to:

/:username/card/:cardId

Acceptance Criteria

  • No nested app.get() calls remain inside request handlers
  • Public profile routes return correct responses
  • Card-sharing routes work correctly
  • Existing QR endpoint behavior remains unchanged
  • Integration coverage added for affected public routes

Severity

Critical

This breaks the primary public-sharing functionality in production while tests remain green due to incomplete route coverage.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions