Skip to content

VPR-141 feat(healthchecks): add /health endpoints and UI dashboard#159

Open
rlorenzo wants to merge 6 commits intomainfrom
VPR-141-health-check
Open

VPR-141 feat(healthchecks): add /health endpoints and UI dashboard#159
rlorenzo wants to merge 6 commits intomainfrom
VPR-141-health-check

Conversation

@rlorenzo
Copy link
Copy Markdown
Contributor

@rlorenzo rlorenzo commented Apr 22, 2026

  • /health (anonymous liveness for Jenkins) + /health/detail (tagged "ready", IP-gated to SVM /20 and infra /24, not CAS-gated so it stays reachable when auth is degraded).
  • HealthChecks.UI dashboard at /healthchecks with UC Davis branding, duration humanizer, and a campus-status banner that appears when any campus-* check is non-healthy.
  • Adaptive polling decorator on campus checks (LDAP/CAS/SMTP/VMACs): healthy results cached 1 hour, failures re-probe every 5 min (one UI poll cycle). Cuts external traffic from 12/hour to 1/hour per instance while healthy.
  • Real LDAPS bind, MailKit SMTP connect, AWS SSM probe, disk checks for app/photos/CMS/logs, EF DbContext checks for all contexts.
  • Adopts DotNetDiag.HealthChecks.UI 10.0.7 (fork of abandoned Xabaril packages; upstream does not build on .NET 10). Pinned exactly.
  • Jenkins Deploy stages now poll /health post-deploy.

Summary by CodeRabbit

  • New Features

    • Added a visual HealthChecks dashboard with branding and client-side enhancements (human-readable durations, campus-status banner).
    • Added post-deployment health verification steps for test and prod that validate the app’s /2/health endpoint.
  • Improvements

    • Expanded runtime health probes (disk, SMTP, LDAP, AWS SSM, HTTP endpoints) for more comprehensive monitoring.
    • Improved proxy/client-IP handling and refined internal allowlist and CSP behavior for health UI.

Copilot AI review requested due to automatic review settings April 22, 2026 23:41
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 0% with 533 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.89%. Comparing base (5cb764f) to head (a83c9f1).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
web/Classes/HealthChecks/HealthCheckExtensions.cs 0.00% 206 Missing ⚠️
web/Classes/HealthChecks/DiskSpaceHealthCheck.cs 0.00% 86 Missing ⚠️
web/Classes/HealthChecks/SmtpHealthCheck.cs 0.00% 47 Missing ⚠️
...Classes/HealthChecks/AdaptivePollingHealthCheck.cs 0.00% 42 Missing ⚠️
web/Classes/CloudflareNetworks.cs 0.00% 39 Missing ⚠️
web/Classes/HealthChecks/LdapHealthCheck.cs 0.00% 33 Missing ⚠️
...eb/Classes/HealthChecks/HttpEndpointHealthCheck.cs 0.00% 28 Missing ⚠️
web/Classes/HealthChecks/AwsSsmHealthCheck.cs 0.00% 23 Missing ⚠️
web/Program.cs 0.00% 17 Missing ⚠️
...b/Classes/HealthChecks/HealthCheckCollectorAuth.cs 0.00% 7 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #159      +/-   ##
==========================================
- Coverage   43.24%   42.89%   -0.35%     
==========================================
  Files         862      873      +11     
  Lines       50405    51007     +602     
  Branches     4706     4759      +53     
==========================================
+ Hits        21796    21879      +83     
- Misses      28086    28605     +519     
  Partials      523      523              
Flag Coverage Δ
backend 42.95% <0.00%> (-0.37%) ⬇️
frontend 41.69% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Fixed
Comment thread web/Classes/HealthChecks/DiskSpaceHealthCheck.cs Fixed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds operational health checking to the web app: anonymous liveness, IP-gated readiness details, and an internal HealthChecks.UI dashboard with UC Davis branding and reduced external probe traffic via adaptive polling.

Changes:

  • Introduces /health and /health/detail endpoints plus the /healthchecks UI, including IP allowlisting and CSP bypass for the UI bundle.
  • Adds multiple new health checks (DB contexts, disk space, LDAP, SMTP, CAS/VMACs HTTP probes, AWS SSM) and an adaptive polling decorator to reduce probe frequency when healthy.
  • Updates Jenkins deploy stages to poll /health after deploy; adds UI branding assets and a small injected JS enhancer.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web/wwwroot/js/healthchecks-ui-extras.js Injected UI script to humanize duration cells and show a campus-status banner.
web/wwwroot/healthchecks-ui-logo.png New logo asset for HealthChecks.UI branding.
web/wwwroot/css/healthchecks-ui-branding.css UC Davis palette + UI CSS tweaks (contrast, layout, banner styling).
web/appsettings.json Expands InternalAllowlist to CIDR ranges for health detail/UI access.
web/Viper.csproj Adds DotNetDiag HealthChecks.UI packages and EF health checks package reference.
web/Program.cs Hooks health check DI + pipeline wiring; conditionally applies CSP outside UI paths.
web/Classes/HealthChecks/SmtpHealthCheck.cs MailKit-based SMTP reachability/TLS probe.
web/Classes/HealthChecks/LdapHealthCheck.cs Real LDAPS bind probe for directory health.
web/Classes/HealthChecks/HttpEndpointHealthCheck.cs Generic HTTP endpoint reachability probe.
web/Classes/HealthChecks/HealthCheckExtensions.cs Centralized health checks registration + endpoint/UI mapping + UI HTML injection.
web/Classes/HealthChecks/DiskSpaceHealthCheck.cs Drive space (and optional writability) checks for app/photos/CMS/log paths.
web/Classes/HealthChecks/AwsSsmHealthCheck.cs AWS SSM reachability probe using a lightweight DescribeParameters call.
web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs Caches health results with status-dependent TTLs to reduce probe load.
JenkinsFile Adds post-deploy /health polling for test and prod.

Comment thread web/wwwroot/js/healthchecks-ui-extras.js Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class health check endpoints and an operator-facing HealthChecks.UI dashboard to VIPER, including custom probes (DB, disk, LDAP/CAS/SMTP/SSM) and Jenkins post-deploy verification.

Changes:

  • Introduces /health liveness, /health/detail readiness JSON, and /healthchecks UI with IP allowlisting.
  • Adds multiple custom IHealthCheck implementations plus an adaptive polling decorator to reduce external traffic.
  • Updates Jenkins deploy stages to poll /health after deployment.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/wwwroot/js/healthchecks-ui-extras.js UI-side DOM tweaks (duration humanizer + campus-status banner).
web/wwwroot/healthchecks-ui-logo.png Adds UC Davis-branded logo asset for the dashboard.
web/wwwroot/css/healthchecks-ui-branding.css Custom palette/branding + minor UI layout/accessibility tweaks.
web/appsettings.json Expands InternalAllowlist to CIDR ranges for readiness/UI access.
web/Viper.csproj Adds HealthChecks.UI + EF Core health check package references.
web/Program.cs Hooks health check DI + pipeline wiring; skips CSP on UI paths.
web/Classes/HealthChecks/SmtpHealthCheck.cs Adds SMTP relay probe via MailKit connect/noop/disconnect.
web/Classes/HealthChecks/LdapHealthCheck.cs Adds LDAPS bind probe matching existing LDAP service settings.
web/Classes/HealthChecks/HttpEndpointHealthCheck.cs Adds generic HTTP reachability probe for CAS/VMACs.
web/Classes/HealthChecks/HealthCheckExtensions.cs Centralizes health check registration, endpoint mapping, UI config, and response-body script injection.
web/Classes/HealthChecks/DiskSpaceHealthCheck.cs Adds disk free-space (and optional writability) probe for key volumes.
web/Classes/HealthChecks/AwsSsmHealthCheck.cs Adds lightweight SSM reachability probe.
web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs Adds status-based caching to reduce expensive probe frequency.
JenkinsFile Adds post-deploy /health polling for test and prod stages.

Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/wwwroot/css/healthchecks-ui-branding.css Outdated
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 23, 2026

Bundle Report

Changes will increase total bundle size by 2.92kB (0.14%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.14MB 2.92kB (0.14%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/GenericError-*.css 434 bytes 203.39kB 0.21%
assets/TermManagement-*.js 449 bytes 53.58kB 0.85%
assets/schedule-*.js (New) 51.46kB 51.46kB 100.0% 🚀
assets/CourseDetail-*.js 193 bytes 40.63kB 0.48%
assets/PhotoGallery-*.js 75 bytes 35.62kB 0.21%
assets/InstructorEdit-*.js 245 bytes 32.23kB 0.77%
assets/InstructorList-*.js 31 bytes 26.09kB 0.12%
assets/StaffDashboard-*.js -265 bytes 25.6kB -1.02%
assets/CrossListedCoursesSection-*.js 111 bytes 23.7kB 0.47%
assets/GenericError-*.js -537 bytes 21.11kB -2.48%
assets/ClinicianScheduleView-*.js -1.38kB 18.9kB -6.81%
assets/MultiYearReport-*.js 74 bytes 18.64kB 0.4%
assets/EmergencyContactForm-*.js 17 bytes 17.57kB 0.1%
assets/AuditList-*.js 38 bytes 17.39kB 0.22%
assets/RotationScheduleView-*.js 120 bytes 17.01kB 0.71%
assets/CourseList-*.js 109 bytes 15.99kB 0.69%
assets/EffortTypeList-*.js 71 bytes 15.4kB 0.46%
assets/CourseImportDialog-*.js 43 bytes 10.88kB 0.4%
assets/CourseLinkDialog-*.js 225 bytes 9.31kB 2.48%
assets/ManageBundleCompetencies-*.js 85 bytes 8.9kB 0.96%
assets/StudentClassYear-*.js -1 bytes 8.44kB -0.01%
assets/EmergencyContactView-*.js 36 bytes 8.03kB 0.45%
assets/schedule-*.css (New) 7.07kB 7.07kB 100.0% 🚀
assets/ManageSessionCompetencies-*.js 96 bytes 6.86kB 1.42%
assets/ManageCompetencies-*.js 70 bytes 6.65kB 1.06%
assets/TermSelection-*.js -57 bytes 6.45kB -0.88%
assets/UnitList-*.js 57 bytes 6.34kB 0.91%
assets/CompetenciesBundleReport-*.js 122 bytes 6.1kB 2.04%
assets/AssessmentEpa-*.js 39 bytes 5.73kB 0.69%
assets/MyAssessments-*.js 75 bytes 5.67kB 1.34%
assets/EffortRecordEditDialog-*.js 77 bytes 5.42kB 1.44%
assets/SchoolSummary-*.js -114 bytes 4.32kB -2.57%
assets/ManageCourseCompetencies-*.js 52 bytes 3.7kB 1.43%
assets/LevelSelect-*.js -16 bytes 2.65kB -0.6%
assets/SchoolSummary-*.css -49 bytes 1.75kB -2.73%
assets/TermManagement-*.css 54 bytes 1.46kB 3.85%
assets/StaffDashboard-*.css -66 bytes 1.45kB -4.34%
assets/CompetenciesBundleReport-*.css 594 bytes 1.28kB 86.97% ⚠️
assets/colors-*.js (New) 891 bytes 891 bytes 100.0% 🚀
assets/StatusBadge-*.js (New) 458 bytes 458 bytes 100.0% 🚀
assets/format-*.js 4 bytes 161 bytes 2.55%
assets/RecentSelections-*.js (Deleted) -51.59kB 0 bytes -100.0% 🗑️
assets/RecentSelections-*.css (Deleted) -6.48kB 0 bytes -100.0% 🗑️

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class health checking to VIPER, including liveness/readiness endpoints for deploy automation and an IP-gated HealthChecks.UI dashboard tailored to campus ops needs.

Changes:

  • Introduces /health (anonymous liveness) and /health/detail (IP-gated readiness with tagged checks) plus HealthChecks.UI at /healthchecks.
  • Adds health check implementations (LDAP, SMTP, HTTP endpoint probes, disk space, AWS SSM) and an adaptive polling decorator to reduce external probe traffic.
  • Updates Jenkins deploy stages to poll /2/health post-deploy; adds UC Davis branding + UI tweaks (duration humanizer + campus-status banner).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web/wwwroot/js/healthchecks-ui-extras.js Injected UI tweaks (duration humanizer, campus-status banner) via MutationObserver.
web/wwwroot/css/healthchecks-ui-branding.css UC Davis palette + UI readability adjustments + campus-status banner styling.
web/appsettings.json Replaces a single internal allowlisted IP with CIDR ranges for staff + infra.
web/Viper.csproj Adds DotNetDiag HealthChecks.UI packages and EF Core health check package.
web/Program.cs Hooks health check DI + pipeline wiring; skips CSP on HealthChecks.UI paths.
web/Classes/HealthChecks/SmtpHealthCheck.cs MailKit-based SMTP reachability probe.
web/Classes/HealthChecks/LdapHealthCheck.cs Real LDAPS bind probe using existing LDAP service credentials.
web/Classes/HealthChecks/HttpEndpointHealthCheck.cs HTTP(S) reachability probe (treats non-5xx as healthy).
web/Classes/HealthChecks/HealthCheckExtensions.cs Centralizes health check registration, endpoint mapping, UI wiring, and IP gating.
web/Classes/HealthChecks/DiskSpaceHealthCheck.cs Disk free-space (and optional writability) probe for key volumes/paths.
web/Classes/HealthChecks/AwsSsmHealthCheck.cs AWS SSM reachability probe via DescribeParameters.
web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs Caches healthy vs unhealthy results for different durations to reduce probe load.
JenkinsFile Adds post-deploy polling of /2/health for TEST and PROD.

Comment thread web/Program.cs Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds operational health monitoring to VIPER by introducing anonymous liveness and IP-gated readiness endpoints, plus a branded HealthChecks.UI dashboard tailored for the campus environment (path base, reduced external probing, and operator-focused UI tweaks).

Changes:

  • Adds /health liveness and /health/detail readiness endpoints plus a HealthChecks.UI dashboard (IP-restricted, CSP-exempted for UI paths).
  • Implements custom health checks (LDAP bind, SMTP connect, HTTP probe, AWS SSM probe, disk space) and an adaptive polling decorator to reduce external traffic.
  • Updates Jenkins deploy stages to poll /health after deployment and updates allowlists to CIDR ranges.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/wwwroot/js/healthchecks-ui-extras.js Injected client-side UI enhancements (duration formatting + campus-status banner).
web/wwwroot/css/healthchecks-ui-branding.css HealthChecks.UI UC Davis branding + accessibility/UX tweaks.
web/appsettings.json Replaces single internal IP with CIDR-based allowlist ranges.
web/Viper.csproj Adds DotNetDiag HealthChecks.UI packages and EF Core health check package.
web/Program.cs Wires health checks via extensions and skips CSP for HealthChecks.UI paths.
web/Classes/HealthChecks/SmtpHealthCheck.cs New MailKit-based SMTP probe health check.
web/Classes/HealthChecks/LdapHealthCheck.cs New LDAPS bind health check (Windows-only).
web/Classes/HealthChecks/HttpEndpointHealthCheck.cs New HTTP reachability probe health check.
web/Classes/HealthChecks/HealthCheckExtensions.cs Central DI + pipeline wiring for health endpoints/UI, IP gating, and script injection.
web/Classes/HealthChecks/DiskSpaceHealthCheck.cs New disk space (and optional writability) health check.
web/Classes/HealthChecks/AwsSsmHealthCheck.cs New AWS SSM reachability health check.
web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs New decorator to cache healthy results longer than unhealthy ones.
JenkinsFile Adds post-deploy /health polling for test and prod stages.

Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Outdated
@rlorenzo rlorenzo force-pushed the VPR-141-health-check branch from 4752b79 to a465446 Compare April 27, 2026 17:17
- /health (anonymous liveness for Jenkins) + /health/detail (tagged
  "ready", IP-gated to SVM /20 and infra /24, not CAS-gated so it
  stays reachable when auth is degraded).
- HealthChecks.UI dashboard at /healthchecks with UC Davis branding,
  duration humanizer, and a campus-status banner that appears when
  any campus-* check is non-healthy.
- Adaptive polling decorator on campus checks (LDAP/CAS/SMTP/VMACs):
  healthy results cached 1 hour, failures re-probe every 5 min (one
  UI poll cycle). Cuts external traffic from 12/hour to 1/hour per
  instance while healthy.
- Real LDAPS bind, MailKit SMTP connect, AWS SSM probe, disk checks
  for app/photos/CMS/logs, EF DbContext checks for all contexts.
- Adopts DotNetDiag.HealthChecks.UI 10.0.7 (fork of abandoned Xabaril
  packages; upstream does not build on .NET 10). Pinned exactly.
- Jenkins Deploy stages now poll /health post-deploy.
@rlorenzo rlorenzo force-pushed the VPR-141-health-check branch from a465446 to 77f2c9f Compare April 27, 2026 20:23
- F5 forwards client IP via X-Forwarded-For, so traffic from
  192.168.56.0/24 hosts hitting the public URL gates on the
  egress IP, not the internal interface - the SVM infra entry
  never matched in practice.
- 169.237.251.0/24 covers the campus VPN pool so on-VPN admins
  can reach /health/detail and /healthchecks.
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Fixed
Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs Fixed
@rlorenzo rlorenzo force-pushed the VPR-141-health-check branch from 864b4a4 to 462496d Compare April 29, 2026 00:54
Comment thread web/Program.cs Fixed
- Cloudflare fronts vetmed.ucdavis.edu, so the proxy chain is
  User -> CF -> F5 -> app. Without trusting CF as a known proxy,
  UseForwardedHeaders ignores XFF and RemoteIpAddress stays at
  the CF edge - breaking every IP-based allowlist in the app,
  not just health checks.
- Fetches CF's published v4/v6 CIDRs from cloudflare.com at
  startup (cached for the process lifetime) and adds them to
  KnownIPNetworks. Falls back to a hardcoded snapshot on fetch
  failure so a CF blip during deploy doesn't break startup.
- Bumps ForwardLimit to 2 so the chain walk continues through
  both proxy hops (F5 and CF) and lands on the real client IP
  rather than stopping at the CF edge.
@rlorenzo rlorenzo force-pushed the VPR-141-health-check branch from 1058a5c to e2e1665 Compare April 30, 2026 15:59
@rlorenzo rlorenzo requested a review from bsedwards April 30, 2026 16:12
@rlorenzo
Copy link
Copy Markdown
Contributor Author

@bsedwards The detailed health check is at https://secure-test.vetmed.ucdavis.edu/2/healthchecks

It is IP restricted to:

  • the SVM staff network (192.168.32.0/20), or
  • the UC Davis campus VPN (169.237.251.0/24)

Copy link
Copy Markdown
Collaborator

@bsedwards bsedwards left a comment

Choose a reason for hiding this comment

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

A couple things:

  • Cloudflare is only in front of secure-test at the moment. We are working on getting internal NAT'ed IPs not routed through Cloudflare so we can "see" the internal IP and not the F5's external IP. Cloudflare will be in front of Prod servers at some point, but Ops is still working out the kinks.
  • Can we either restrict the detail and UI health check endpoints to just developer and system (e.g. Jenkins) IPs, or require some other token based or basic authentication? Allowing access to entire networks is overly broad.

- UI collector now self-calls via localhost so the request bypasses
  Cloudflare/F5; the public URL's NAT'd source IP fails the narrowed
  allowlist.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Warning

Rate limit exceeded

@rlorenzo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 49 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fb4800db-eca4-4d8c-929e-6e79ac0d52ca

📥 Commits

Reviewing files that changed from the base of the PR and between a109c2d and a83c9f1.

📒 Files selected for processing (4)
  • web/Classes/HealthChecks/HealthCheckCollectorAuth.cs
  • web/Classes/HealthChecks/HealthCheckCollectorTokenHandler.cs
  • web/Classes/HealthChecks/HealthCheckExtensions.cs
  • web/appsettings.json
📝 Walkthrough

Walkthrough

Issue: lack of comprehensive health monitoring and proxy-aware forwarding. Fix: adds multiple health checks (disk, AWS SSM, HTTP, LDAP, SMTP), adaptive polling, HealthChecks.UI and branding, Cloudflare CIDR fetching for ForwardedHeaders, Jenkins post-deploy health probes, and updated IP allowlist.

Changes

Cohort / File(s) Summary
Health Check Implementations
web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs, web/Classes/HealthChecks/AwsSsmHealthCheck.cs, web/Classes/HealthChecks/DiskSpaceHealthCheck.cs, web/Classes/HealthChecks/HttpEndpointHealthCheck.cs, web/Classes/HealthChecks/LdapHealthCheck.cs, web/Classes/HealthChecks/SmtpHealthCheck.cs
Adds six IHealthCheck implementations: adaptive caching wrapper, AWS SSM Parameter Store probe, disk-space (with optional write test), HTTP endpoint probe, LDAP bind test, and SMTP connect/NOOP check. Each supports configurable timeouts, thresholds, and healthy-when-missing options.
Health Check Registration & Wiring
web/Classes/HealthChecks/HealthCheckExtensions.cs, web/Program.cs
Introduces centralized registration (AddViperHealthChecks) and middleware wiring (UseViperHealthChecks) for /health and /health/detail, HealthChecks.UI setup, adaptive caching defaults, IP allowlist gating for UI, and conditional CSP exclusion for UI paths. Program startup pre-fetches Cloudflare CIDRs and configures ForwardedHeaders.
Cloudflare CIDR Utility & Project deps
web/Classes/CloudflareNetworks.cs, web/Viper.csproj
Adds CloudflareNetworks.FetchOrFallback to retrieve ips-v4/ips-v6 with HTTP fallback to hardcoded snapshot. Project file adds HealthChecks UI packages and EF Core health-check integration.
HealthChecks UI Branding & Client Script
web/wwwroot/css/healthchecks-ui-branding.css, web/wwwroot/js/healthchecks-ui-extras.js
Adds branding CSS (colors, logo, layout tweaks) and a JS mutation observer that converts TimeSpan cells to human-readable durations and injects/removes a campus status banner based on campus check health.
Configuration
web/appsettings.json
Updates InternalAllowlist: removes one IP and replaces it with five entries (four IPv4 addresses and one CIDR range).
Deployment Pipeline
JenkinsFile
Adds post-deployment health verification steps to test and prod deploy stages: retry loop calling /2/health with 10s timeout and backoff delays; succeed on HTTP 200, otherwise fail stage after max attempts.

Sequence Diagram(s)

sequenceDiagram
    participant Startup as Application Startup
    participant CF as Cloudflare Endpoint
    participant Registry as HealthCheck Registry
    participant Middleware as ForwardedHeaders & HealthEndpoints
    participant Jenkins as Jenkins Pipeline

    Startup->>CF: FetchOrFallback(logger) (ips-v4, ips-v6)
    alt fetch success
        CF-->>Startup: CIDR list
    else fetch failure / timeout
        CF-->>Startup: Hardcoded fallback CIDRs
    end

    Startup->>Registry: AddViperHealthChecks() (register Disk, AWS SSM, HTTP, LDAP, SMTP, Adaptive)
    Registry-->>Startup: Health checks configured

    Startup->>Middleware: Configure ForwardedHeaders(ForwardLimit=2, KnownIPNetworks=CF CIDRs)
    Startup->>Middleware: UseViperHealthChecks() (map /health, /health/detail, UI gating)

    Jenkins->>Middleware: Post-deploy probe /2/health (retry loop)
    alt 200 received
        Middleware-->>Jenkins: 200 OK (deployment healthy)
    else timeout/exhausted
        Middleware-->>Jenkins: non-200 / failure (pipeline stage fails)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% 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 clearly summarizes the main change: adding /health endpoints and a UI dashboard for health checks, which aligns with the substantial changeset across health check implementations, extensions, and UI components.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch VPR-141-health-check

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
Review rate limit: 0/1 reviews remaining, refill in 17 minutes and 49 seconds.

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

Copy link
Copy Markdown

@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: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@JenkinsFile`:
- Around line 146-165: The JenkinsFile contains duplicated PowerShell
health-check blocks; extract them into a single reusable Groovy function (e.g.,
verifyHealth(String url)) that wraps the existing powershell step logic and
accepts the URL as a parameter, then replace the two inline powershell blocks
with calls to verifyHealth("<env-specific-url>"); ensure the function preserves
the maxAttempts, delay logic, try/catch and exit codes exactly and keep the
powershell label 'Verify /health returns 200' so behavior and logs remain
unchanged.

In `@web/Classes/CloudflareNetworks.cs`:
- Around line 40-57: The current catch in FetchOrFallback only handles
HttpRequestException and TaskCanceledException, but DNS/socket failures can
surface as other exception types; change the exception handling on
FetchOrFallback to catch broader failures (e.g., catch Exception ex or add
SocketException) so any network/DNS/socket error is logged via logger.Warn and
the method returns HardcodedFallback; keep the existing log message and return
behavior, and ensure the method still uses HardcodedFallback when any
network-related exception occurs.

In `@web/Classes/HealthChecks/AwsSsmHealthCheck.cs`:
- Around line 36-46: The health check in AwsSsmHealthCheck currently calls
DescribeParametersAsync which requires different IAM rights than the config
loader; change the probe to call GetParametersByPathAsync (or GetParameterAsync
for a known key) against the same path/key your configuration loader uses so the
health check uses the same IAM permissions. In practice, inside the health check
method replace the DescribeParametersAsync call with a call to
client.GetParametersByPathAsync(new GetParametersByPathRequest { Path = <your
configured path prefix>, MaxResults = 1 }, cancellationToken) (or
client.GetParameterAsync for a single known parameter), ensure you pass
cancellationToken, and keep the existing exception handling that checks
_healthyWhenMissing and returns HealthCheckResult accordingly; update references
in AwsSsmHealthCheck to use the configuration path field (e.g. the class’s
path/prefix member) along with existing _region and _healthyWhenMissing.

In `@web/Classes/HealthChecks/HealthCheckExtensions.cs`:
- Around line 139-143: Wrap the call that constructs a Uri from
EmailSettings:BaseUrl in a safe check inside
AddViperHealthChecks/HealthCheckExtensions: instead of directly calling new
Uri(baseUrl).AbsolutePath, use Uri.TryCreate to validate the baseUrl (or catch
UriFormatException around new Uri) and if it fails log a warning and fall back
to a safe default path (e.g., "/") before continuing to register the health
check; update the code around the new Uri(baseUrl).AbsolutePath reference so
malformed EmailSettings:BaseUrl no longer crashes startup and the
HttpEndpointHealthCheck receives a validated/fallback path.
- Around line 194-203: HealthCheckExtensions.cs currently constructs a Uri from
configuration["EmailSettings:BaseUrl"] which can throw UriFormatException on
malformed/scheme-less values; change the logic that computes healthEndpointUrl
to validate baseUrl using Uri.TryCreate(..., UriKind.Absolute, out var uri)
(matching the pattern used in VerificationService.cs) and when TryCreate fails
or baseUrl is null/whitespace, set healthEndpointUrl to "/health/detail"; when
TryCreate succeeds use uri.AbsolutePath.TrimEnd('/') to build
$"http://localhost{pathBase}/health/detail".

In `@web/Classes/HealthChecks/HttpEndpointHealthCheck.cs`:
- Around line 39-43: The health check in HttpEndpointHealthCheck currently calls
client.GetAsync(_url, cancellationToken) which buffers the entire response body;
change the call in the CheckHealthAsync method to use the overload that includes
HttpCompletionOption.ResponseHeadersRead so only headers are read
(client.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead,
cancellationToken)), preserving the existing response disposal and status code
logic so the probe returns based on StatusCode without downloading the full
body.

In `@web/wwwroot/css/healthchecks-ui-branding.css`:
- Around line 49-55: Update the decorative left border in the
`#campus-status-banner` rule to use rem units instead of pixels; replace the
hardcoded "4px" value for "border-left" with an equivalent rem measurement
(e.g., 0.25rem or whatever matches your base font sizing) so the selector
"#campus-status-banner" follows the project's sizing guideline using rem units.

In `@web/wwwroot/js/healthchecks-ui-extras.js`:
- Around line 43-54: The hasUnhealthyCampusCheck function uses a fragile DOM
selector (row.querySelector(".hc-status .material-icons") and comparing
icon.textContent to "check_circle") that is tightly coupled to Xabaril UI
internals; add an inline comment immediately above this selector explaining that
this check depends on the current forked UI structure/icon name, that changes
upstream could break it, and mark a TODO to replace with a more robust status
API or data-attribute if/when available (include function name
hasUnhealthyCampusCheck and the selector string in the comment for clarity).
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 60ec9da2-248f-413f-bcda-83460be8a905

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb764f and 23fcaa1.

📒 Files selected for processing (14)
  • JenkinsFile
  • web/Classes/CloudflareNetworks.cs
  • web/Classes/HealthChecks/AdaptivePollingHealthCheck.cs
  • web/Classes/HealthChecks/AwsSsmHealthCheck.cs
  • web/Classes/HealthChecks/DiskSpaceHealthCheck.cs
  • web/Classes/HealthChecks/HealthCheckExtensions.cs
  • web/Classes/HealthChecks/HttpEndpointHealthCheck.cs
  • web/Classes/HealthChecks/LdapHealthCheck.cs
  • web/Classes/HealthChecks/SmtpHealthCheck.cs
  • web/Program.cs
  • web/Viper.csproj
  • web/appsettings.json
  • web/wwwroot/css/healthchecks-ui-branding.css
  • web/wwwroot/js/healthchecks-ui-extras.js

Comment thread JenkinsFile
Comment on lines +146 to +165
powershell label: 'Verify /health returns 200', script: '''
$url = "https://secure.vetmed.ucdavis.edu/2/health"
$maxAttempts = 15
for ($i = 1; $i -le $maxAttempts; $i++) {
$delay = if ($i -le 5) { 2 } else { 4 }
try {
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
Write-Host "Attempt ${i}: $url returned 200 OK"
exit 0
}
Write-Host "Attempt ${i}: $url returned $($response.StatusCode)"
} catch {
Write-Host "Attempt ${i}: $($_.Exception.Message)"
}
if ($i -lt $maxAttempts) { Start-Sleep -Seconds $delay }
}
Write-Error "Health check at $url failed after $maxAttempts attempts"
exit 1
'''
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider extracting duplicated health-check logic.

The test and prod PowerShell blocks are identical except for the URL. A shared function would reduce maintenance burden.

♻️ Example consolidation
def verifyHealth(String url) {
    powershell label: 'Verify /health returns 200', script: """
        \$url = "$url"
        \$maxAttempts = 15
        for (\$i = 1; \$i -le \$maxAttempts; \$i++) {
            \$delay = if (\$i -le 5) { 2 } else { 4 }
            try {
                \$response = Invoke-WebRequest -Uri \$url -UseBasicParsing -TimeoutSec 10
                if (\$response.StatusCode -eq 200) {
                    Write-Host "Attempt \${\$i}: \$url returned 200 OK"
                    exit 0
                }
                Write-Host "Attempt \${\$i}: \$url returned \$(\$response.StatusCode)"
            } catch {
                Write-Host "Attempt \${\$i}: \$(\$_.Exception.Message)"
            }
            if (\$i -lt \$maxAttempts) { Start-Sleep -Seconds \$delay }
        }
        Write-Error "Health check at \$url failed after \$maxAttempts attempts"
        exit 1
    """
}

Then call verifyHealth("https://secure-test.vetmed.ucdavis.edu/2/health") in each stage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@JenkinsFile` around lines 146 - 165, The JenkinsFile contains duplicated
PowerShell health-check blocks; extract them into a single reusable Groovy
function (e.g., verifyHealth(String url)) that wraps the existing powershell
step logic and accepts the URL as a parameter, then replace the two inline
powershell blocks with calls to verifyHealth("<env-specific-url>"); ensure the
function preserves the maxAttempts, delay logic, try/catch and exit codes
exactly and keep the powershell label 'Verify /health returns 200' so behavior
and logs remain unchanged.

Comment on lines +40 to +57
public static IReadOnlyList<string> FetchOrFallback(NLog.Logger logger)
{
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var v4 = http.GetStringAsync("https://www.cloudflare.com/ips-v4/").GetAwaiter().GetResult();
var v6 = http.GetStringAsync("https://www.cloudflare.com/ips-v6/").GetAwaiter().GetResult();
var cidrs = (v4 + "\n" + v6)
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
logger.Info("Fetched {Count} Cloudflare networks from cloudflare.com", cidrs.Length);
return cidrs;
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
logger.Warn(ex, "Failed to fetch Cloudflare IP ranges; using hardcoded fallback ({Count} entries)", HardcodedFallback.Length);
return HardcodedFallback;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

LGTM. The synchronous blocking via GetAwaiter().GetResult() is appropriate here since this runs once at startup before the host starts. The 5-second timeout and hardcoded fallback provide resilience.

One edge case: SocketException or DNS failures may not always be wrapped in HttpRequestException depending on the runtime. Consider broadening the catch or adding a general fallback.

♻️ Broader exception handling
-            catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
+            catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Net.Sockets.SocketException)

Or use a general catch that still logs and falls back gracefully:

-            catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
+            catch (Exception ex)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/Classes/CloudflareNetworks.cs` around lines 40 - 57, The current catch in
FetchOrFallback only handles HttpRequestException and TaskCanceledException, but
DNS/socket failures can surface as other exception types; change the exception
handling on FetchOrFallback to catch broader failures (e.g., catch Exception ex
or add SocketException) so any network/DNS/socket error is logged via
logger.Warn and the method returns HardcodedFallback; keep the existing log
message and return behavior, and ensure the method still uses HardcodedFallback
when any network-related exception occurs.

Comment on lines +36 to +46
using var client = new AmazonSimpleSystemsManagementClient(_region);
await client.DescribeParametersAsync(
new DescribeParametersRequest { MaxResults = 1 },
cancellationToken);
return HealthCheckResult.Healthy("AWS SSM reachable.");
}
catch (AmazonServiceException ex)
{
return _healthyWhenMissing
? HealthCheckResult.Healthy("AWS SSM not configured (skipped).")
: HealthCheckResult.Unhealthy($"AWS SSM unreachable: {ex.ErrorCode}.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What AWS SSM API operations does Amazon.Extensions.Configuration.SystemsManager use to load configuration, and how do their IAM permissions differ from DescribeParameters?

💡 Result:

Amazon.Extensions.Configuration.SystemsManager primarily uses the AWS SSM API operation GetParametersByPath (with Recursive=true and WithDecryption=true) to load configuration parameters from Parameter Store when a path prefix is specified (the typical usage via AddSystemsManager(path)). It may also use GetParameters (batched) if specific ParameterNames are provided, or GetParameter for Secrets Manager-referenced paths. The corresponding IAM permission is ssm:GetParametersByPath on the resource ARN matching the path prefix (e.g., "arn:aws:ssm:::parameter/myapp/"). This differs from ssm:DescribeParameters (used by the DescribeParameters API), which only lists metadata about parameters (names, types, etc.) but does not return parameter values. ssm:DescribeParameters requires no resource-level restrictions (often "Resource": "") and cannot access SecureString values or decrypt them. In contrast, ssm:GetParametersByPath directly retrieves and decrypts values but requires permission on the specific parameter resources matching the path, providing finer-grained control over value access.

Citations:


Use the same SSM operation the configuration loader uses.

The health check calls DescribeParameters to validate AWS SSM reachability, but the app loads configuration via GetParametersByPath. These require different IAM permissions (ssm:DescribeParameters vs. ssm:GetParametersByPath). A least-privilege role with only read access to the config path will fail this readiness check even though startup succeeds.

Probe an actual parameter from the configured path instead—either query a known key with GetParameter or use GetParametersByPath with the same path prefix the configuration loader uses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/Classes/HealthChecks/AwsSsmHealthCheck.cs` around lines 36 - 46, The
health check in AwsSsmHealthCheck currently calls DescribeParametersAsync which
requires different IAM rights than the config loader; change the probe to call
GetParametersByPathAsync (or GetParameterAsync for a known key) against the same
path/key your configuration loader uses so the health check uses the same IAM
permissions. In practice, inside the health check method replace the
DescribeParametersAsync call with a call to client.GetParametersByPathAsync(new
GetParametersByPathRequest { Path = <your configured path prefix>, MaxResults =
1 }, cancellationToken) (or client.GetParameterAsync for a single known
parameter), ensure you pass cancellationToken, and keep the existing exception
handling that checks _healthyWhenMissing and returns HealthCheckResult
accordingly; update references in AwsSsmHealthCheck to use the configuration
path field (e.g. the class’s path/prefix member) along with existing _region and
_healthyWhenMissing.

Comment thread web/Classes/HealthChecks/HealthCheckExtensions.cs
Comment on lines +194 to +203
var baseUrl = configuration["EmailSettings:BaseUrl"]?.TrimEnd('/');
string healthEndpointUrl;
if (string.IsNullOrWhiteSpace(baseUrl))
{
healthEndpointUrl = "/health/detail";
}
else
{
var pathBase = new Uri(baseUrl).AbsolutePath.TrimEnd('/');
healthEndpointUrl = $"http://localhost{pathBase}/health/detail";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i 'appsettings*.json' . -x rg -n '"BaseUrl"\s*:' {}

Repository: ucdavis/VIPER

Length of output: 39


🏁 Script executed:

# Find and examine the HealthCheckExtensions.cs file
fd -t f 'HealthCheckExtensions.cs' . -x wc -l

Repository: ucdavis/VIPER

Length of output: 113


🏁 Script executed:

# Read the file around lines 194-203
fd -t f 'HealthCheckExtensions.cs' . -x cat -n | sed -n '180,220p'

Repository: ucdavis/VIPER

Length of output: 2230


🏁 Script executed:

# Search for EmailSettings configuration patterns in the codebase
rg -i 'emailsettings|baseurl' --type cs -B 2 -A 2

Repository: ucdavis/VIPER

Length of output: 31888


🏁 Script executed:

# Check if Uri.TryCreate is already used elsewhere in the codebase
rg 'Uri\.TryCreate' --type cs

Repository: ucdavis/VIPER

Length of output: 898


🏁 Script executed:

# Check if new Uri() is used elsewhere without Try-pattern
rg 'new Uri\(' --type cs -B 1 -A 1 | head -40

Repository: ucdavis/VIPER

Length of output: 2388


🏁 Script executed:

# Confirm UriFormatException is the thrown exception for invalid URIs
python3 <<'EOF'
# Verify Uri constructor behavior
try:
    from urllib.parse import urlparse
    # Simulate what happens with scheme-less or malformed URLs
    test_urls = [
        "localhost:5000",      # scheme-less
        "///invalid",          # invalid format
        "http://valid.com"     # valid
    ]
    print("Testing invalid URIs that would fail in .NET Uri constructor:")
    for url in test_urls:
        try:
            # This shows why Uri(url) without scheme check fails
            result = urlparse(url)
            if not result.scheme:
                print(f"  '{url}' → scheme-less (Uri constructor would throw)")
            else:
                print(f"  '{url}' → valid")
        except Exception as e:
            print(f"  '{url}' → error: {e}")
EOF

Repository: ucdavis/VIPER

Length of output: 170


🏁 Script executed:

# Check appsettings files to see if BaseUrl is ever explicitly set
find . -name 'appsettings*.json' -o -name 'appsettings*.Development.json' | xargs cat 2>/dev/null | grep -i baseurl || echo "No BaseUrl found in config files"

Repository: ucdavis/VIPER

Length of output: 325


Guard BaseUrl parsing before service registration.

new Uri(baseUrl) throws UriFormatException during app startup when EmailSettings:BaseUrl is malformed or scheme-less. Use Uri.TryCreate(..., UriKind.Absolute, out var uri) and fall back to "/health/detail" when the value is invalid, matching the pattern already in VerificationService.cs.

Suggested fix
-            else
-            {
-                var pathBase = new Uri(baseUrl).AbsolutePath.TrimEnd('/');
-                healthEndpointUrl = $"http://localhost{pathBase}/health/detail";
-            }
+            else if (Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri))
+            {
+                var pathBase = uri.AbsolutePath.TrimEnd('/');
+                healthEndpointUrl = $"http://localhost{pathBase}/health/detail";
+            }
+            else
+            {
+                healthEndpointUrl = "/health/detail";
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/Classes/HealthChecks/HealthCheckExtensions.cs` around lines 194 - 203,
HealthCheckExtensions.cs currently constructs a Uri from
configuration["EmailSettings:BaseUrl"] which can throw UriFormatException on
malformed/scheme-less values; change the logic that computes healthEndpointUrl
to validate baseUrl using Uri.TryCreate(..., UriKind.Absolute, out var uri)
(matching the pattern used in VerificationService.cs) and when TryCreate fails
or baseUrl is null/whitespace, set healthEndpointUrl to "/health/detail"; when
TryCreate succeeds use uri.AbsolutePath.TrimEnd('/') to build
$"http://localhost{pathBase}/health/detail".

Comment on lines +39 to +43
using var response = await client.GetAsync(_url, cancellationToken);
var code = (int)response.StatusCode;
return code < 500
? HealthCheckResult.Healthy($"{_displayName} reachable (HTTP {code}).")
: HealthCheckResult.Unhealthy($"{_displayName} returned HTTP {code}.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In .NET HttpClient, does GetAsync(string, CancellationToken) buffer the full response body by default, and is HttpCompletionOption.ResponseHeadersRead the right option for a health check that only needs the status code?

💡 Result:

Yes—by default, HttpClient’s GetAsync (without HttpCompletionOption.ResponseHeadersRead) buffers the entire response body before completing the task, reading all response content into memory. HttpCompletionOption.ResponseHeadersRead is the right option for a health check that only needs the status code/headers (it completes once the response headers are available and does not read/buffer the content yet). Practical implication: if you use ResponseHeadersRead, you should validate the status code immediately (e.g., response.EnsureSuccessStatusCode) and avoid reading the body unless you truly need it; if you do need to read the body, you’ll need to apply your own timeout/cancellation to the content read separately because HttpClient.Timeout (and the overload’s cancellationToken) only applies up to the point headers are read when using ResponseHeadersRead.

Citations:


Use HttpCompletionOption.ResponseHeadersRead for status-only probes.

GetAsync(_url, cancellationToken) buffers the full response body by default. This health check only reads StatusCode, so a slow or large body unnecessarily delays the probe, creating false negatives and wasting bandwidth on every poll.

Suggested fix
-                using var response = await client.GetAsync(_url, cancellationToken);
+                using var response = await client.GetAsync(
+                    _url,
+                    HttpCompletionOption.ResponseHeadersRead,
+                    cancellationToken);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using var response = await client.GetAsync(_url, cancellationToken);
var code = (int)response.StatusCode;
return code < 500
? HealthCheckResult.Healthy($"{_displayName} reachable (HTTP {code}).")
: HealthCheckResult.Unhealthy($"{_displayName} returned HTTP {code}.");
using var response = await client.GetAsync(
_url,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
var code = (int)response.StatusCode;
return code < 500
? HealthCheckResult.Healthy($"{_displayName} reachable (HTTP {code}).")
: HealthCheckResult.Unhealthy($"{_displayName} returned HTTP {code}.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/Classes/HealthChecks/HttpEndpointHealthCheck.cs` around lines 39 - 43,
The health check in HttpEndpointHealthCheck currently calls
client.GetAsync(_url, cancellationToken) which buffers the entire response body;
change the call in the CheckHealthAsync method to use the overload that includes
HttpCompletionOption.ResponseHeadersRead so only headers are read
(client.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead,
cancellationToken)), preserving the existing response disposal and status code
logic so the probe returns based on StatusCode without downloading the full
body.

Comment on lines +49 to +55
#campus-status-banner {
margin: 0 2rem 1rem 2rem;
padding: 0.75rem 1rem;
background-color: #fff8e1;
border-left: 4px solid var(--warningColor);
color: var(--darkColor);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor: Line 53 uses 4px for border-left. Per coding guidelines, prefer rem for sizing.

Given this is a decorative border for a third-party UI and unlikely to cause issues, this is very low priority.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/wwwroot/css/healthchecks-ui-branding.css` around lines 49 - 55, Update
the decorative left border in the `#campus-status-banner` rule to use rem units
instead of pixels; replace the hardcoded "4px" value for "border-left" with an
equivalent rem measurement (e.g., 0.25rem or whatever matches your base font
sizing) so the selector "#campus-status-banner" follows the project's sizing
guideline using rem units.

Comment on lines +43 to +54
function hasUnhealthyCampusCheck() {
const rows = document.querySelectorAll("tr");
for (const row of rows) {
const nameCell = row.querySelector("td");
if (!nameCell) continue;
if (!nameCell.textContent.trim().startsWith("campus-")) continue;
const icon = row.querySelector(".hc-status .material-icons");
// check_circle = Healthy; anything else (error, warning, cancel) is a problem
if (icon && icon.textContent.trim() !== "check_circle") return true;
}
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Fragile selector coupling. The check for .hc-status .material-icons with text check_circle is tightly coupled to the Xabaril UI's internal DOM structure. If the fork or upstream changes icon names or structure, this will silently fail.

Acceptable for now since you control the pinned fork version. Add a comment noting this coupling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/wwwroot/js/healthchecks-ui-extras.js` around lines 43 - 54, The
hasUnhealthyCampusCheck function uses a fragile DOM selector
(row.querySelector(".hc-status .material-icons") and comparing icon.textContent
to "check_circle") that is tightly coupled to Xabaril UI internals; add an
inline comment immediately above this selector explaining that this check
depends on the current forked UI structure/icon name, that changes upstream
could break it, and mark a TODO to replace with a more robust status API or
data-attribute if/when available (include function name hasUnhealthyCampusCheck
and the selector string in the comment for clarity).

rlorenzo added 2 commits May 1, 2026 14:56
The localhost loopback approach hit IIS host-header routing: requests
with Host: localhost don't match the /2 site binding and get a 404.
Going back to the public BaseUrl works because internal DNS resolves
to the F5, keeping the self-call inside 192.168.56.0/24 - narrower
than the original /20 + VPN /24 that was flagged in review.
… token

The /health/detail endpoint must stay IP-allowlisted, but the in-app
UI collector also has to reach it without widening the list. On every
poll, a delegating handler stamps a process-unique token header that
the endpoint filter recognizes; the IP check still applies to all
other callers, so the allowlist can stay scoped to dev IPs only.
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.

5 participants