From 813855f903d9b580c055403b94cdee6d1c5c1426 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:42:58 +0000 Subject: [PATCH] fix: harden API and Sheets error handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/api/src/lib/errors.ts | 24 ++++++++++++++---- apps/api/test/errors.test.ts | 38 ++++++++++++++++++++++++++++ packages/shared/src/google-sheets.ts | 4 +-- 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 apps/api/test/errors.test.ts diff --git a/apps/api/src/lib/errors.ts b/apps/api/src/lib/errors.ts index c1b28fc..34ad538 100644 --- a/apps/api/src/lib/errors.ts +++ b/apps/api/src/lib/errors.ts @@ -11,11 +11,13 @@ export const invalidQueryError = { message: 'invalid query parameters', }; -const internalError = (e: unknown) => ({ +const internalError = () => ({ error: 'INTERNAL' as const, - message: e instanceof Error ? e.message : String(e), + message: 'internal server error', }); +const sheetsWriteFailedMessage = 'failed to save data'; + const mentorErrorStatus: Record = { EMAIL_ALREADY_EXISTS: 409, NOT_FOUND: 404, @@ -51,16 +53,28 @@ const sessionErrorStatus: Record = { // app.onError(apiErrorHandler) で一括登録して使う。 export const apiErrorHandler: ErrorHandler = (e, c) => { if (e instanceof CheckinError) { - return c.json({ error: e.code, message: e.message }, checkinErrorStatus[e.code]); + return c.json( + { + error: e.code, + message: e.code === 'SHEETS_WRITE_FAILED' ? sheetsWriteFailedMessage : e.message, + }, + checkinErrorStatus[e.code], + ); } if (e instanceof MentorError) { return c.json({ error: e.code, message: e.message }, mentorErrorStatus[e.code]); } if (e instanceof PreRegistrationError) { - return c.json({ error: e.code, message: e.message }, preRegistrationErrorStatus[e.code]); + return c.json( + { + error: e.code, + message: e.code === 'SHEETS_WRITE_FAILED' ? sheetsWriteFailedMessage : e.message, + }, + preRegistrationErrorStatus[e.code], + ); } if (e instanceof SessionError) { return c.json({ error: e.code, message: e.message }, sessionErrorStatus[e.code]); } - return c.json(internalError(e), 500); + return c.json(internalError(), 500); }; diff --git a/apps/api/test/errors.test.ts b/apps/api/test/errors.test.ts new file mode 100644 index 0000000..ef485f8 --- /dev/null +++ b/apps/api/test/errors.test.ts @@ -0,0 +1,38 @@ +import { Hono } from 'hono'; +import { describe, expect, it } from 'vitest'; +import { CheckinError } from '../src/lib/checkin'; +import { apiErrorHandler } from '../src/lib/errors'; + +describe('apiErrorHandler', () => { + it('does not expose unexpected error messages', async () => { + const app = new Hono(); + app.get('/unexpected', () => { + throw new Error('upstream response contains a secret'); + }); + app.onError(apiErrorHandler); + + const response = await app.request('/unexpected'); + + expect(response.status).toBe(500); + expect(await response.json()).toEqual({ + error: 'INTERNAL', + message: 'internal server error', + }); + }); + + it('does not expose Sheets error messages for domain failures', async () => { + const app = new Hono(); + app.get('/sheets-failure', () => { + throw new CheckinError('SHEETS_WRITE_FAILED', 'Sheets response contains a secret'); + }); + app.onError(apiErrorHandler); + + const response = await app.request('/sheets-failure'); + + expect(response.status).toBe(502); + expect(await response.json()).toEqual({ + error: 'SHEETS_WRITE_FAILED', + message: 'failed to save data', + }); + }); +}); diff --git a/packages/shared/src/google-sheets.ts b/packages/shared/src/google-sheets.ts index a1f5fce..f651db8 100644 --- a/packages/shared/src/google-sheets.ts +++ b/packages/shared/src/google-sheets.ts @@ -128,7 +128,7 @@ export const updateSheetRow = async ( values: string[][], ): Promise => { const token = await getCachedAccessToken(encodedServiceAccountKey); - const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}?valueInputOption=USER_ENTERED`; + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}?valueInputOption=RAW`; const resp = await fetch(url, { method: 'PUT', headers: { @@ -152,7 +152,7 @@ export const appendSheetRows = async ( values: string[][], ): Promise => { const token = await getCachedAccessToken(encodedServiceAccountKey); - const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}:append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS`; + const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(range)}:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS`; const resp = await fetch(url, { method: 'POST', headers: {