Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Hono } from "hono";
import { cors } from "hono/cors";
import { compress } from "hono/compress";
import { bodyLimit } from "hono/body-limit";
import { requestId } from "hono/request-id";
import { serve } from "@hono/node-server";
import { createLogger, sendInfoAlert, getSupabase, sendCriticalAlert, truncateErrorMessage } from "@percolator/shared";
import { initSentry, sentryMiddleware, flushSentry } from "./middleware/sentry.js";
Expand Down Expand Up @@ -106,6 +107,10 @@ app.use("*", bodyLimit({
onError: (c) => c.json({ error: "Request body too large" }, 413),
}));

// Request ID — generates a UUID per request for log correlation and debugging.
// Respects incoming X-Request-Id headers (e.g., from load balancers).
app.use("*", requestId());

// Default-deny for mutation methods. Until write endpoints are added,
// reject any POST/PUT/DELETE/PATCH requests that reach the API.
// When write routes are needed, apply requireApiKey() from middleware/auth.ts
Expand Down Expand Up @@ -211,13 +216,15 @@ app.get("/", (c) => c.json({

// Global error handler
app.onError((err, c) => {
const reqId = c.get("requestId");
logger.error("Unhandled error", {
requestId: reqId,
error: truncateErrorMessage(err.message, 120),
stack: truncateErrorMessage(err.stack ?? "", 500),
endpoint: c.req.path,
method: c.req.method
});

// Report to Sentry (sentryMiddleware may have already captured it,
// but this ensures errors from middleware chain are also caught)
try {
Expand All @@ -226,14 +233,16 @@ app.onError((err, c) => {
endpoint: c.req.path,
method: c.req.method,
handler: "onError",
request_id: reqId,
},
});
} catch (_sentryErr) {}

// Truncate error message for API response (details only in development)
const showDetails = process.env.NODE_ENV !== "production";
return c.json({
error: "Internal server error",
requestId: reqId,
...(showDetails && { details: truncateErrorMessage(err.message, 200) })
}, 500);
});
Expand Down
2 changes: 2 additions & 0 deletions src/middleware/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export function sentryMiddleware(): MiddlewareHandler {
// Set request context
scope.setTag("http.method", c.req.method);
scope.setTag("http.path", c.req.path);
const reqId = c.get("requestId");
if (reqId) scope.setTag("request_id", reqId);

// Set a hashed API key fingerprint as pseudonymous user ID.
// Avoid sending any raw key material (even a prefix) to third-party services.
Expand Down
4 changes: 2 additions & 2 deletions tests/routes/prices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ describe("prices routes", () => {
expect(res.status).toBe(200);
const data = await res.json();
expect(data.prices).toHaveLength(2);
expect(mockSupabase.order).toHaveBeenCalledWith("timestamp", { ascending: false });
expect(mockSupabase.limit).toHaveBeenCalledWith(100);
expect(mockSupabase.order).toHaveBeenCalledWith("timestamp", { ascending: true });
expect(mockSupabase.limit).toHaveBeenCalledWith(1500);
});

it("should return 400 for invalid slab", async () => {
Expand Down
Loading