diff --git a/webapp/nodejs/src/app_handlers.ts b/webapp/nodejs/src/app_handlers.ts index 4c5438ed..a4e4b089 100644 --- a/webapp/nodejs/src/app_handlers.ts +++ b/webapp/nodejs/src/app_handlers.ts @@ -26,6 +26,7 @@ import { FARE_PER_DISTANCE, getLatestRideStatus, INITIAL_FARE, + responseError, } from "./common.js"; import type { CountResult } from "./types/util.js"; import { requestPaymentGatewayPostPayment } from "./payment_gateway.js"; @@ -47,8 +48,11 @@ export const appPostUsers = async (ctx: Context) => { reqJson.lastname === "" || reqJson.date_of_birth === "" ) { - return ctx.text( - "required fields(username, firstname, lastname, date_of_birth) are empty", + return responseError( + ctx, + new Error( + "required fields(username, firstname, lastname, date_of_birth) are empty", + ), 400, ); } @@ -86,7 +90,11 @@ export const appPostUsers = async (ctx: Context) => { `INV_${reqJson.invitation_code}`, ); if (coupons.length >= 3) { - return ctx.text("この招待コードは使用できません。", 400); + return responseError( + ctx, + new Error("この招待コードは使用できません。"), + 400, + ); } // ユーザーチェック @@ -95,7 +103,11 @@ export const appPostUsers = async (ctx: Context) => { [reqJson.invitation_code], ); if (inviter.length === 0) { - return ctx.text("この招待コードは使用できません。", 400); + return responseError( + ctx, + new Error("この招待コードは使用できません。"), + 400, + ); } // 招待クーポン付与 @@ -124,14 +136,18 @@ export const appPostUsers = async (ctx: Context) => { ); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; export const appPostPaymentMethods = async (ctx: Context) => { const reqJson = await ctx.req.json<{ token: string }>(); if (reqJson.token === "") { - return ctx.text("token is required but was empty", 400); + return responseError( + ctx, + new Error("token is required but was empty"), + 400, + ); } const user = ctx.var.user; await ctx.var.dbConn.query( @@ -220,7 +236,7 @@ export const appGetRides = async (ctx: Context) => { ); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; @@ -230,8 +246,11 @@ export const appPostRides = async (ctx: Context) => { destination_coordinate: Coordinate; }>(); if (!reqJson.pickup_coordinate || !reqJson.destination_coordinate) { - return ctx.text( - "required fields(pickup_coordinate, destination_coordinate) are empty", + return responseError( + ctx, + new Error( + "required fields(pickup_coordinate, destination_coordinate) are empty", + ), 400, ); } @@ -251,7 +270,7 @@ export const appPostRides = async (ctx: Context) => { } } if (continuingRideCount > 0) { - return ctx.text("ride already exists", 409); + return responseError(ctx, new Error("ride already exists"), 409); } await ctx.var.dbConn.query( "INSERT INTO rides (id, user_id, pickup_latitude, pickup_longitude, destination_latitude, destination_longitude) VALUES (?, ?, ?, ?, ?, ?)", @@ -336,7 +355,7 @@ export const appPostRides = async (ctx: Context) => { ); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; @@ -346,8 +365,11 @@ export const appPostRidesEstimatedFare = async (ctx: Context) => { destination_coordinate: Coordinate; }>(); if (!reqJson.pickup_coordinate || !reqJson.destination_coordinate) { - return ctx.text( - "required fields(pickup_coordinate, destination_coordinate) are empty", + return responseError( + ctx, + new Error( + "required fields(pickup_coordinate, destination_coordinate) are empty", + ), 400, ); } @@ -379,7 +401,7 @@ export const appPostRidesEstimatedFare = async (ctx: Context) => { ); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; @@ -387,7 +409,11 @@ export const appPostRideEvaluatation = async (ctx: Context) => { const rideId = ctx.req.param("ride_id"); const reqJson = await ctx.req.json<{ evaluation: number }>(); if (reqJson.evaluation < 1 || reqJson.evaluation > 5) { - return ctx.text("evaluation must be between 1 and 5", 400); + return responseError( + ctx, + new Error("evaluation must be between 1 and 5"), + 400, + ); } await ctx.var.dbConn.beginTransaction(); try { @@ -396,11 +422,11 @@ export const appPostRideEvaluatation = async (ctx: Context) => { rideId, ); if (!ride) { - return ctx.text("ride not found", 404); + return responseError(ctx, new Error("ride not found"), 404); } const status = await getLatestRideStatus(ctx.var.dbConn, ride.id); if (status !== "ARRIVED") { - return ctx.text("not arrived yet", 400); + return responseError(ctx, new Error("ride not arrived yet"), 400); } const [result] = await ctx.var.dbConn.query( @@ -408,7 +434,7 @@ export const appPostRideEvaluatation = async (ctx: Context) => { [reqJson.evaluation, rideId], ); if (result.affectedRows === 0) { - return ctx.text("ride not found", 404); + return responseError(ctx, new Error("ride not found"), 404); } await ctx.var.dbConn.query( @@ -421,14 +447,14 @@ export const appPostRideEvaluatation = async (ctx: Context) => { rideId, ); if (!ride) { - return ctx.text("ride not found", 404); + return responseError(ctx, new Error("ride not found"), 404); } const [[paymentToken]] = await ctx.var.dbConn.query< Array >("SELECT * FROM payment_tokens WHERE user_id = ?", [ride.user_id]); if (!paymentToken) { - return ctx.text("payment token not registered", 400); + return responseError(ctx, new Error("payment token not registered"), 400); } const fare = await calculateDiscountedFare( ctx.var.dbConn, @@ -457,7 +483,7 @@ export const appPostRideEvaluatation = async (ctx: Context) => { }, ); if (err instanceof ErroredUpstream) { - return ctx.text(`${err}`, 502); + return responseError(ctx, err, 502); } await ctx.var.dbConn.commit(); return ctx.json( @@ -466,9 +492,9 @@ export const appPostRideEvaluatation = async (ctx: Context) => { }, 200, ); - } catch (err) { + } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${err}`, 500); + return responseError(ctx, e, 500); } }; @@ -571,7 +597,7 @@ export const appGetNotification = async (ctx: Context) => { return ctx.json(response, 200); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; @@ -631,23 +657,27 @@ export const appGetNearbyChairs = async (ctx: Context) => { const lonStr = ctx.req.query("longitude"); const distanceStr = ctx.req.query("distance"); if (!latStr || !lonStr) { - return ctx.text("latitude and longitude is empty", 400); + return responseError( + ctx, + new Error("latitude and longitude is empty"), + 400, + ); } const lat = atoi(latStr); if (lat === false) { - return ctx.text("latitude is invalid", 400); + return responseError(ctx, new Error("latitude is invalid"), 400); } const lon = atoi(lonStr); if (lon === false) { - return ctx.text("longitude is invalid", 400); + return responseError(ctx, new Error("longitude is invalid"), 400); } let distance: number | false = 50; if (distanceStr) { distance = atoi(distanceStr); if (distance === false) { - return ctx.text("distance is invalid", 400); + return responseError(ctx, new Error("distance is invalid"), 400); } } @@ -722,9 +752,9 @@ export const appGetNearbyChairs = async (ctx: Context) => { }, 200, ); - } catch (err) { + } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${err}`, 500); + return responseError(ctx, e, 500); } }; diff --git a/webapp/nodejs/src/chair_handlers.ts b/webapp/nodejs/src/chair_handlers.ts index c4de5d66..742b004b 100644 --- a/webapp/nodejs/src/chair_handlers.ts +++ b/webapp/nodejs/src/chair_handlers.ts @@ -2,7 +2,7 @@ import type { Context } from "hono"; import { setCookie } from "hono/cookie"; import type { RowDataPacket } from "mysql2"; import { ulid } from "ulid"; -import { getLatestRideStatus } from "./common.js"; +import { getLatestRideStatus, responseError } from "./common.js"; import type { Environment } from "./types/hono.js"; import type { ChairLocation, @@ -22,8 +22,11 @@ export const chairPostChairs = async (ctx: Context) => { }>(); const { name, model, chair_register_token } = reqJson; if (!name || !model || !chair_register_token) { - return ctx.text( - "some of required fields(name, model, chair_register_token) are empty", + return responseError( + ctx, + new Error( + "some of required fields(name, model, chair_register_token) are empty", + ), 400, ); } @@ -32,7 +35,7 @@ export const chairPostChairs = async (ctx: Context) => { [chair_register_token], ); if (!owner) { - return ctx.text("invalid chair_register_token", 401); + return responseError(ctx, new Error("invalid chair_register_token"), 401); } const chairID = ulid(); const accessToken = secureRandomStr(32); @@ -55,7 +58,7 @@ export const chairPostActivity = async (ctx: Context) => { chair.id, ]); } catch (e) { - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } return ctx.body(null, 204); }; @@ -106,7 +109,7 @@ export const chairPostCoordinate = async (ctx: Context) => { return ctx.json({ recorded_at: location.created_at.getTime() }, 200); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; @@ -169,7 +172,7 @@ export const chairGetNotification = async (ctx: Context) => { ); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; @@ -184,10 +187,10 @@ export const chairPostRideStatus = async (ctx: Context) => { [rideID], ); if (!ride) { - return ctx.text("ride not found", 404); + return responseError(ctx, new Error("ride not found"), 404); } if (ride.chair_id !== chair.id) { - return ctx.text("not assigned to this ride", 400); + return responseError(ctx, new Error("cnot assigned to this ride"), 400); } switch (reqJson.status) { // Acknowledge the ride @@ -201,7 +204,11 @@ export const chairPostRideStatus = async (ctx: Context) => { case "CARRYING": { const status = await getLatestRideStatus(ctx.var.dbConn, ride.id); if (status !== "PICKUP") { - return ctx.text("chair has not arrived yet", 400); + return responseError( + ctx, + new Error("chair has not arrived yet"), + 400, + ); } await ctx.var.dbConn.query( "INSERT INTO ride_statuses (id, ride_id, status) VALUES (?, ?, ?)", @@ -210,12 +217,12 @@ export const chairPostRideStatus = async (ctx: Context) => { break; } default: - return ctx.text("invalid status", 400); + return responseError(ctx, new Error("invalid status"), 400); } await ctx.var.dbConn.commit(); return ctx.body(null, 204); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } }; diff --git a/webapp/nodejs/src/common.ts b/webapp/nodejs/src/common.ts index 5daa62a9..6378bcd2 100644 --- a/webapp/nodejs/src/common.ts +++ b/webapp/nodejs/src/common.ts @@ -1,5 +1,7 @@ import type { Connection, RowDataPacket } from "mysql2/promise"; import type { Ride, RideStatus } from "./types/models.js"; +import type { Context } from "hono"; +import type { StatusCode } from "hono/utils/http-status"; export const INITIAL_FARE = 500; export const FARE_PER_DISTANCE = 100; @@ -59,3 +61,14 @@ export class ErroredUpstream extends Error { this.name = "ErroredUpstream"; } } + +export const responseError = ( + ctx: Context, + error: unknown, + statusCode: StatusCode, +) => { + if (error instanceof Error) { + return ctx.json({ message: error.message }, statusCode); + } + throw new Error("error is not an instance of Error"); +}; diff --git a/webapp/nodejs/src/main.ts b/webapp/nodejs/src/main.ts index 512629d4..29acf3e4 100644 --- a/webapp/nodejs/src/main.ts +++ b/webapp/nodejs/src/main.ts @@ -32,6 +32,7 @@ import type { Environment } from "./types/hono.js"; import { execSync } from "node:child_process"; import { internalGetMatching } from "./internal_handlers.js"; import { createPool } from "mysql2/promise"; +import { responseError } from "./common.js"; const pool = createPool({ host: process.env.ISUCON_DB_HOST || "127.0.0.1", @@ -110,7 +111,7 @@ async function postInitialize(ctx: Context) { try { execSync("../sql/init.sh", { stdio: "inherit" }); } catch (error) { - return ctx.text(`Failed to initialize\n${error}`, 500); + return responseError(ctx, error, 500); } try { await ctx.var.dbConn.query( @@ -118,7 +119,7 @@ async function postInitialize(ctx: Context) { [body.payment_server], ); } catch (error) { - return ctx.text(`Internal Server Error\n${error}`, 500); + return responseError(ctx, error, 500); } return ctx.json({ language: "node" }); } diff --git a/webapp/nodejs/src/middlewares.ts b/webapp/nodejs/src/middlewares.ts index 77135dab..428c096b 100644 --- a/webapp/nodejs/src/middlewares.ts +++ b/webapp/nodejs/src/middlewares.ts @@ -3,12 +3,17 @@ import { createMiddleware } from "hono/factory"; import type { RowDataPacket } from "mysql2/promise"; import type { Environment } from "./types/hono.js"; import type { Chair, Owner, User } from "./types/models.js"; +import { responseError } from "./common.js"; export const appAuthMiddleware = createMiddleware( async (ctx, next) => { const accessToken = getCookie(ctx, "app_session"); if (!accessToken) { - return ctx.text("app_session cookie is required", 401); + return responseError( + ctx, + new Error("app_session cookie is required"), + 401, + ); } try { const [[user]] = await ctx.var.dbConn.query>( @@ -16,11 +21,11 @@ export const appAuthMiddleware = createMiddleware( [accessToken], ); if (!user) { - return ctx.text("invalid access token", 401); + return responseError(ctx, new Error("invalid access token"), 401); } ctx.set("user", user); } catch (error) { - return ctx.text(`Internal Server Error\n${error}`, 500); + return responseError(ctx, error, 500); } await next(); }, @@ -30,18 +35,22 @@ export const ownerAuthMiddleware = createMiddleware( async (ctx, next) => { const accessToken = getCookie(ctx, "owner_session"); if (!accessToken) { - return ctx.text("owner_session cookie is required", 401); + return responseError( + ctx, + new Error("owner_session cookie is required"), + 401, + ); } try { const [[owner]] = await ctx.var.dbConn.query< Array >("SELECT * FROM owners WHERE access_token = ?", [accessToken]); if (!owner) { - return ctx.text("invalid access token", 401); + return responseError(ctx, new Error("invalid access token"), 401); } ctx.set("owner", owner); } catch (error) { - return ctx.text(`Internal Server Error\n${error}`, 500); + return responseError(ctx, error, 500); } await next(); }, @@ -51,18 +60,22 @@ export const chairAuthMiddleware = createMiddleware( async (ctx, next) => { const accessToken = getCookie(ctx, "chair_session"); if (!accessToken) { - return ctx.text("chair_session cookie is required", 401); + return responseError( + ctx, + new Error("chair_session cookie is required"), + 401, + ); } try { const [[chair]] = await ctx.var.dbConn.query< Array >("SELECT * FROM chairs WHERE access_token = ?", [accessToken]); if (!chair) { - return ctx.text("invalid access token", 401); + return responseError(ctx, new Error("invalid access token"), 401); } ctx.set("chair", chair); } catch (error) { - return ctx.text(`Internal Server Error\n${error}`, 500); + return responseError(ctx, error, 500); } await next(); }, diff --git a/webapp/nodejs/src/owner_handlers.ts b/webapp/nodejs/src/owner_handlers.ts index 86c78e33..64bb5b71 100644 --- a/webapp/nodejs/src/owner_handlers.ts +++ b/webapp/nodejs/src/owner_handlers.ts @@ -4,14 +4,18 @@ import { secureRandomStr } from "./utils/random.js"; import { setCookie } from "hono/cookie"; import type { RowDataPacket } from "mysql2"; import type { Chair, Ride } from "./types/models.js"; -import { calculateSale } from "./common.js"; +import { calculateSale, responseError } from "./common.js"; import { ulid } from "ulid"; export const ownerPostOwners = async (ctx: Context) => { const reqJson = await ctx.req.json<{ name: string }>(); const { name } = reqJson; if (!name) { - return ctx.text("some of required fields(name) are empty", 400); + return responseError( + ctx, + new Error("some of required fields(name) are empty"), + 400, + ); } const ownerId = ulid(); const accessToken = secureRandomStr(32); @@ -74,7 +78,7 @@ export const ownerGetSales = async (ctx: Context) => { }); } catch (e) { await ctx.var.dbConn.rollback(); - return ctx.text(`${e}`, 500); + return responseError(ctx, e, 500); } };