Skip to content

Conversation

@Oksamies
Copy link
Contributor

No description provided.

Copy link
Contributor Author

Oksamies commented Nov 19, 2025

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR introduces a comprehensive error-handling infrastructure across the Cyberstorm Remix and Thunderstore API packages. It adds a new NimbusErrorBoundary component with styling for displaying errors, utilities for normalizing errors from multiple sources into a standardized UserFacingErrorPayload shape, and a configurable loader error handler with predefined error mappings for common HTTP statuses. The changes include support for routing errors, API errors, and async error contexts, alongside helper utilities for payload parsing and validation. A utility for sanitizing server detail strings and a search parameter parser are also added. Documentation outlines the unified error-handling flow for server loaders, client loaders, and error boundaries.

Possibly related PRs

Based on the provided data, no strongly related PRs were identified with direct code-level connections to this error-handling implementation.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the purpose, changes, and impact of the pull request for better context.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing Nimbus error handling components and utilities for consistent user-facing error messages.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (10)
apps/cyberstorm-remix/cyberstorm/utils/searchParamsUtils.ts (1)

8-21: Solid, defensive parsing; consider clarifying signed-int behavior and return type

The implementation is robust and conservative: trims input, rejects mixed strings ("10px"), guards against NaN and unsafe integers, and normalizes invalid inputs to undefined. No correctness issues from what I can see.

Two small, optional follow-ups to consider:

  • If search params are expected to support negative or +-prefixed values (e.g. "‑1", "+5"), the /^\d+$/ check will currently reject them. Either broaden the pattern (e.g. /^[+-]?\d+$/) or document that only non‑negative integers are valid.
  • For an exported utility that’s part of shared infra, adding an explicit return type can make the contract clearer:
    export function parseIntegerSearchParam(value: string | null): number | undefined { ... }

Please double-check existing and planned callers to confirm whether signed integers should be accepted or if non‑negative-only is the intended contract.

packages/thunderstore-api/src/errors/sanitizeServerDetail.ts (1)

1-40: Sanitizer behavior is solid; no changes required

The combination of control-character stripping, whitespace collapsing, and 400‑char truncation with an ellipsis gives you a safe, bounded string for UI use. The early returns on falsy/empty values keep things defensive. If you ever need different limits per call-site, you could consider making MAX_LENGTH configurable, but that’s strictly optional.

apps/cyberstorm-remix/cyberstorm/utils/errors/resolveRouteErrorPayload.ts (1)

1-72: Route error normalization is coherent; consider hiding raw Error messages

The overall flow—RouteErrorResponse → UserFacingErrorPayload parsing → ApiError → generic payload—is clear and matches the documented strategy.

One behavioral nit: for generic Error instances you currently surface error.message directly as the headline. That can leak fairly technical messages (“Invariant failed…”) to end users, which may not match the “consistent, user‑friendly” copy you’re aiming for. You might prefer a generic headline for these cases and keep the original message only in logging or context.

For example (optional):

-  if (error instanceof Error) {
-    return {
-      headline: error.message || "Something went wrong.",
-      description: undefined,
-      category: "server",
-      status: 500,
-    };
-  }
+  if (error instanceof Error) {
+    return {
+      headline: "Something went wrong.",
+      description: undefined,
+      category: "server",
+      status: 500,
+    };
+  }

Everything else in this helper looks good to me.

docs/error-handling.md (1)

1-198: Docs align with implementation; consider making snippets copy‑pasteable

The narrative matches the behavior in handleLoaderError, userFacingErrorResponse, and resolveRouteErrorPayload well. One minor nit: in the first loader example you call throwUserFacingPayloadResponse but don’t show its import, which makes the snippet slightly less copy‑pasteable for newer contributors. Adding that import (and similar ones elsewhere) would make this doc even more self‑contained.

apps/cyberstorm-remix/cyberstorm/utils/errors/handleLoaderError.ts (1)

1-88: Loader error handling logic is solid; double‑check findLast support

The mapping flow (default mappings + route‑specific overrides, last‑match wins) and payload construction look good and match the docs. Rethrowing Response unchanged and delegating unmapped cases to throwUserFacingErrorResponse gives a clear, predictable path.

One thing to verify: Array.prototype.findLast is still relatively new. If your Remix server or bundler targets environments that don’t support it out of the box, this could break at runtime. If there’s any doubt, consider a more broadly compatible pattern:

-    const mapping = allOptions.findLast((candidate) => {
-      const statuses = Array.isArray(candidate.status)
-        ? candidate.status
-        : [candidate.status];
-      return statuses.includes(error.statusCode);
-    });
+    const mapping = [...allOptions].reverse().find((candidate) => {
+      const statuses = Array.isArray(candidate.status)
+        ? candidate.status
+        : [candidate.status];
+      return statuses.includes(error.statusCode);
+    });

Otherwise, the structure and typing of LoaderErrorMapping / HandleLoaderErrorOptions look good.

apps/cyberstorm-remix/cyberstorm/utils/errors/loaderMappings.ts (1)

24-37: Confirm whether CONFLICT and RATE_LIMIT should be in defaultErrorMappings

CONFLICT_MAPPING (409) and RATE_LIMIT_MAPPING (429) are defined but not included in defaultErrorMappings. If callers rely on defaultErrorMappings as a complete baseline, consider adding them or documenting that they’re opt‑in only to avoid silent omission of these common cases.

Also applies to: 71-81

apps/cyberstorm-remix/cyberstorm/utils/errors/userFacingErrorResponse.ts (1)

72-107: Consider tightening the runtime payload validation

isUserFacingErrorPayload only checks that headline and category are strings. If you expect to rely on payloads coming from untrusted sources, consider also validating that:

  • category is one of the known UserFacingErrorCategory values, and
  • status (when present) is a number, and
  • context (when present) is a plain object.

This would make parseUserFacingErrorPayload more defensive without changing the external API.

packages/thunderstore-api/src/errors/userFacingError.ts (1)

199-216: Align 409/conflict categorization with loader mappings

categorizeStatus doesn’t treat 409 specially and will currently return "unknown", whereas CONFLICT_MAPPING in loaderMappings.ts uses status 409 with category "server". For consistency between API error categorization and loader mappings, consider adding a 409 branch here (likely mapping to "server" or a dedicated "conflict" category if you introduce one).

apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/NimbusErrorBoundary.tsx (2)

130-177: Minor UX/accessibility nits for the fallback surface

The fallback is functionally sound (safe payload resolution, sane default copy, robust retry behavior), but you might consider:

  • Using a heading element (<h1>/<h2>) instead of a bare <p> for title to improve semantics.
  • Optionally exposing a role="alert" or similar on the container when appropriate.

These are small tweaks that can be deferred but would improve accessibility.


183-215: Use the safe resolver in NimbusDefaultRouteErrorBoundary and generalize the JSDoc

NimbusDefaultRouteErrorBoundary calls resolveRouteErrorPayload directly, whereas NimbusErrorBoundaryFallback wraps it in safeResolveRouteErrorPayload to avoid mapper failures crashing the UI. For consistency and extra robustness, consider using the safe wrapper here as well, and update the JSDoc to reflect that this component is generic (the comment still references the wiki route).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7488263 and 2d3f2a3.

📒 Files selected for processing (11)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/NimbusErrorBoundary.css (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/NimbusErrorBoundary.tsx (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/index.ts (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/handleLoaderError.ts (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/loaderMappings.ts (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/resolveRouteErrorPayload.ts (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/errors/userFacingErrorResponse.ts (1 hunks)
  • apps/cyberstorm-remix/cyberstorm/utils/searchParamsUtils.ts (1 hunks)
  • docs/error-handling.md (1 hunks)
  • packages/thunderstore-api/src/errors/sanitizeServerDetail.ts (1 hunks)
  • packages/thunderstore-api/src/errors/userFacingError.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
packages/thunderstore-api/src/errors/sanitizeServerDetail.ts (1)
.yarn/releases/yarn-1.19.0.cjs (1)
  • cleaned (42026-42046)
apps/cyberstorm-remix/cyberstorm/utils/errors/handleLoaderError.ts (4)
packages/thunderstore-api/src/errors/userFacingError.ts (2)
  • UserFacingErrorCategory (9-16)
  • mapApiErrorToUserFacingError (92-159)
apps/cyberstorm-remix/cyberstorm/utils/errors/userFacingErrorResponse.ts (4)
  • CreateUserFacingErrorResponseOptions (29-33)
  • UserFacingErrorPayload (11-17)
  • throwUserFacingPayloadResponse (55-67)
  • throwUserFacingErrorResponse (38-50)
apps/cyberstorm-remix/cyberstorm/utils/errors/loaderMappings.ts (1)
  • defaultErrorMappings (71-81)
packages/thunderstore-api/src/errors.ts (1)
  • ApiError (14-64)
apps/cyberstorm-remix/cyberstorm/utils/errors/resolveRouteErrorPayload.ts (3)
packages/thunderstore-api/src/errors/userFacingError.ts (2)
  • UserFacingError (44-62)
  • mapApiErrorToUserFacingError (92-159)
apps/cyberstorm-remix/cyberstorm/utils/errors/userFacingErrorResponse.ts (2)
  • UserFacingErrorPayload (11-17)
  • parseUserFacingErrorPayload (72-91)
packages/thunderstore-api/src/errors.ts (1)
  • ApiError (14-64)
packages/thunderstore-api/src/errors/userFacingError.ts (2)
packages/thunderstore-api/src/errors.ts (4)
  • ApiError (14-64)
  • RequestBodyParseError (66-70)
  • RequestQueryParamsParseError (72-76)
  • ParseError (78-82)
packages/thunderstore-api/src/errors/sanitizeServerDetail.ts (1)
  • sanitizeServerDetail (29-40)
apps/cyberstorm-remix/cyberstorm/utils/errors/userFacingErrorResponse.ts (1)
packages/thunderstore-api/src/errors/userFacingError.ts (4)
  • UserFacingErrorCategory (9-16)
  • MapUserFacingErrorOptions (18-22)
  • mapApiErrorToUserFacingError (92-159)
  • UserFacingError (44-62)
apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/NimbusErrorBoundary.tsx (2)
packages/cyberstorm/src/utils/utils.ts (1)
  • classnames (34-38)
apps/cyberstorm-remix/cyberstorm/utils/errors/resolveRouteErrorPayload.ts (1)
  • resolveRouteErrorPayload (31-72)
apps/cyberstorm-remix/cyberstorm/utils/errors/loaderMappings.ts (1)
apps/cyberstorm-remix/cyberstorm/utils/errors/handleLoaderError.ts (1)
  • LoaderErrorMapping (17-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Generate visual diffs
🔇 Additional comments (7)
apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/index.ts (1)

1-1: Barrel export is straightforward and appropriate

Re-exporting from "./NimbusErrorBoundary" keeps consumers on a single, stable path; no changes needed here.

apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/NimbusErrorBoundary.css (1)

1-45: Nimbus error boundary styling looks consistent and self-contained

Use of @layer nimbus-components, design tokens, and hover state is clean and should integrate well with the existing design system; no functional or maintainability concerns from this snippet.

apps/cyberstorm-remix/cyberstorm/utils/errors/loaderMappings.ts (1)

3-37: Loader mappings are clear and consistent

Statuses and categories for the predefined mappings look sensible and align with expected user-facing copy; factories keep server and not-found cases reusable without over-configuring options.

apps/cyberstorm-remix/cyberstorm/utils/errors/userFacingErrorResponse.ts (1)

38-67: Centralized Response throwing looks solid

throwUserFacingErrorResponse and throwUserFacingPayloadResponse cleanly centralize mapping to UserFacingError plus Response construction with sane defaults (status override, JSON body, no-store caching). This should keep loader error handling consistent across call sites.

packages/thunderstore-api/src/errors/userFacingError.ts (2)

92-159: Verify ApiError exposes the fields used here

mapApiErrorToUserFacingError relies on ApiError having statusCode, statusText, serverMessage, and context?.sessionWasUsed. The ApiError snippet in errors.ts only shows response and responseJson, so just confirm that the additional fields/getters exist (or are added in this PR) so this code type‑checks and doesn’t fall back to "unknown" unexpectedly.


164-188: Not-found helper is well-structured

createResourceNotFoundError nicely sanitizes labels/identifiers, constructs clear copy, and enriches context with resource and optional identifier. This should make 404s much easier to handle consistently at the UI layer.

apps/cyberstorm-remix/cyberstorm/utils/errors/NimbusErrorBoundary/NimbusErrorBoundary.tsx (1)

77-123: Error boundary core implementation looks good

The class-based NimbusErrorBoundary follows the standard React error boundary pattern (getDerivedStateFromError, componentDidCatch, local error state) and cleanly wires in onError, onReset, and a pluggable fallback component. Props surface and reset behavior look straightforward to use.

@Oksamies Oksamies marked this pull request as ready for review November 27, 2025 11:55
};

export const VALIDATION_MAPPING: LoaderErrorMapping = {
status: 422,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think our backend is currently more likely to return 400 Bad Request than this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Most likely yes, but that's alright. For specific cases there can always be additional/overriding mappings added

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's alright. Go check the backend code base. Plenty of endpoints return 400. AFAICT none return 422. So 422 is the specific case that can be taken care when needed. 400 is the common case that shouldn't require boilerplate on UI components. Or am I misunderstanding something here?

* Creates a reusable server-error mapping with configurable copy and status.
*/
export function createServerErrorMapping(
headline = "Something went wrong.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this and the 404 counterpart below use the text's used in figma design?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AFAIK those texts are more placeholders, let's keep this like this for now

Copy link
Contributor

Choose a reason for hiding this comment

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

Figma designs have now been updated and the alternative error messages have been cleared out. Let's use the texts there. If you want to implement the glitch effect shown on Figma at the same go, these can be done separately, but in that case create a task for them.


Client loaders stream data to the browser but should produce the same payloads and tone as their server counterparts. Treat them exactly like server loaders:

1. Validate required params (`namespaceId`, `teamName`, etc.) and throw `throwUserFacingPayloadResponse` immediately when missing.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can these params ever be missing in the clientLoader (or loader for that matter)? If the parameter is missing from the URL does the request even match the route definition? If not, the clientLoader is never called but RR shows 404 via some magic? (Not sure about this but I think it seemed this way when I tested things.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No but the check needs to be in place for typing. And one can add additional checks e.g. if team name includes bad words.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I would then perhaps prefer params.namespaceId! etc. I usually don't like using non-null assertion, but in this case it would reduce boilerplate and make the code more readable. Maybe we should make this a squad decision?

@@ -0,0 +1,198 @@
# Error Handling
Copy link
Contributor

Choose a reason for hiding this comment

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

@Roffenlund would you mind review just this file without checking any of the related code. I want to know if it given you a clear picture how you should write loaders and components without checking the implementation.

@Oksamies Oksamies changed the base branch from master to graphite-base/1613 December 2, 2025 13:07
@Oksamies Oksamies changed the base branch from graphite-base/1613 to master December 2, 2025 13:07
@Oksamies Oksamies changed the base branch from master to graphite-base/1613 December 2, 2025 13:08
@Oksamies Oksamies force-pushed the 11-19-implement_nimbus_error_handling_components_and_utilities_for_consistent_user-facing_error_messages branch from 2d3f2a3 to e63509b Compare December 2, 2025 13:08
@Oksamies Oksamies changed the base branch from graphite-base/1613 to 12-01-chore_migrate_build_system_to_vite_and_update_ts_config December 2, 2025 13:08
@Oksamies Oksamies changed the base branch from 12-01-chore_migrate_build_system_to_vite_and_update_ts_config to graphite-base/1613 December 3, 2025 01:44
@Oksamies Oksamies force-pushed the 11-19-implement_nimbus_error_handling_components_and_utilities_for_consistent_user-facing_error_messages branch from e63509b to e20d99e Compare December 3, 2025 01:44
@Oksamies Oksamies changed the base branch from graphite-base/1613 to 12-01-chore_migrate_build_system_to_vite_and_update_ts_config December 3, 2025 01:44
);

if (error instanceof ApiError && allOptions.length) {
const mapping = allOptions.findLast((candidate) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that status is just number and not an array, I think this cleans up to:

    const mapping = allOptions.findLast(
      (mapping) => mapping.status === error.response.status
    );

Comment on lines +70 to +72
2. Call `getLoaderTools()` so you reuse the configured `DapperTs` instance *and* hydrated session tools. The helper now returns `{ dapper, sessionTools }` for client and server paths.
3. Wrap awaited work in `try/catch` and delegate to `handleLoaderError`. For Promises you hand off to Suspense, attach `.catch(error => handleLoaderError(error, { mappings }))` so rejections surface Nimbus copy.
4. Share error-mapping constants between server and client loaders to prevent drift.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would something like this be clearer:

2. Use `const { dapper, sessionTools } = getLoaderTools()` to reuse preconfigured `DapperTs` instance and hydrated session tools.

No need to mention "server paths" in the clientLoader section of the doc. And if sessionTools is worth mentioning here, it should be used in a code sample too (one code sample or a separate one where sessionTools is used).


3. If you need to await promise to resolve in the loader, wrap the promise in `try/catch` and use `handleLoaderError` in the `catch` block to show consistent error messages.
4. If there's no need to await in the loader, return an unresolved Promise to be used by Suspense. Attach `.catch(error => handleLoaderError(error, { mappings }))` to the promise to show consistent error messages.

A bit more verbose but makes the points clearer I think?


5. Share error-mapping constants between server and client loaders to show consistent error messages.

I still think Daniel should review the readme with fresh eyes.

### Example pattern

```tsx
const teamNotFoundMappings = [
Copy link
Contributor

Choose a reason for hiding this comment

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

This example seems to have nothing to do with Teams?

};
}

throwUserFacingPayloadResponse({
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this reuse (fixed) teamNotFoundMapping?

});

it("trims before truncating when slice ends in space", () => {
// "a" * 399 + " " + "b"
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: IMO unnecessary comments.

const category = categorizeStatus(error.response.status);
const headline = sanitizeServerDetail(error.message) ?? fallbackHeadline;
const sanitizedServerDetail = sanitizeServerDetail(error.message ?? "");
const sanitizedFallback = sanitizeServerDetail(fallbackDescription);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we sanitize the fallback, isn't it something we control to begin with?

const headline = sanitizeServerDetail(error.message) ?? fallbackHeadline;
const sanitizedServerDetail = sanitizeServerDetail(error.message ?? "");
const sanitizedFallback = sanitizeServerDetail(fallbackDescription);
const descriptionCandidate = sanitizedServerDetail || sanitizedFallback;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nothing "candidate" about this, this will be straight up the description.

});
}

return new UserFacingError({
Copy link
Contributor

Choose a reason for hiding this comment

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

Déjà vu but this could probably be combined with the isNetworkError(error) branch above.

Comment on lines +208 to +211
typeof error === "object" &&
error !== null &&
"message" in error &&
typeof (error as { message: unknown }).message === "string"
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to my comment in the unit tests for this function: is the four-line check really needed? Why don't we just check error instanceof Error here?

Comment on lines 56 to +57
export * from "./errors";
export * from "./errors/index";
Copy link
Contributor

Choose a reason for hiding this comment

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

Something to consider adding to your cleanup task list: maybe move the ApiError from errors.ts to error/errors.ts oslt?

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