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:
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:
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:
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.
Summary
Two major public-facing routes in
public.tsregister nestedapp.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/:usernameGET /api/u/:username/card/:cardIdThe QR endpoint (
/:username/qr) is not affected because it uses the correct single-handler pattern.Affected File
Root Cause
The outer routes are registered correctly during initialization:
However, inside the handler body, another
app.get()is registered:This means Fastify attempts to add routes during request execution rather than during server startup.
Fastify rejects this with runtime errors similar to:
The same nested-registration pattern also exists for:
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:
which uses the correct implementation pattern and therefore hides the failure entirely.
Production Impact
The primary public profile routes become unusable:
Expected:
Actual:
Reproduction
Attempt:
Result:
Meanwhile:
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:
Apply the same fix to:
Acceptance Criteria
app.get()calls remain inside request handlersSeverity
Critical
This breaks the primary public-sharing functionality in production while tests remain green due to incomplete route coverage.