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
73 changes: 73 additions & 0 deletions src/plugins/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@
import { AppError } from "../lib/errors";
import { toRequestLimitError } from "../lib/request-limits";

function isHorizonError(error: unknown): error is Error & {
response?: { status?: number };
status?: number;
statusCode?: number;
operation?: string;
code?: string;
} {
if (!error || typeof error !== "object") return false;
const candidate = error as Record<string, unknown>;
const responseStatus = typeof candidate.response === "object" && candidate.response
? (candidate.response as { status?: number }).status
: undefined;
const status = typeof candidate.status === "number" ? candidate.status : undefined;
const code = typeof candidate.code === "string" ? candidate.code : undefined;
const operation = typeof candidate.operation === "string" ? candidate.operation : undefined;
const name = typeof candidate.name === "string" ? candidate.name : undefined;

return (
typeof responseStatus === "number" ||
typeof status === "number" ||
typeof code === "string" ||
typeof operation === "string" ||
name === "TimeoutError" ||
name === "TransportError" ||
name === "BadRequestError" ||
name === "NotFoundError"
);
}

export default fp(async function errorHandlerPlugin(app: FastifyInstance) {
app.setErrorHandler((err: Error, req: FastifyRequest, reply: FastifyReply) => {
const requestId = req.id as string;
Expand Down Expand Up @@ -54,6 +83,50 @@
});
}

const upstreamStatus =
(err as Record<string, unknown>).response && typeof (err as any).response === "object"

Check failure on line 87 in src/plugins/error-handler.ts

View workflow job for this annotation

GitHub Actions / build-test

Conversion of type 'Error' to type 'Record<string, unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
? (err as any).response.status
: (err as any).statusCode ?? (err as any).status;

if (isHorizonError(err) && typeof upstreamStatus === "number") {
const operation = (err as any).operation ?? "Horizon request";
req.log.warn(
{
requestId,
operation,
statusCode: upstreamStatus,
errorCode: (err as any).code ?? "UPSTREAM_ERROR",
err,
},
"Horizon upstream failure"
);

if (upstreamStatus === 429) {
return reply.code(429).send({
code: "RATE_LIMITED",
error: "RATE_LIMITED",
message: "Horizon is rate limiting requests. Please retry shortly.",
requestId,
});
}

if (upstreamStatus >= 500 || upstreamStatus === 408) {
return reply.code(502).send({
code: "UPSTREAM_ERROR",
error: "UPSTREAM_ERROR",
message: `${operation} is temporarily unavailable. Please retry shortly.`,
requestId,
});
}

return reply.code(502).send({
code: "UPSTREAM_ERROR",
error: "UPSTREAM_ERROR",
message: `${operation} failed while contacting the Stellar network.`,
requestId,
});
}

if ((err as any).statusCode === 429) {
return reply.code(429).send({
code: "RATE_LIMITED",
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/openapi.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUi from "@fastify/swagger-ui";
import { config } from "../config";

export default async function openAPIPlugin(app: FastifyInstance) {
export default fp(async function openAPIPlugin(app: FastifyInstance) {
await app.register(fastifySwagger, {
openapi: {
openapi: "3.0.0",
Expand Down Expand Up @@ -117,4 +118,4 @@ export default async function openAPIPlugin(app: FastifyInstance) {
deepLinking: false,
},
});
}
}, { name: "openapi-plugin" });
Loading
Loading