Loading...
Loading...
Implement secure error handling to prevent information leakage and provide appropriate error responses. Use this skill when you need to handle errors in API routes, prevent stack trace exposure, implement environment-aware error messages, or use the error handler utilities. Triggers include "error handling", "handle errors", "error messages", "information leakage", "stack trace", "handleApiError", "production errors", "error responses".
npx skill4agent add harperaa/secure-claude-skills secure-error-handlingError: column 'credit_cards.number' does not existcredit_cardsError at /var/www/app/lib/payment.js:47Stripe API error: Invalid API key formatPostgreSQL 9.4 connection failed{
error: "Database connection failed",
stack: "Error: connection timeout at db.connect (database.js:42:15)...",
context: "user-profile-update",
timestamp: "2025-10-15T10:30:00Z"
}{
error: "Internal server error",
message: "An unexpected error occurred. Please try again later."
}lib/errorHandler.tsimport { handleApiError } from '@/lib/errorHandler';
async function handler(request: NextRequest) {
try {
// Risky operation
await processPayment(data);
return NextResponse.json({ success: true });
} catch (error) {
return handleApiError(error, 'payment-processing');
// Production: "Internal server error"
// Development: Full stack trace
}
}import { handleValidationError } from '@/lib/errorHandler';
if (!isValidEmail(email)) {
return handleValidationError(
'Validation failed',
{ email: 'Invalid email format' }
);
}{
"error": "Validation failed",
"details": {
"email": "Invalid email format"
}
}import { handleForbiddenError } from '@/lib/errorHandler';
// Check if user owns this resource
if (resource.userId !== userId) {
return handleForbiddenError('You do not have access to this resource');
}{
"error": "Forbidden",
"message": "You do not have access to this resource"
}import { handleUnauthorizedError } from '@/lib/errorHandler';
import { auth } from '@clerk/nextjs/server';
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError('Authentication required');
}{
"error": "Unauthorized",
"message": "Authentication required"
}import { handleNotFoundError } from '@/lib/errorHandler';
const post = await db.posts.findOne({ id: postId });
if (!post) {
return handleNotFoundError('Post');
}{
"error": "Not found",
"message": "Post not found"
}// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { validateRequest } from '@/lib/validateRequest';
import { idSchema } from '@/lib/validation';
import {
handleApiError,
handleUnauthorizedError,
handleForbiddenError,
handleNotFoundError,
handleValidationError
} from '@/lib/errorHandler';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
// Authentication check
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError('Please sign in to view posts');
}
// Validate ID parameter
const validation = validateRequest(idSchema, params.id);
if (!validation.success) {
return handleValidationError('Invalid post ID', { id: 'Must be valid ID' });
}
const postId = validation.data;
// Fetch post
const post = await db.posts.findOne({ id: postId });
// Handle not found
if (!post) {
return handleNotFoundError('Post');
}
// Check authorization
if (post.userId !== userId && !post.isPublic) {
return handleForbiddenError('You do not have access to this post');
}
return NextResponse.json({ post });
} catch (error) {
// Catch unexpected errors
return handleApiError(error, 'get-post');
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
const validation = validateRequest(idSchema, params.id);
if (!validation.success) {
return handleValidationError('Invalid post ID', validation.error);
}
const postId = validation.data;
const post = await db.posts.findOne({ id: postId });
if (!post) {
return handleNotFoundError('Post');
}
// Only post owner can delete
if (post.userId !== userId) {
return handleForbiddenError('Only the post author can delete this post');
}
await db.posts.delete({ id: postId });
return NextResponse.json({ success: true });
} catch (error) {
return handleApiError(error, 'delete-post');
}
}// app/api/process-payment/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { auth } from '@clerk/nextjs/server';
import { handleApiError, handleUnauthorizedError, handleValidationError } from '@/lib/errorHandler';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
async function paymentHandler(request: NextRequest) {
try {
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
const body = await request.json();
const { amount, paymentMethodId } = body;
// Validate amount
if (!amount || amount < 50) {
return handleValidationError('Invalid amount', {
amount: 'Amount must be at least $0.50'
});
}
// Process payment
try {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
payment_method: paymentMethodId,
confirm: true,
metadata: { userId }
});
return NextResponse.json({
success: true,
paymentIntentId: paymentIntent.id
});
} catch (stripeError: any) {
// Handle Stripe-specific errors
console.error('Stripe error:', stripeError);
// Don't expose Stripe error details to client
if (stripeError.type === 'StripeCardError') {
return NextResponse.json(
{
error: 'Payment failed',
message: 'Your card was declined. Please try a different payment method.'
},
{ status: 400 }
);
}
// Generic error for other Stripe issues
return NextResponse.json(
{
error: 'Payment processing failed',
message: 'Unable to process payment. Please try again later.'
},
{ status: 500 }
);
}
} catch (error) {
// Catch-all for unexpected errors
return handleApiError(error, 'process-payment');
}
}
export const POST = withRateLimit(withCsrf(paymentHandler));
export const config = {
runtime: 'nodejs',
};// app/api/users/[id]/profile/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { validateRequest } from '@/lib/validateRequest';
import { updateProfileSchema } from '@/lib/validation';
import {
handleApiError,
handleUnauthorizedError,
handleForbiddenError,
handleNotFoundError
} from '@/lib/errorHandler';
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
// Users can only update their own profile
if (params.id !== userId) {
return handleForbiddenError('You can only update your own profile');
}
const body = await request.json();
// Validate input
const validation = validateRequest(updateProfileSchema, body);
if (!validation.success) {
return validation.response;
}
const { displayName, bio, website } = validation.data;
// Update profile
try {
const updatedProfile = await db.profiles.update(
{ userId },
{
displayName,
bio,
website,
updatedAt: Date.now()
}
);
if (!updatedProfile) {
return handleNotFoundError('Profile');
}
return NextResponse.json({ profile: updatedProfile });
} catch (dbError: any) {
// Log database error for debugging
console.error('Database error:', dbError);
// Don't expose database structure to client
if (dbError.code === 'UNIQUE_VIOLATION') {
return NextResponse.json(
{
error: 'Update failed',
message: 'This username is already taken'
},
{ status: 409 }
);
}
// Generic database error
return NextResponse.json(
{
error: 'Database error',
message: 'Failed to update profile. Please try again.'
},
{ status: 500 }
);
}
} catch (error) {
return handleApiError(error, 'update-profile');
}
}import { NextResponse } from 'next/server';
export function handleApiError(error: unknown, context: string) {
console.error(`[${context}] Error:`, error);
if (process.env.NODE_ENV === 'production') {
// Production: Generic error
return NextResponse.json(
{
error: 'Internal server error',
message: 'An unexpected error occurred. Please try again later.'
},
{ status: 500 }
);
} else {
// Development: Full error details
return NextResponse.json(
{
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
context,
timestamp: new Date().toISOString()
},
{ status: 500 }
);
}
}
export function handleValidationError(
message: string,
details: Record<string, string>
) {
return NextResponse.json(
{
error: 'Validation failed',
message,
details
},
{ status: 400 }
);
}
export function handleForbiddenError(message?: string) {
return NextResponse.json(
{
error: 'Forbidden',
message: message || 'Access denied'
},
{ status: 403 }
);
}
export function handleUnauthorizedError(message?: string) {
return NextResponse.json(
{
error: 'Unauthorized',
message: message || 'Authentication required'
},
{ status: 401 }
);
}
export function handleNotFoundError(resource: string) {
return NextResponse.json(
{
error: 'Not found',
message: `${resource} not found`
},
{ status: 404 }
);
}process.env// ✅ Good logging
console.error('Payment failed', {
userId,
errorCode: error.code,
errorType: error.type,
timestamp: new Date().toISOString(),
path: request.nextUrl.pathname
});
// ❌ Bad logging
console.error('Payment failed', {
userId,
creditCard: cardNumber, // ❌ Never log payment info
apiKey: stripeKey, // ❌ Never log secrets
request: req.body // ❌ May contain sensitive data
});const SENSITIVE_FIELDS = [
'password', 'token', 'secret', 'apiKey', 'ssn',
'creditCard', 'cvv', 'cardNumber'
];
function safelog(data: any) {
const sanitized = { ...data };
SENSITIVE_FIELDS.forEach(field => {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
});
console.log(sanitized);
}
// Usage
safelog({
userId: 'user123',
email: 'user@example.com',
password: 'secret123' // Will be [REDACTED]
});// lib/logger.ts
export function logSecurityEvent(event: {
type: string;
userId?: string;
ip?: string;
details?: Record<string, any>;
}) {
const logEntry = {
...event,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
};
if (process.env.NODE_ENV === 'production') {
// Send to logging service (Vercel logs, Datadog, etc.)
console.log(JSON.stringify(logEntry));
} else {
// Pretty print in development
console.log('Security Event:', logEntry);
}
}
// Usage
logSecurityEvent({
type: 'UNAUTHORIZED_ACCESS_ATTEMPT',
userId,
ip: request.ip,
details: {
path: request.nextUrl.pathname,
method: request.method
}
});// components/ErrorDisplay.tsx
export function ErrorDisplay({ error }: { error: ApiError }) {
const getMessage = () => {
switch (error.status) {
case 400:
return error.details
? Object.entries(error.details).map(([field, msg]) =>
`${field}: ${msg}`
).join(', ')
: 'Invalid input. Please check your data.';
case 401:
return 'Please sign in to continue.';
case 403:
return 'You don\'t have permission to do that.';
case 404:
return 'The requested resource was not found.';
case 429:
return 'Too many requests. Please wait a moment.';
case 500:
return 'Something went wrong. Please try again later.';
default:
return 'An error occurred. Please try again.';
}
};
return (
<div className="error-message">
{getMessage()}
</div>
);
}async function createPost(data: PostData) {
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
// Handle different error types
switch (response.status) {
case 400:
// Validation error - show field errors
if (error.details) {
showFieldErrors(error.details);
}
break;
case 401:
// Redirect to login
router.push('/sign-in');
break;
case 403:
// Show access denied message
alert(error.message);
break;
case 429:
// Rate limited - show retry message
alert(`Too many requests. Please wait ${error.retryAfter} seconds.`);
break;
default:
// Generic error
alert('An error occurred. Please try again.');
}
return null;
}
return await response.json();
} catch (error) {
console.error('Network error:', error);
alert('Network error. Please check your connection.');
return null;
}
}input-validationvalidateRequest()auth-securitysecurity-testing