Skip to content

RE1-T117 Trying to fix timeouts#415

Merged
ucswift merged 3 commits into
masterfrom
develop
Jun 23, 2026
Merged

RE1-T117 Trying to fix timeouts#415
ucswift merged 3 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes
    • Improved billing-related error handling so billing lookup failures return safe results instead of causing unhandled errors.
    • Subscription cancel and add-on purchase flows now stop or redirect gracefully when required billing details (plan/add-on/payment) can’t be found.
    • Stripe checkout now returns a clear server error when the selected plan or session isn’t available.
  • Performance
    • Standardized billing API request timeouts to 5 seconds for more consistent, faster failure detection.

@request-info

request-info Bot commented Jun 23, 2026

Copy link
Copy Markdown

Thanks for opening this, but we'd appreciate a little more information. Could you update it with more details?

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

SubscriptionsService introduces a shared BillingApiTimeoutMs = 5000 constant and applies it across all Billing API call sites to replace previously hardcoded MaxTimeout values. Eight methods now wrap Billing API calls in try/catch blocks that log exceptions and return safe defaults (null or new instances) instead of propagating errors. GetAdjustedUpgradePriceAsync explicitly validates that required billing data is not null, throwing InvalidOperationException if missing. SubscriptionController adds defensive null checks in Cancel, BuyAddon, ManagePTTAddon, and GetStripeSession actions before dereferencing billing objects, redirecting or returning error responses instead of crashing on null dereferences.

Changes

Billing API Timeout and Error Handling

Layer / File(s) Summary
BillingApiTimeoutMs constant and exception handling in SubscriptionsService
Core/Resgrid.Services/SubscriptionsService.cs
Adds private const int BillingApiTimeoutMs = 5000 and applies it to replace hardcoded timeouts across all Billing API methods (GetCurrentPlanForDepartmentAsync, GetPlanCountsForDepartmentAsync, GetCurrentPaymentForDepartmentAsync, GetPreviousNonFreePaymentForDepartmentAsync, GetUpcomingPaymentForDepartmentAsync, GetPaymentByTransactionIdAsync, GetPlanByIdAsync, GetPlanByExternalIdAsync, GetPaymentByIdAsync). Wraps each Billing API call in try/catch that logs exceptions via Framework.Logging.LogException and returns null or new DepartmentPlanCount() instead of propagating failures. GetPlanCountsForDepartmentAsync now returns null when response data is missing. GetAdjustedUpgradePriceAsync adds explicit fail-fast guards that throw InvalidOperationException when required payment or plan data is null.
Null guards in SubscriptionController action methods
Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs
Adds defensive null checks in Cancel (GET/POST) to redirect to CancelFailure when current payment lookup returns null, in BuyAddon (GET) to return NotFound() when add-on is null and to guard plan access before assignment, in ManagePTTAddon (GET) to return HTTP 500 when add-on lookup is null, and in GetStripeSession (GET) to return HTTP 500 with messages when plan or Stripe session is null.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Resgrid/Core#324: Both modify SubscriptionController.cs with defensive validation in checkout flow (GetStripeSession); this PR adds null checks for missing billing data while the retrieved PR validates range constraints.
  • Resgrid/Core#411: Both add defensive null-handling for missing billing/payment data; this PR guards Cancel/BuyAddon/GetStripeSession in SubscriptionController.cs while PR #411 updates LimitsService to treat null plan results as hard-deny.

Suggested reviewers

  • github-actions
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions fixing timeouts, which is a real part of the changeset (timeout constants and REST client configuration), but the changes also include significant error handling improvements and null-guard additions that are equally important to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Services/SubscriptionsService.cs`:
- Around line 101-128: The catch block in the method that fetches plan counts
currently returns new DepartmentPlanCount() on exception, which signals zero
usage to downstream code instead of indicating that billing data is unavailable.
To maintain fail-closed semantics and align with the signal that
LimitsService.ValidateDepartmentIsWithinLimitsAsync expects (null indicates
billing unavailable), change the catch block to return null instead of new
DepartmentPlanCount(). This ensures that billing outages trigger denial of
service rather than allowing operations with assumed zero usage.
- Around line 319-346: The catch block in the GetPlanByIdAsync method is
returning null when an exception occurs, which masks errors (like timeouts from
the Billing API) and makes them indistinguishable from a legitimate not-found
response. This causes callers like SubscriptionController to dereference the
plan object without null checks, resulting in crashes. Instead of catching the
exception and returning null, either rethrow the exception after logging it or
throw a more meaningful custom exception so that calling code can properly
handle actual errors versus genuinely missing plans.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b9aa7bcd-3cd8-47d4-ada7-cb40bed12947

📥 Commits

Reviewing files that changed from the base of the PR and between 27a0c4f and 97e824e.

📒 Files selected for processing (1)
  • Core/Resgrid.Services/SubscriptionsService.cs

Comment thread Core/Resgrid.Services/SubscriptionsService.cs
Comment on lines +319 to +346
try
{
MaxTimeout = 200000 // ms
};
var options = new RestClientOptions(Config.SystemBehaviorConfig.BillingApiBaseUrl)
{
MaxTimeout = BillingApiTimeoutMs // ms
};

var client = new RestClient(options, configureSerialization: s => s.UseNewtonsoftJson());
var request = new RestRequest($"/api/Billing/GetPlanById", Method.Get);
request.AddHeader("X-API-Key", Config.ApiConfig.BackendInternalApikey);
request.AddHeader("Content-Type", "application/json");
request.AddParameter("planId", planId, ParameterType.QueryString);
var client = new RestClient(options, configureSerialization: s => s.UseNewtonsoftJson());
var request = new RestRequest($"/api/Billing/GetPlanById", Method.Get);
request.AddHeader("X-API-Key", Config.ApiConfig.BackendInternalApikey);
request.AddHeader("Content-Type", "application/json");
request.AddParameter("planId", planId, ParameterType.QueryString);

var response = await client.ExecuteAsync<GetCurrentPlanForDepartmentResult>(request);
var response = await client.ExecuteAsync<GetCurrentPlanForDepartmentResult>(request);

if (response.StatusCode == HttpStatusCode.NotFound)
return null;
if (response.StatusCode == HttpStatusCode.NotFound)
return null;

if (response.Data == null)
return null;
if (response.Data == null)
return null;

return response.Data.Data;
return response.Data.Data;
}
catch (Exception ex)
{
Framework.Logging.LogException(ex);
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid null fallback in GetPlanByIdAsync on Billing exceptions.

Line 345 returns null after catch; a known caller (SubscriptionController, Line 595-599 in provided context) dereferences plan.Frequency without a null guard, which can crash requests when Billing times out/errors.

Suggested fix
 				catch (Exception ex)
 				{
 					Framework.Logging.LogException(ex);
-					return null;
+					return await _plansRepository.GetPlanByPlanIdAsync(planId);
 				}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/SubscriptionsService.cs` around lines 319 - 346, The
catch block in the GetPlanByIdAsync method is returning null when an exception
occurs, which masks errors (like timeouts from the Billing API) and makes them
indistinguishable from a legitimate not-found response. This causes callers like
SubscriptionController to dereference the plan object without null checks,
resulting in crashes. Instead of catching the exception and returning null,
either rethrow the exception after logging it or throw a more meaningful custom
exception so that calling code can properly handle actual errors versus
genuinely missing plans.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Core/Resgrid.Services/SubscriptionsService.cs (2)

68-90: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t convert Billing timeouts into a free-plan response.

With the new 5s timeout, an empty/timed-out Billing response still hits lines 82-90 and returns freePlan, which masks an outage as a free-tier downgrade. Preserve NotFound => freePlan, but return null for unusable payloads/exceptions so callers can fail safely.

Proposed fix
 					if (response.StatusCode == HttpStatusCode.NotFound)
 						return freePlan;
 
 					if (response.Data == null || response.Data.Data == null)
-						return freePlan;
+						return null;
 
 					return response.Data.Data;
 				}
 				catch (Exception ex)
 				{
 					Framework.Logging.LogException(ex);
-					return freePlan;
+					return null;
 				}

As per coding guidelines, “When SubscriptionsService.GetCurrentPlanForDepartmentAsync() receives a null response from the Billing API, it returns null instead of a fallback free plan.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/SubscriptionsService.cs` around lines 68 - 90, The
GetCurrentPlanForDepartmentAsync method currently returns freePlan whenever
response.Data is null (lines 82-84), which masks timeout and outage scenarios as
legitimate free-tier responses. Modify the null check for response.Data and
response.Data.Data to return null instead of freePlan, while keeping the
NotFound status code check intact so that only actual 404 responses return
freePlan. This distinction allows callers to properly handle failures versus
legitimate free plan scenarios.

Source: Coding guidelines


30-33: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Apply the shared timeout to every Billing API client.

This constant says every Billing call should use the short timeout, but many call sites below still create RestClient directly without RestClientOptions (for example lines 536, 727, 812, 1069, 1182, and 1248). Those paths can still hang for the old default duration.

Proposed direction
 		// Billing API calls must fail fast. A slow/unresponsive Billing API previously blocked the
 		// request thread for up to 200s (e.g. the Subscription and User Dashboard pages would hang
 		// for ~3 minutes). Keep every Billing call on this short, shared timeout.
 		private const int BillingApiTimeoutMs = 5000;
+
+		private static RestClient CreateBillingClient()
+		{
+			var options = new RestClientOptions(Config.SystemBehaviorConfig.BillingApiBaseUrl)
+			{
+				MaxTimeout = BillingApiTimeoutMs
+			};
+
+			return new RestClient(options, configureSerialization: s => s.UseNewtonsoftJson());
+		}

Then replace each new RestClient(Config.SystemBehaviorConfig.BillingApiBaseUrl, ...) Billing call with CreateBillingClient().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/SubscriptionsService.cs` around lines 30 - 33, The
BillingApiTimeoutMs constant is defined but not consistently applied across all
Billing API calls. Create a helper method CreateBillingClient() that
instantiates a RestClient with the BillingApiTimeoutMs timeout configured via
RestClientOptions, then replace all direct instantiations of new
RestClient(Config.SystemBehaviorConfig.BillingApiBaseUrl, ...) throughout the
file with calls to this new CreateBillingClient() method to ensure the short
timeout is applied uniformly across all Billing API operations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Services/SubscriptionsService.cs`:
- Around line 623-629: The current validation throws an error when payment.Plan
is null without attempting to load it first. Before throwing the
InvalidOperationException that checks if payment?.Plan == null or plan == null,
add logic to hydrate payment.Plan using payment.PlanId when payment exists but
payment.Plan is null. Use the same approach already employed elsewhere in the
file to fetch the plan by ID, then re-evaluate the null conditions only after
attempting to load the missing Plan navigation property, so that valid payment
records with missing navigations can still be processed rather than failing
immediately.

In `@Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs`:
- Around line 608-611: The GetPlanAddonByIdAsync method call can return null,
but model.PlanAddon is being dereferenced without a guard check before the
existing plan null guard. Add a null guard immediately after the
GetPlanAddonByIdAsync call to check if the returned add-on is null, and only
proceed with dereferencing model.PlanAddon when it is not null. This guard
should be placed before the existing plan null check that protects
plan.Frequency dereferencing.
- Around line 769-773: Add a null guard for the `session` object returned from
the `CreateStripeSessionForSub` method call, similar to the existing guard for
`plan`. After obtaining the session, check if it is null and return a StatusCode
500 response with an appropriate error message before the code attempts to
dereference `session.CustomerId` and `session.SessionId`. This will prevent a
NullReferenceException when the Stripe session creation fails or returns null.

---

Outside diff comments:
In `@Core/Resgrid.Services/SubscriptionsService.cs`:
- Around line 68-90: The GetCurrentPlanForDepartmentAsync method currently
returns freePlan whenever response.Data is null (lines 82-84), which masks
timeout and outage scenarios as legitimate free-tier responses. Modify the null
check for response.Data and response.Data.Data to return null instead of
freePlan, while keeping the NotFound status code check intact so that only
actual 404 responses return freePlan. This distinction allows callers to
properly handle failures versus legitimate free plan scenarios.
- Around line 30-33: The BillingApiTimeoutMs constant is defined but not
consistently applied across all Billing API calls. Create a helper method
CreateBillingClient() that instantiates a RestClient with the
BillingApiTimeoutMs timeout configured via RestClientOptions, then replace all
direct instantiations of new
RestClient(Config.SystemBehaviorConfig.BillingApiBaseUrl, ...) throughout the
file with calls to this new CreateBillingClient() method to ensure the short
timeout is applied uniformly across all Billing API operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1c7cacea-f072-45d0-93a6-49fc9eb6f129

📥 Commits

Reviewing files that changed from the base of the PR and between 97e824e and 8c7da0e.

📒 Files selected for processing (2)
  • Core/Resgrid.Services/SubscriptionsService.cs
  • Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs

Comment thread Core/Resgrid.Services/SubscriptionsService.cs
Comment thread Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs
Comment thread Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs (1)

789-793: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Consider validating session.SessionId to fully address the earlier comment.

While the session null guard prevents NRE, the session object might still have a null or empty SessionId field, which would break the client checkout flow when returned at line 802. The past review comment suggested checking string.IsNullOrWhiteSpace(session.SessionId) in addition to the null guard.

Suggested enhancement for session validation
 var session = await _subscriptionsService.CreateStripeSessionForSub(DepartmentId, stripeCustomerId, plan.GetExternalKey(), plan.PlanId, user.Email, department.Name, count, discountCode);
-// CreateStripeSessionForSub returns null when the Billing API is unavailable / Stripe session
-// creation fails. Fail with a clear error instead of NRE'ing on session.CustomerId/SessionId below.
-if (session == null)
-	return StatusCode(StatusCodes.Status500InternalServerError, "Unable to start the checkout session. Please try again.");
+// CreateStripeSessionForSub returns null when the Billing API is unavailable / Stripe session
+// creation fails. Validate both session and SessionId before returning to client.
+if (session == null || string.IsNullOrWhiteSpace(session.SessionId))
+	return StatusCode(StatusCodes.Status500InternalServerError, "Unable to start the checkout session. Please try again.");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs` around
lines 789 - 793, The session null check in the SubscriptionController prevents
NRE but does not validate that the session object has a valid SessionId value.
Add an additional check using string.IsNullOrWhiteSpace() to validate that
session.SessionId is not null or empty in the same if condition that checks for
null session. This ensures the session object both exists and contains the
necessary SessionId before it is used later in the response, preventing the
client checkout flow from breaking due to a missing session identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs`:
- Around line 789-793: The session null check in the SubscriptionController
prevents NRE but does not validate that the session object has a valid SessionId
value. Add an additional check using string.IsNullOrWhiteSpace() to validate
that session.SessionId is not null or empty in the same if condition that checks
for null session. This ensures the session object both exists and contains the
necessary SessionId before it is used later in the response, preventing the
client checkout flow from breaking due to a missing session identifier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6634b22d-5c08-49a8-90c2-30d90e49f2a6

📥 Commits

Reviewing files that changed from the base of the PR and between 8c7da0e and 5d85535.

📒 Files selected for processing (2)
  • Core/Resgrid.Services/SubscriptionsService.cs
  • Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Core/Resgrid.Services/SubscriptionsService.cs

@ucswift

ucswift commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit 848f34a into master Jun 23, 2026
18 of 19 checks passed
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.

1 participant