All Docs
FeaturesCalmony PayUpdated March 15, 2026

Security Fix: Error Message Safety in tRPC (SEC-23)

Security Fix: Error Message Safety in tRPC (SEC-23)

Version: 1.0.77
Control: SEC-23
Category: Data Protection

Overview

Release 1.0.77 resolves a data protection issue where raw internal error messages could be exposed to API consumers through tRPC JSON responses in production. This post explains what the problem was, what changed, and what it means for integrators.

The Problem

Several tRPC mutation handlers in the customers router were using plain JavaScript Error objects:

// Before — unsafe in production
throw new Error('No projects found');
throw new Error('Subscription not found');

tRPC v11 will catch these and return an INTERNAL_SERVER_ERROR HTTP response, but without an explicit error formatter, the original message string may still appear in the JSON response body:

{
  "error": {
    "json": {
      "message": "No projects found",
      "code": -32603,
      "data": {
        "code": "INTERNAL_SERVER_ERROR",
        "httpStatus": 500
      }
    }
  }
}

Messages like these reveal internal data model assumptions and resource naming conventions to any client that receives an error response — this is classified as an information disclosure risk under SEC-23.

What Changed

1. TRPCError replaces raw Error throws

All raw throw new Error(...) calls in src/lib/routers/customers.ts have been replaced with TRPCError, which gives explicit control over the error code and message:

import { TRPCError } from '@trpc/server';

// After — explicit, structured errors
throw new TRPCError({
  code: 'NOT_FOUND',
  message: 'No projects found',
});

Using TRPCError with appropriate codes (e.g. NOT_FOUND, BAD_REQUEST) also produces more semantically correct HTTP status codes for clients.

2. Production errorFormatter added to trpc.ts

An errorFormatter has been added to the tRPC initialisation to ensure that any INTERNAL_SERVER_ERROR reaching a production client returns only a generic message, regardless of the underlying throw:

errorFormatter({ shape }) {
  if (
    process.env.NODE_ENV === 'production' &&
    shape.data.code === 'INTERNAL_SERVER_ERROR'
  ) {
    return {
      ...shape,
      message: 'An internal error occurred.',
    };
  }
  return shape;
},

This acts as a safety net across all routers — even if a raw Error is thrown unexpectedly elsewhere, internal message strings will not reach the client in production.

Environment Behaviour

EnvironmentINTERNAL_SERVER_ERROR messageOther error codes
production'An internal error occurred.'Unchanged
developmentOriginal message (for debugging)Unchanged
testOriginal message (for debugging)Unchanged

Impact on Integrators

  • No changes to successful response shapes. This fix only affects error responses.
  • If your integration previously parsed specific INTERNAL_SERVER_ERROR message strings to infer state (which is not a supported pattern), those strings will now be replaced with the generic message in production.
  • Error codes such as NOT_FOUND, BAD_REQUEST, and UNAUTHORIZED are unaffected and continue to return their configured messages.

Recommended Practice

When building on Calmony Pay's tRPC routers, always throw TRPCError with an explicit code rather than a plain Error:

// ✅ Correct
throw new TRPCError({ code: 'NOT_FOUND', message: 'Subscription not found' });

// ❌ Avoid
throw new Error('Subscription not found');

This ensures errors are classified correctly, produce the right HTTP status codes, and behave consistently across environments.