diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 80cf99d50..29684d05c 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "10.0.9",
+ "version": "10.0.10",
"commands": [
"dotnet-ef"
]
@@ -15,7 +15,7 @@
]
},
"jetbrains.resharper.globaltools": {
- "version": "2026.1.3",
+ "version": "2026.2.0",
"commands": [
"jb"
]
diff --git a/test/CTS/AssessmentControllerTest.cs b/test/CTS/AssessmentControllerTest.cs
index 3f166ad2d..a5c8141da 100644
--- a/test/CTS/AssessmentControllerTest.cs
+++ b/test/CTS/AssessmentControllerTest.cs
@@ -123,10 +123,10 @@ public async Task GetAssessmentCheck()
var actrlAsChief = GetAssessmentController(SetupUsers.UserType.Chief);
var encounterIdExists = SetupAssessments.GetEncounters().First().EncounterId;
- var encounterIdOtherFac = SetupAssessments.GetEncounters().Where(e => e.EnteredBy != SetupUsers.facultyUser.AaudUserId).First().EncounterId;
- var encounterIdOtherStd = SetupAssessments.GetEncounters().Where(e => e.StudentUserId != SetupUsers.studentUser1.AaudUserId).First().EncounterId;
- var encounterIdChief = SetupAssessments.GetEncounters().Where(e => e.ServiceId == SetupServices.ServiceChiefs[0].ServiceId).First().EncounterId;
- var encounterIdNotChief = SetupAssessments.GetEncounters().Where(e => e.ServiceId != SetupServices.ServiceChiefs[0].ServiceId).First().EncounterId;
+ var encounterIdOtherFac = SetupAssessments.GetEncounters().First(e => e.EnteredBy != SetupUsers.facultyUser.AaudUserId).EncounterId;
+ var encounterIdOtherStd = SetupAssessments.GetEncounters().First(e => e.StudentUserId != SetupUsers.studentUser1.AaudUserId).EncounterId;
+ var encounterIdChief = SetupAssessments.GetEncounters().First(e => e.ServiceId == SetupServices.ServiceChiefs[0].ServiceId).EncounterId;
+ var encounterIdNotChief = SetupAssessments.GetEncounters().First(e => e.ServiceId != SetupServices.ServiceChiefs[0].ServiceId).EncounterId;
var encounterIdNotExists = 99999;
//act
diff --git a/test/CTS/SetupAssessments.cs b/test/CTS/SetupAssessments.cs
index a32b41d57..f329ffb1f 100644
--- a/test/CTS/SetupAssessments.cs
+++ b/test/CTS/SetupAssessments.cs
@@ -23,7 +23,7 @@ internal static class SetupAssessments
FullName = SetupUsers.studentUser1.DisplayLastName + ", " + SetupUsers.studentUser1.DisplayFirstName,
MailId = "",
},
- EnteredByPerson = SetupPeople.GetPeople().Where(p => p.PersonId == SetupUsers.facultyUser.AaudUserId).FirstOrDefault(),
+ EnteredByPerson = SetupPeople.GetPeople().FirstOrDefault(p => p.PersonId == SetupUsers.facultyUser.AaudUserId),
ServiceId = 1,
},
new Encounter
@@ -38,7 +38,7 @@ internal static class SetupAssessments
FullName = SetupUsers.studentUser1.DisplayLastName + ", " + SetupUsers.studentUser1.DisplayFirstName,
MailId = "",
},
- EnteredByPerson = SetupPeople.GetPeople().Where(p => p.PersonId == SetupUsers.facultyUser.AaudUserId).FirstOrDefault(),
+ EnteredByPerson = SetupPeople.GetPeople().FirstOrDefault(p => p.PersonId == SetupUsers.facultyUser.AaudUserId),
ServiceId = 2,
},
new Encounter
@@ -53,7 +53,7 @@ internal static class SetupAssessments
FullName = SetupUsers.studentUser2.DisplayLastName + ", " + SetupUsers.studentUser2.DisplayFirstName,
MailId = "",
},
- EnteredByPerson = SetupPeople.GetPeople().Where(p => p.PersonId == SetupUsers.otherFacultyUser.AaudUserId).FirstOrDefault(),
+ EnteredByPerson = SetupPeople.GetPeople().FirstOrDefault(p => p.PersonId == SetupUsers.otherFacultyUser.AaudUserId),
ServiceId = 3,
},
};
diff --git a/test/Viper.test.csproj b/test/Viper.test.csproj
index af5cccc12..a2c0e746d 100644
--- a/test/Viper.test.csproj
+++ b/test/Viper.test.csproj
@@ -22,23 +22,25 @@
-
-
+
+
+
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/web/Areas/Effort/Services/CourseClassificationService.cs b/web/Areas/Effort/Services/CourseClassificationService.cs
index e4865b212..02b2f9bc3 100644
--- a/web/Areas/Effort/Services/CourseClassificationService.cs
+++ b/web/Areas/Effort/Services/CourseClassificationService.cs
@@ -34,7 +34,7 @@ public bool Is199299Course(string? crseNumb)
// Match 199 or 299 followed by non-digit or end of string
// Valid: "199", "299", "199A", "299R", "199B"
// Invalid: "1990", "2995", "19900"
- return Regex.IsMatch(crseNumb.Trim(), @"^(199|299)([^0-9]|$)", RegexOptions.IgnoreCase);
+ return Regex.IsMatch(crseNumb.Trim(), @"^(199|299)([^0-9]|$)", RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1));
}
///
diff --git a/web/Areas/RAPS/Controllers/AdGroupRolesController.cs b/web/Areas/RAPS/Controllers/AdGroupRolesController.cs
index 8412ff84e..80be33037 100644
--- a/web/Areas/RAPS/Controllers/AdGroupRolesController.cs
+++ b/web/Areas/RAPS/Controllers/AdGroupRolesController.cs
@@ -27,8 +27,8 @@ public AdGroupRolesController(RAPSContext context)
private bool GroupExists(int groupId)
{
var group = _context.OuGroups
- .Where(gr => gr.OugroupId == groupId)
- .FirstOrDefault();
+ .AsNoTracking()
+ .FirstOrDefault(gr => gr.OugroupId == groupId);
return group != null;
}
diff --git a/web/Areas/RAPS/Services/RAPSCacheService.cs b/web/Areas/RAPS/Services/RAPSCacheService.cs
index 3156d05c0..1590c1fb6 100644
--- a/web/Areas/RAPS/Services/RAPSCacheService.cs
+++ b/web/Areas/RAPS/Services/RAPSCacheService.cs
@@ -1,3 +1,4 @@
+using Microsoft.EntityFrameworkCore;
using Viper.Classes.SQLContext;
using Viper.Models.AAUD;
@@ -16,7 +17,7 @@ public RAPSCacheService(RAPSContext rapsContext, AAUDContext aaudContext, IUserH
public void ClearCachedRolesAndPermissionsForUser(string mothraId)
{
- AaudUser? user = aaudContext.AaudUsers.Where(u => u.MothraId == mothraId).FirstOrDefault();
+ AaudUser? user = aaudContext.AaudUsers.AsNoTracking().FirstOrDefault(u => u.MothraId == mothraId);
if (user != null)
{
userHelper.ClearCachedRolesAndPermissions(user);
diff --git a/web/Areas/RAPS/Services/VMACSExport.cs b/web/Areas/RAPS/Services/VMACSExport.cs
index 64c465b21..062df033e 100644
--- a/web/Areas/RAPS/Services/VMACSExport.cs
+++ b/web/Areas/RAPS/Services/VMACSExport.cs
@@ -214,7 +214,9 @@ join role in _RAPSContext.TblRoles on rm.RoleId equals role.RoleId
//Get a single record with their ids and blank role so that an empty permissions array can be pushed.
if (userList.Count == 0 && loginId.Length > 0)
{
- var user = _RAPSContext.VwAaudUser.Where(a => a.LoginId == loginId).FirstOrDefault();
+ var user = _RAPSContext.VwAaudUser
+ .AsNoTracking()
+ .FirstOrDefault(a => a.LoginId == loginId);
if (user != null)
{
userList.Add(new UserList
diff --git a/web/Areas/RAPS/Views/Groups/List.cshtml b/web/Areas/RAPS/Views/Groups/List.cshtml
index f5b73eab9..d0c2c6a57 100644
--- a/web/Areas/RAPS/Views/Groups/List.cshtml
+++ b/web/Areas/RAPS/Views/Groups/List.cshtml
@@ -136,11 +136,11 @@
},
async mounted() {
this.groupTable.load(this)
- this.ouGroups = await viperFetch(this, "Groups/OU")
+ this.ouGroups = (await viperFetch(this, "Groups/OU")) ?? []
this.ouGroups = this.ouGroups
.map(g => ({ label: "OU: " + g.cn, value: g.distinguishedName }))
- this.adGroups = await viperFetch(this, "Groups/AD")
+ this.adGroups = (await viperFetch(this, "Groups/AD")) ?? []
this.adGroups = this.adGroups
.map(g => ({ label: "AD: " + (g.displayName ?? g.samAccountName), value: g.distinguishedName }))
diff --git a/web/Areas/RAPS/Views/Groups/Members.cshtml b/web/Areas/RAPS/Views/Groups/Members.cshtml
index 5f1e78fb8..4ffe0b6ba 100644
--- a/web/Areas/RAPS/Views/Groups/Members.cshtml
+++ b/web/Areas/RAPS/Views/Groups/Members.cshtml
@@ -9,8 +9,8 @@
-
-
+
+
{
return {label: formatDate(d), value: d}
})
diff --git a/web/Areas/RAPS/Views/Members/Permissions.cshtml b/web/Areas/RAPS/Views/Members/Permissions.cshtml
index 84e57c2b1..24b924e56 100644
--- a/web/Areas/RAPS/Views/Members/Permissions.cshtml
+++ b/web/Areas/RAPS/Views/Members/Permissions.cshtml
@@ -251,7 +251,7 @@
this.selectedPermissions = []
},
loadMember: async function () {
- this.member = await viperFetch(this, "members/" + this.memberId)
+ this.member = (await viperFetch(this, "members/" + this.memberId)) ?? {}
}
},
mounted() {
diff --git a/web/Areas/RAPS/Views/Members/RSOP.cshtml b/web/Areas/RAPS/Views/Members/RSOP.cshtml
index ceae260bf..f3479ddec 100644
--- a/web/Areas/RAPS/Views/Members/RSOP.cshtml
+++ b/web/Areas/RAPS/Views/Members/RSOP.cshtml
@@ -46,7 +46,7 @@
},
methods: {
loadMember: async function () {
- this.member = await viperFetch(this, "Members/" + this.memberId)
+ this.member = (await viperFetch(this, "Members/" + this.memberId)) ?? {}
}
},
async mounted() {
diff --git a/web/Areas/RAPS/Views/Members/Roles.cshtml b/web/Areas/RAPS/Views/Members/Roles.cshtml
index 7ef9a8bca..39b3b2558 100644
--- a/web/Areas/RAPS/Views/Members/Roles.cshtml
+++ b/web/Areas/RAPS/Views/Members/Roles.cshtml
@@ -265,7 +265,7 @@
this.selectedRoles = []
},
loadMember: async function () {
- this.member = await viperFetch(this, "Members/" + this.memberId)
+ this.member = (await viperFetch(this, "Members/" + this.memberId)) ?? {}
},
pushToVMACS: async function (skipToggleCheck) {
if (skipToggleCheck || this.updateVMACS) {
diff --git a/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml b/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml
index 8654d27f3..604263e91 100644
--- a/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml
+++ b/web/Areas/RAPS/Views/Permissions/AllMembers.cshtml
@@ -56,7 +56,7 @@
},
methods: {
loadPermission: async function () {
- this.permission = await viperFetch(this, "Permissions/" + this.permissionId)
+ this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {}
}
},
async mounted() {
diff --git a/web/Areas/RAPS/Views/Permissions/Members.cshtml b/web/Areas/RAPS/Views/Permissions/Members.cshtml
index e8aa02346..7db52a135 100644
--- a/web/Areas/RAPS/Views/Permissions/Members.cshtml
+++ b/web/Areas/RAPS/Views/Permissions/Members.cshtml
@@ -170,7 +170,7 @@
},
methods: {
loadPermission: async function () {
- this.permission = await viperFetch(this, "Permissions/" + this.permissionId)
+ this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {}
},
memberSearch: function (val, update, abort) {
if (val.length < 3) {
diff --git a/web/Areas/RAPS/Views/Permissions/Roles.cshtml b/web/Areas/RAPS/Views/Permissions/Roles.cshtml
index 95d512689..60e736627 100644
--- a/web/Areas/RAPS/Views/Permissions/Roles.cshtml
+++ b/web/Areas/RAPS/Views/Permissions/Roles.cshtml
@@ -102,7 +102,7 @@
},
methods: {
loadPermission: async function () {
- this.permission = await viperFetch(this, "Permissions/" + this.permissionId)
+ this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {}
},
updateRolePermissions: async function (allowAccess = 1) {
var roleIds = this.selectedPermissions.reduce( (ids, sp) => {ids.push(sp.roleId); return ids;}, [] )
diff --git a/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml b/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml
index f7d3c8b98..28303c8ac 100644
--- a/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml
+++ b/web/Areas/RAPS/Views/Permissions/RolesRO.cshtml
@@ -49,7 +49,7 @@
},
methods: {
loadPermission: async function () {
- this.permission = await viperFetch(this, "Permissions/" + this.permissionId)
+ this.permission = (await viperFetch(this, "Permissions/" + this.permissionId)) ?? {}
},
loadTables: async function () {
await this.rolePermissionTable.load(this)
diff --git a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml
index 03b7c86d4..fa4231afa 100644
--- a/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml
+++ b/web/Areas/RAPS/Views/Roles/ApplyTemplate.cshtml
@@ -17,7 +17,7 @@
| Review Changes |
-
+
|
@@ -50,6 +50,8 @@
membersFound: false, //show table if a matching user was found
roleTemplateId: 0,
roleTemplate: {},
+ // Starts false so applying is disabled until the template is read.
+ templateLoaded: false,
loginids: "",
userSearch: "",
userSearchResults: [],
@@ -142,7 +144,12 @@
},
async mounted() {
this.roleTemplateId = this.urlParams.get("roleTemplateId")
- this.roleTemplate = await viperFetch(this, "RoleTemplates/" + this.roleTemplateId)
+ // roleTemplateRoles must be an array: the VMACS push path calls .reduce() on it.
+ // Track load failure separately so an unread template is never mistaken for
+ // an empty one, which would push an empty role list to VMACS.
+ const template = await viperFetch(this, "RoleTemplates/" + this.roleTemplateId)
+ this.templateLoaded = template !== undefined
+ this.roleTemplate = template ?? { roleTemplateRoles: [] }
this.VMACSPush = this.urlPath.indexOf("/VMACS.") > 0
},
watch: {
diff --git a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml
index 0f18480aa..2b1818408 100644
--- a/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml
+++ b/web/Areas/RAPS/Views/Roles/DelegateRoles.cshtml
@@ -12,7 +12,7 @@
:pagination="{rowsPerPage:0}"
:loading="loadingRoles">
-
+
@@ -26,6 +26,8 @@
roles: [],
selectedRoles: [],
loadingRoles: false,
+ // Starts false so the page is disabled until the initial read succeeds.
+ rolesLoaded: false,
columns: [
{ name: "role", label: "Role", field: "friendlyName", align: "left" },
{ name: "description", label: "Description", field: "description", align: "left" }
@@ -34,13 +36,24 @@
},
methods: {
loadRoles: async function() {
+ this.rolesLoaded = false
var childRoles = await viperFetch(this, "Roles/ControlledBy/" + + this.urlParams.get("roleId"))
- var allRoles = (await viperFetch(this, "Roles?Application=0"))
- .filter(r => childRoles.find(cr => cr.roleId === r.roleId) === undefined)
- this.selectedRoles = childRoles;//.reduce((result, cr) => { result.push(cr.roleId); return result }, [])
- this.roles = childRoles.concat(allRoles)
+ var allRoles = await viperFetch(this, "Roles?Application=0")
+ // A failed or still-pending read must not look like "nothing is
+ // selected": submitting that would PUT an empty list and wipe the
+ // existing delegations. Stay disabled until both reads succeed.
+ if (childRoles === undefined || allRoles === undefined) {
+ return
+ }
+ this.selectedRoles = childRoles
+ this.roles = childRoles.concat(
+ allRoles.filter(r => childRoles.find(cr => cr.roleId === r.roleId) === undefined))
+ this.rolesLoaded = true
},
submitChanges: async function() {
+ if (!this.rolesLoaded) {
+ return
+ }
var roleIds = this.selectedRoles.reduce( (result, role) => {
result.push(role.roleId)
return result
@@ -57,8 +70,8 @@
}
},
async mounted() {
- this.loadRoles()
- this.role = await viperFetch(this, "Roles/" + this.urlParams.get("roleId"))
+ await this.loadRoles()
+ this.role = (await viperFetch(this, "Roles/" + this.urlParams.get("roleId"))) ?? {}
}
})
diff --git a/web/Areas/RAPS/Views/Roles/Members.cshtml b/web/Areas/RAPS/Views/Roles/Members.cshtml
index d852e60d9..3ce720a0d 100644
--- a/web/Areas/RAPS/Views/Roles/Members.cshtml
+++ b/web/Areas/RAPS/Views/Roles/Members.cshtml
@@ -162,7 +162,8 @@
},
methods: {
loadRole: async function () {
- this.role = await viperFetch(this, "Roles/" + this.roleId)
+ // instance must be a string: mounted() calls role.instance.indexOf().
+ this.role = (await viperFetch(this, "Roles/" + this.roleId)) ?? { instance: "" }
},
memberSearch: function (val, update, abort) {
if (val.length < 3) {
diff --git a/web/Areas/RAPS/Views/Roles/Permissions.cshtml b/web/Areas/RAPS/Views/Roles/Permissions.cshtml
index 2e1b7614e..6bfbc4e05 100644
--- a/web/Areas/RAPS/Views/Roles/Permissions.cshtml
+++ b/web/Areas/RAPS/Views/Roles/Permissions.cshtml
@@ -80,7 +80,7 @@
},
methods: {
loadRole: async function () {
- this.role = await viperFetch(this, "Roles/" + this.roleId)
+ this.role = (await viperFetch(this, "Roles/" + this.roleId)) ?? {}
},
loadRolePermissions: async function() {
await this.rolePermissionTable.load(this)
diff --git a/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml b/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml
index 0a0f33925..89f742c15 100644
--- a/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml
+++ b/web/Areas/RAPS/Views/Roles/TemplateRoles.cshtml
@@ -66,7 +66,7 @@
},
async mounted() {
this.loadRoles()
- this.roleTemplate = await viperFetch(this, "RoleTemplates/" + this.urlParams.get("roleTemplateId"))
+ this.roleTemplate = (await viperFetch(this, "RoleTemplates/" + this.urlParams.get("roleTemplateId"))) ?? {}
}
})
diff --git a/web/Areas/Students/Services/PhotoExportService.cs b/web/Areas/Students/Services/PhotoExportService.cs
index 3729c75eb..db4aaa89b 100644
--- a/web/Areas/Students/Services/PhotoExportService.cs
+++ b/web/Areas/Students/Services/PhotoExportService.cs
@@ -64,7 +64,7 @@ public class PhotoExportService : IPhotoExportService
// and our own home-grown checker flag 3+ consecutive whitespace chars
// ("RepeatedBlanks"); collapsing them on the way out is a safe no-op for
// clean data and a structural improvement for messy data.
- private static readonly Regex RepeatedWhitespaceRegex = new(@"\s{2,}", RegexOptions.Compiled);
+ private static readonly Regex RepeatedWhitespaceRegex = new(@"\s{2,}", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
private static string CollapseWhitespace(string? text) =>
string.IsNullOrEmpty(text) ? string.Empty : RepeatedWhitespaceRegex.Replace(text, " ").Trim();
diff --git a/web/Areas/Students/Services/PhotoService.cs b/web/Areas/Students/Services/PhotoService.cs
index 93ef0788e..3f8f45c4b 100644
--- a/web/Areas/Students/Services/PhotoService.cs
+++ b/web/Areas/Students/Services/PhotoService.cs
@@ -27,7 +27,7 @@ public class PhotoService : IPhotoService
private const int DefaultCacheDurationHours = 24;
// Compiled regex for mailId validation - compiled once for performance
- private static readonly Regex MailIdValidationRegex = new("^[a-z0-9.-]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ private static readonly Regex MailIdValidationRegex = new("^[a-z0-9.-]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
public PhotoService(IConfiguration configuration, IMemoryCache cache, ILogger logger, IWebHostEnvironment webHostEnvironment)
{
diff --git a/web/Viper.csproj b/web/Viper.csproj
index 7b8e781c2..ad780ee0d 100644
--- a/web/Viper.csproj
+++ b/web/Viper.csproj
@@ -42,20 +42,20 @@
-
-
-
+
+
+
-
-
+
+
-
+
-
+
-
+
@@ -71,18 +71,17 @@
-
-
-
+
+
+
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
diff --git a/web/ViteProxyHelpers.cs b/web/ViteProxyHelpers.cs
index 0e4e4f9d4..a1f8a00d7 100644
--- a/web/ViteProxyHelpers.cs
+++ b/web/ViteProxyHelpers.cs
@@ -41,10 +41,15 @@ internal static class ViteProxyHelpers
private const string VueAppRoutePattern = @"^/({0})(/.*)?$";
private const string VueAppAssetPattern = @"^/({0})/.*\.(js|ts|css|map|vue|json)$|^/({0})\.(js|ts|css|map|vue|json)$";
+ // Bounds matching so a pathological request path can't pin a request thread.
+ // Must stay above the regex fields: static initialisers run in textual order.
+ private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
+
// Regex for built asset files with Vite hashes (used to skip proxying built assets)
private static readonly Regex AssetHashRegex = new Regex(
$@"{Regex.Escape(ViteAssetsBasePath)}{AssetHashPattern}{SupportedAssetExtensions}$",
- RegexOptions.IgnoreCase | RegexOptions.Compiled
+ RegexOptions.IgnoreCase | RegexOptions.Compiled,
+ RegexTimeout
);
// Cached compiled regexes - initialized once at startup
@@ -76,15 +81,15 @@ private static void InitializeRegexes(string[] vueAppNames)
if (string.IsNullOrEmpty(vueAppsPattern))
{
// @"(?!)" is a negative lookahead that never matches any input; used here to ensure no routes/assets match when no app names are provided.
- _vueAppRouteRegex = new Regex(@"(?!)", RegexOptions.Compiled);
- _vueAppAssetRegex = new Regex(@"(?!)", RegexOptions.Compiled);
+ _vueAppRouteRegex = new Regex(@"(?!)", RegexOptions.Compiled, RegexTimeout);
+ _vueAppAssetRegex = new Regex(@"(?!)", RegexOptions.Compiled, RegexTimeout);
}
else
{
_vueAppRouteRegex = new Regex(string.Format(VueAppRoutePattern, vueAppsPattern),
- RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout);
_vueAppAssetRegex = new Regex(string.Format(VueAppAssetPattern, vueAppsPattern),
- RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexTimeout);
}
}
}
@@ -98,7 +103,7 @@ public static bool ShouldProxyToVite(HttpContext context, string[] vueAppNames)
var path = context.Request.Path;
// Skip proxying built asset files with hashes - serve as static files
- if (path.HasValue && AssetHashRegex.IsMatch(path.Value))
+ if (path.HasValue && IsMatchWithinTimeout(AssetHashRegex, path.Value))
{
return false;
}
@@ -122,19 +127,36 @@ public static bool ShouldProxyToVite(HttpContext context, string[] vueAppNames)
var pathValue = path.Value;
// Match Vue app routes: /CTS, /Computing, /Students (exact match or with sub-paths)
- if (_vueAppRouteRegex!.IsMatch(pathValue))
+ if (IsMatchWithinTimeout(_vueAppRouteRegex!, pathValue))
{
return true;
}
// Match: /AppName/file.ext (assets in app subdirectories)
// OR: /appname.ext (root-level entry files like /cts.ts)
- return _vueAppAssetRegex!.IsMatch(pathValue);
+ return IsMatchWithinTimeout(_vueAppAssetRegex!, pathValue);
}
return false;
}
+ ///
+ /// Runs a match under the configured timeout. A timed-out match is treated as a
+ /// non-match so the request falls through to normal handling, which is how it
+ /// behaved before timeouts were added, rather than surfacing as a 500.
+ ///
+ private static bool IsMatchWithinTimeout(Regex regex, string input)
+ {
+ try
+ {
+ return regex.IsMatch(input);
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return false;
+ }
+ }
+
///
/// Builds the target URL for proxying to Vite development server
///