Skip to content

fix(health): fix issue where chain id wasn't cached#98

Open
FranklinWaller wants to merge 1 commit into
mainfrom
patch/broken-health-api
Open

fix(health): fix issue where chain id wasn't cached#98
FranklinWaller wants to merge 1 commit into
mainfrom
patch/broken-health-api

Conversation

@FranklinWaller
Copy link
Copy Markdown
Member

Motivation

(Write your motivation here)

Explanation of Changes

(Write your explanation here)

Testing

(Write your test plan here)

Related PRs and Issues

(Link your related PRs and Issues here)

@Thomasvdam
Copy link
Copy Markdown
Member

I'm not sure if this entirely correct, because now the health check doesn't actually verify that the proxy can reach the chain in future health checks. Maybe wrap it in a cache with TTL until we have time to properly think of how to do a health check mechanism that harmonises core and Fast?

@FranklinWaller FranklinWaller force-pushed the patch/broken-health-api branch from aa88d20 to 48cdfbb Compare March 25, 2026 17:30
@FranklinWaller
Copy link
Copy Markdown
Member Author

@Thomasvdam I changed it to do without caching because we do indeed want to know which proxies are unhealthy. Instead i've opted for just the fastOnly flag. Then the health endpoint won't return a unhealthy status when the chain is upgrading.

@FranklinWaller
Copy link
Copy Markdown
Member Author

Also made the better effect version, this is just a hotfix

#100

Comment on lines +2 to +3
import { Duration, Match, Option, Runtime } from "effect";
import { Effect } from "effect";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { Duration, Match, Option, Runtime } from "effect";
import { Effect } from "effect";
import { Duration, Effect, Match, Option, Runtime } from "effect";

});
});
if (config.fastOnly) {
healthy = true;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant (line 49)

});
group.get("info", async () => {
const chainId = config.fastOnly
? Result.ok(dataProxy.options.chainId)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this not be null?
Just to avoid that if it is fastOnly, it is clear that chainId was not being used (even though it might be set).

Comment on lines 13 to 92
export const statusPlugin = (
context: Context,
dataProxy: DataProxy,
options: Config["statusEndpoints"],
config: Config,
) =>
Effect.gen(function* () {
const runtime = yield* Effect.runtime();

return (app: Elysia) => {
const plugin = new Elysia({
name: "status",
});
return (app: Elysia) =>
Runtime.runSync(
runtime,
Effect.gen(function* () {
const plugin = new Elysia({
name: "status",
});

plugin.group(options.root, (group) => {
if (options.apiKey) {
const { header, secret } = options.apiKey;
group.onBeforeHandle(({ request }) => {
const apiKey = request.headers.get(header);
if (apiKey !== secret) {
return new Response("Unauthorized", { status: 401 });
plugin.group(options.root, (group) => {
if (options.apiKey) {
const { header, secret } = options.apiKey;
group.onBeforeHandle(({ request }) => {
const apiKey = request.headers.get(header);
if (apiKey !== secret) {
return new Response("Unauthorized", { status: 401 });
}
});
}
});
}

// List all available endpoints
group.get("", () => {
return Response.json({
endpoints: [`${options.root}/health`, `${options.root}/info`],
});
});
// List all available endpoints
group.get("", () => {
return Response.json({
endpoints: [`${options.root}/health`, `${options.root}/info`],
});
});

group.get("health", async ({ set }) => {
const chainId = await effectToAsyncResult(
getRpcChainId(dataProxy.options.rpcUrl),
);
const hasCorrectChainId =
chainId.isOk && chainId.value === dataProxy.options.chainId;
set.status = chainId.isOk && hasCorrectChainId ? 200 : 500;
group.get("health", async ({ set }) => {
let healthy = true;

return Response.json({
status: chainId.isOk && hasCorrectChainId ? "healthy" : "unhealthy",
metrics: context.getMetrics(),
});
});
if (config.fastOnly) {
healthy = true;
set.status = 200;
} else {
const chainId = await effectToAsyncResult(runtime, getRpcChainId(dataProxy.options.rpcUrl));
const hasCorrectChainId =
chainId.isOk && chainId.value === dataProxy.options.chainId;

group.get("info", async () => {
const chainId = await effectToAsyncResult(
getRpcChainId(dataProxy.options.rpcUrl),
);
set.status = chainId.isOk && hasCorrectChainId ? 200 : 500;
healthy = chainId.isOk && hasCorrectChainId;
}

return Response.json({
pubKey: context.getPublicKey(),
fastConfig: context.getFastConfig(),
version: getVersions().proxy,
chainId: dataProxy.options.chainId,
rpcChainId: chainId.isOk ? chainId.value : null,
});
});
return Response.json({
status: healthy ? "healthy" : "unhealthy",
metrics: context.getMetrics(),
});
});

return group;
});
group.get("info", async () => {
const chainId = config.fastOnly
? Result.ok(dataProxy.options.chainId)
: await effectToAsyncResult(runtime, getRpcChainId(dataProxy.options.rpcUrl));

Runtime.runSync(
runtime,
Effect.logInfo(
`Status endpoints: /${options.root}/health, /${options.root}/info`,
),
);
return Response.json({
pubKey: context.getPublicKey(),
fastConfig: context.getFastConfig(),
version: getVersions().proxy,
chainId: dataProxy.options.chainId,
rpcChainId: chainId.isOk ? chainId.value : null,
});
});

return app.use(plugin);
};
return group;
});

logger.info(
`Status endpoints: /${options.root}/health, /${options.root}/info`,
);
return app.use(plugin);
}),
);
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if we add the runtime as an expected input param, we could simplify a little bit the statusPlugin code.

On the proxy-server.ts:

// Get the runtime once at startup
const runtime = Effect.runSync(Effect.runtime<never>());

server.use(
	statusPlugin(statusContext, dataProxy, config.statusEndpoints, config, runtime),
);

Entirely optional, we could avoid the outer Effect.gen:

export const statusPlugin = (
    context: Context,
    dataProxy: DataProxy,
    options: Config["statusEndpoints"],
    config: Config,
    runtime: Runtime.Runtime<never>,   // ← accept runtime as param
) =>
    (app: Elysia) => {
        const plugin = new Elysia({ name: "status" });

        plugin.group(options.root, (group) => {
            // ... all the route handlers, using runtime directly
        });

        logger.info(`Status endpoints: /${options.root}/health, /${options.root}/info`);
        return app.use(plugin);
    };

Copy link
Copy Markdown
Member

@mariocao mariocao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I was told, you can forget about my comment regarding the Effect.gen as this is more a PR for a fix. We can improve that in another PR later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants