Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/brave-koalas-catch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"skycrypt-frontend": patch
---

Stop the production container restart loop caused by unhandled promise rejections under Node 24. Two changes: (1) the `/stats/[ign]/[[profile]]/card` endpoint now uses `Promise.allSettled` rather than `Promise.all` for the parallel `getProfileStats` / `getNetworth` / `getCombined` calls — when one rejects first, the losing-side promises no longer become orphaned rejections that crash the process. (2) A `process.on("unhandledRejection")` safety net in `instrumentation.server.ts` logs + reports any future orphans to Sentry instead of exiting, since Node 24's default `--unhandled-rejections=throw` is fatal. Combined this ends the ~2-minute restart cycle visible in production logs.
10 changes: 10 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"skycrypt-frontend": "3.6.1"
},
"changesets": [
"brave-koalas-catch"
]
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 3.6.2-beta.0

### Patch Changes

- Stop the production container restart loop caused by unhandled promise rejections under Node 24. Two changes: (1) the `/stats/[ign]/[[profile]]/card` endpoint now uses `Promise.allSettled` rather than `Promise.all` for the parallel `getProfileStats` / `getNetworth` / `getCombined` calls — when one rejects first, the losing-side promises no longer become orphaned rejections that crash the process. (2) A `process.on("unhandledRejection")` safety net in `instrumentation.server.ts` logs + reports any future orphans to Sentry instead of exiting, since Node 24's default `--unhandled-rejections=throw` is fatal. Combined this ends the ~2-minute restart cycle visible in production logs. ([`9ffadbc`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/9ffadbcf6e4c5730756a06039dc23cf9a56eda98))

## 3.6.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skycrypt-frontend",
"version": "3.6.1",
"version": "3.6.2-beta.0",
"private": true,
"type": "module",
"repository": {
Expand Down
8 changes: 8 additions & 0 deletions src/instrumentation.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ Sentry.init({
enabled: !dev,
environment: dev ? "development" : "production"
});

// Node 24's default --unhandled-rejections=throw will kill the process on any
// orphaned promise rejection (e.g. losing-side promises in Promise.all when
// one rejects first). Log and report so we get visibility, but don't crash.
process.on("unhandledRejection", (reason) => {
console.error("Unhandled promise rejection:", reason);
Sentry.captureException(reason);
});
19 changes: 15 additions & 4 deletions src/routes/stats/[ign]/[[profile]]/card/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,29 @@ export const GET: RequestHandler = async ({ params, request, url }) => {

try {
const user = (await getApiUuidUsername(ign)).data as ModelsPlayerResolve;
// allSettled (not Promise.all) so a losing-side rejection is never orphaned:
// Promise.all rejects on the first failure but leaves the others running, and
// their later rejection becomes an unhandled rejection that crashes Node 24.
const [
// prettier-ignore
profileData,
networthData,
combinedData
] = await Promise.all([
profileResult,
networthResult,
combinedResult
] = await Promise.allSettled([
// prettier-ignore
getProfileStats({ uuid: user.uuid ?? ign, profileId: profile ?? "" }),
getNetworth({ uuid: user.uuid ?? ign, profileId: profile ?? "" }),
getCombined({ uuid: user.uuid ?? ign, profileId: profile ?? "" })
]);

if (profileResult.status === "rejected") throw profileResult.reason;
if (networthResult.status === "rejected") throw networthResult.reason;
if (combinedResult.status === "rejected") throw combinedResult.reason;

const profileData = profileResult.value;
const networthData = networthResult.value;
const combinedData = combinedResult.value;

const { body: renderedHTML } = render(DefaultCard, {
props: {
profile: profileData,
Expand Down