From 158d976ccb23299ec0b7ba7f343850899966a1bc Mon Sep 17 00:00:00 2001 From: harsh-aghara Date: Tue, 9 Jun 2026 20:39:34 +0530 Subject: [PATCH 1/6] feat(client): add HTTP status codes constants for monitor options --- client/src/Utils/statusCode.ts | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/client/src/Utils/statusCode.ts b/client/src/Utils/statusCode.ts index 33a5f3de1..73ee2a892 100644 --- a/client/src/Utils/statusCode.ts +++ b/client/src/Utils/statusCode.ts @@ -74,3 +74,83 @@ export const getStatusCodeValueType = (code: number): ValueType => { if (code < 400) return "neutral"; return "negative"; }; + +export interface HttpStatusCodeOption { + id: number; + name: string; +} + +export const ALL_HTTP_STATUS_CODES: HttpStatusCodeOption[] = [ + { id: 100, name: "100 - Continue" }, + { id: 101, name: "101 - Switching Protocols" }, + { id: 102, name: "102 - Processing" }, + { id: 103, name: "103 - Early Hints" }, + { id: 200, name: "200 - OK" }, + { id: 201, name: "201 - Created" }, + { id: 202, name: "202 - Accepted" }, + { id: 203, name: "203 - Non-Authoritative Information" }, + { id: 204, name: "204 - No Content" }, + { id: 205, name: "205 - Reset Content" }, + { id: 206, name: "206 - Partial Content" }, + { id: 207, name: "207 - Multi-Status" }, + { id: 208, name: "208 - Already Reported" }, + { id: 226, name: "226 - IM Used" }, + { id: 300, name: "300 - Multiple Choices" }, + { id: 301, name: "301 - Moved Permanently" }, + { id: 302, name: "302 - Found" }, + { id: 303, name: "303 - See Other" }, + { id: 304, name: "304 - Not Modified" }, + { id: 305, name: "305 - Use Proxy" }, + { id: 307, name: "307 - Temporary Redirect" }, + { id: 308, name: "308 - Permanent Redirect" }, + { id: 400, name: "400 - Bad Request" }, + { id: 401, name: "401 - Unauthorized" }, + { id: 402, name: "402 - Payment Required" }, + { id: 403, name: "403 - Forbidden" }, + { id: 404, name: "404 - Not Found" }, + { id: 405, name: "405 - Method Not Allowed" }, + { id: 406, name: "406 - Not Acceptable" }, + { id: 407, name: "407 - Proxy Authentication Required" }, + { id: 408, name: "408 - Request Timeout" }, + { id: 409, name: "409 - Conflict" }, + { id: 410, name: "410 - Gone" }, + { id: 411, name: "411 - Length Required" }, + { id: 412, name: "412 - Precondition Failed" }, + { id: 413, name: "413 - Payload Too Large" }, + { id: 414, name: "414 - URI Too Long" }, + { id: 415, name: "415 - Unsupported Media Type" }, + { id: 416, name: "416 - Range Not Satisfiable" }, + { id: 417, name: "417 - Expectation Failed" }, + { id: 418, name: "418 - I'm a Teapot" }, + { id: 419, name: "419 - Page Expired (Laravel)" }, + { id: 421, name: "421 - Misdirected Request" }, + { id: 422, name: "422 - Unprocessable Entity" }, + { id: 423, name: "423 - Locked" }, + { id: 424, name: "424 - Failed Dependency" }, + { id: 425, name: "425 - Too Early" }, + { id: 426, name: "426 - Upgrade Required" }, + { id: 428, name: "428 - Precondition Required" }, + { id: 429, name: "429 - Too Many Requests" }, + { id: 431, name: "431 - Request Header Fields Too Large" }, + { id: 451, name: "451 - Unavailable For Legal Reasons" }, + { id: 499, name: "499 - Client Closed Request (NGINX)" }, + { id: 500, name: "500 - Internal Server Error" }, + { id: 501, name: "501 - Not Implemented" }, + { id: 502, name: "502 - Bad Gateway" }, + { id: 503, name: "503 - Service Unavailable" }, + { id: 504, name: "504 - Gateway Timeout" }, + { id: 505, name: "505 - HTTP Version Not Supported" }, + { id: 506, name: "506 - Variant Also Negotiates" }, + { id: 507, name: "507 - Insufficient Storage" }, + { id: 508, name: "508 - Loop Detected" }, + { id: 510, name: "510 - Not Extended" }, + { id: 511, name: "511 - Network Authentication Required" }, + { id: 520, name: "520 - Web Server Returned an Unknown Error (Cloudflare)" }, + { id: 521, name: "521 - Web Server Is Down (Cloudflare)" }, + { id: 522, name: "522 - Connection Timed Out (Cloudflare)" }, + { id: 523, name: "523 - Origin Is Unreachable (Cloudflare)" }, + { id: 524, name: "524 - A Timeout Occurred (Cloudflare)" }, + { id: 525, name: "525 - SSL Handshake Failed (Cloudflare)" }, + { id: 526, name: "526 - Invalid SSL Certificate (Cloudflare)" }, + { id: 527, name: "527 - Railgun Error (Cloudflare)" }, +]; From 603bd1d814a982a5cc02c028d012dc992b6ef86e Mon Sep 17 00:00:00 2001 From: harsh-aghara Date: Tue, 9 Jun 2026 20:40:06 +0530 Subject: [PATCH 2/6] feat(client): add customUpCodes to Monitor type and validation schema --- client/src/Types/Monitor.ts | 1 + client/src/Validation/monitor.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 1e3dafb99..b2d90cb0b 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -71,6 +71,7 @@ export interface Monitor { uptimePercentage?: number; notifications: string[]; tags: string[]; + customUpCodes?: number[]; secret?: string; cpuAlertThreshold: number; cpuAlertCounter: number; diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 80d0d77c4..a6006d543 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -64,6 +64,10 @@ const httpSchema = baseSchema.extend({ .register(monitorStepRegistry, { step: 2 }), expectedValue: z.string().optional().register(monitorStepRegistry, { step: 2 }), jsonPath: z.string().optional().register(monitorStepRegistry, { step: 2 }), + customUpCodes: z + .array(z.number().min(100).max(599)) + .optional() + .register(monitorStepRegistry, { step: 2 }), ...geoCheckFields, }); From 1b1d8ea229cc05ed666ab8334cef654c7be3a89f Mon Sep 17 00:00:00 2001 From: harsh-aghara Date: Tue, 9 Jun 2026 20:40:29 +0530 Subject: [PATCH 3/6] feat(client): implement accepted status codes UI in create monitor form --- client/src/Hooks/useMonitorForm.ts | 2 + client/src/Pages/CreateMonitor/index.tsx | 63 ++++++++++++++++++++++++ client/src/locales/en.json | 4 ++ 3 files changed, 69 insertions(+) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 50fa62ec3..0ae463911 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -45,6 +45,7 @@ export const getMonitorDefaults = ( matchMethod: data?.matchMethod || "", expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", + customUpCodes: data?.customUpCodes || [], }; break; case "ping": @@ -138,6 +139,7 @@ export const getMonitorDefaults = ( matchMethod: "", expectedValue: "", jsonPath: "", + customUpCodes: [], }; } diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 94bed9355..2bda92e62 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from "react"; import { useEffect } from "react"; import { logger } from "@/Utils/logger"; +import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode"; import { useParams, useLocation, useNavigate } from "react-router"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -1090,6 +1091,68 @@ const CreateMonitorPage = () => { subtitle={t("pages.createMonitor.form.advanced.description")} rightContent={ + { + const selectedOptions = ALL_HTTP_STATUS_CODES.filter((option) => + field.value?.includes(option.id) + ); + return ( + + option.name} + isOptionEqualToValue={(option, value) => option.id === value.id} + onChange={( + _: unknown, + newValue: typeof ALL_HTTP_STATUS_CODES + ) => { + field.onChange(newValue?.map((v) => v.id) || []); + }} + fieldLabel={t( + "pages.createMonitor.form.advanced.option.customUpCodes.label" + )} + /> + {selectedOptions.length > 0 && ( + + {selectedOptions.map((option, index) => ( + + {option.name} + { + field.onChange( + field.value?.filter((id) => id !== option.id) + ); + }} + aria-label={t( + "pages.createMonitor.form.advanced.option.customUpCodes.removeAriaLabel" + )} + > + + + {index < selectedOptions.length - 1 && } + + ))} + + )} + + ); + }} + /> Date: Tue, 9 Jun 2026 23:45:13 +0530 Subject: [PATCH 4/6] feat: sync custom http status codes between frontend and backend - Add standard custom HTTP status codes (Cloudflare, AWS ELB, etc.) to both frontend and backend allowlists. - Enforce strict validation of custom status codes using Zod sets on both sides, ensuring they match perfectly. --- client/src/Utils/statusCode.ts | 10 +++++++++ client/src/Validation/monitor.ts | 8 ++++++- server/src/domain/monitors/monitor.types.ts | 24 ++++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/client/src/Utils/statusCode.ts b/client/src/Utils/statusCode.ts index 73ee2a892..7b5448d2e 100644 --- a/client/src/Utils/statusCode.ts +++ b/client/src/Utils/statusCode.ts @@ -123,6 +123,7 @@ export const ALL_HTTP_STATUS_CODES: HttpStatusCodeOption[] = [ { id: 417, name: "417 - Expectation Failed" }, { id: 418, name: "418 - I'm a Teapot" }, { id: 419, name: "419 - Page Expired (Laravel)" }, + { id: 420, name: "420 - Enhance Your Calm (Twitter)" }, { id: 421, name: "421 - Misdirected Request" }, { id: 422, name: "422 - Unprocessable Entity" }, { id: 423, name: "423 - Locked" }, @@ -132,7 +133,12 @@ export const ALL_HTTP_STATUS_CODES: HttpStatusCodeOption[] = [ { id: 428, name: "428 - Precondition Required" }, { id: 429, name: "429 - Too Many Requests" }, { id: 431, name: "431 - Request Header Fields Too Large" }, + { id: 440, name: "440 - Login Time-out (IIS)" }, + { id: 449, name: "449 - Retry With (IIS)" }, { id: 451, name: "451 - Unavailable For Legal Reasons" }, + { id: 460, name: "460 - Client Closed Connection (AWS ELB)" }, + { id: 463, name: "463 - X-Forwarded-For Too Large (AWS ELB)" }, + { id: 497, name: "497 - HTTP Request Sent to HTTPS Port (NGINX)" }, { id: 499, name: "499 - Client Closed Request (NGINX)" }, { id: 500, name: "500 - Internal Server Error" }, { id: 501, name: "501 - Not Implemented" }, @@ -143,6 +149,7 @@ export const ALL_HTTP_STATUS_CODES: HttpStatusCodeOption[] = [ { id: 506, name: "506 - Variant Also Negotiates" }, { id: 507, name: "507 - Insufficient Storage" }, { id: 508, name: "508 - Loop Detected" }, + { id: 509, name: "509 - Bandwidth Limit Exceeded (Apache)" }, { id: 510, name: "510 - Not Extended" }, { id: 511, name: "511 - Network Authentication Required" }, { id: 520, name: "520 - Web Server Returned an Unknown Error (Cloudflare)" }, @@ -153,4 +160,7 @@ export const ALL_HTTP_STATUS_CODES: HttpStatusCodeOption[] = [ { id: 525, name: "525 - SSL Handshake Failed (Cloudflare)" }, { id: 526, name: "526 - Invalid SSL Certificate (Cloudflare)" }, { id: 527, name: "527 - Railgun Error (Cloudflare)" }, + { id: 529, name: "529 - Site is overloaded" }, + { id: 530, name: "530 - Site is frozen (Cloudflare)" }, + { id: 561, name: "561 - Unauthorized (AWS ELB)" }, ]; diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index a6006d543..e0c4eae07 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import type { FieldPath } from "react-hook-form"; import { GeoContinents } from "@/Types/GeoCheck"; import { DnsRecordTypes, PageSpeedStrategies, type MonitorType } from "@/Types/Monitor"; +import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode"; // Wizard step a field is validated on. Attached inline to each field below so // the grouping lives next to the field definition; unannotated fields default @@ -53,6 +54,11 @@ const geoCheckFields = { }; // HTTP monitor schema +const httpStatusCodeSet = new Set(ALL_HTTP_STATUS_CODES.map((c) => c.id)); +const httpStatusCode = z.number().refine((code) => httpStatusCodeSet.has(code), { + message: "Must be a valid HTTP status code", +}); + const httpSchema = baseSchema.extend({ type: z.literal("http"), url: urlSchema, @@ -65,7 +71,7 @@ const httpSchema = baseSchema.extend({ expectedValue: z.string().optional().register(monitorStepRegistry, { step: 2 }), jsonPath: z.string().optional().register(monitorStepRegistry, { step: 2 }), customUpCodes: z - .array(z.number().min(100).max(599)) + .array(httpStatusCode) .optional() .register(monitorStepRegistry, { step: 2 }), ...geoCheckFields, diff --git a/server/src/domain/monitors/monitor.types.ts b/server/src/domain/monitors/monitor.types.ts index 37d477704..b614d3191 100644 --- a/server/src/domain/monitors/monitor.types.ts +++ b/server/src/domain/monitors/monitor.types.ts @@ -4,7 +4,29 @@ import type { GeoContinent, GroupedGeoCheck } from "@/domain/geo-checks/geo-chec export type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; import http from "node:http"; -export const HttpStatusCodes = Object.keys(http.STATUS_CODES).map(Number); +export const HttpStatusCodes = [ + ...Object.keys(http.STATUS_CODES).map(Number), + 419, // Page Expired (Laravel) + 420, // Enhance Your Calm (Twitter) + 440, // Login Time-out (IIS) + 449, // Retry With (IIS) + 460, // Client Closed Connection (AWS ELB) + 463, // X-Forwarded-For Too Large (AWS ELB) + 497, // HTTP Request Sent to HTTPS Port (NGINX) + 499, // Client Closed Request (NGINX) + 509, // Bandwidth Limit Exceeded (Apache) + 520, // Web Server Returned an Unknown Error (Cloudflare) + 521, // Web Server Is Down (Cloudflare) + 522, // Connection Timed Out (Cloudflare) + 523, // Origin Is Unreachable (Cloudflare) + 524, // A Timeout Occurred (Cloudflare) + 525, // SSL Handshake Failed (Cloudflare) + 526, // Invalid SSL Certificate (Cloudflare) + 527, // Railgun Error (Cloudflare) + 529, // Site is overloaded + 530, // Site is frozen (Cloudflare) + 561, // Unauthorized (AWS ELB) +]; export const HttpStatusCodeSet = new Set(HttpStatusCodes); export type HttpStatusCode = number; From 6d433fd0ac34349081656775ea8229ba0b9a70ce Mon Sep 17 00:00:00 2001 From: harsh-aghara Date: Thu, 18 Jun 2026 21:37:02 +0530 Subject: [PATCH 5/6] feat(client): polish customUpCodes ui --- client/src/Pages/CreateMonitor/index.tsx | 9 +++++++++ client/src/Utils/statusCode.ts | 3 +++ client/src/locales/en.json | 1 + 3 files changed, 13 insertions(+) diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 2bda92e62..465b9d15d 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -1117,6 +1117,15 @@ const CreateMonitorPage = () => { "pages.createMonitor.form.advanced.option.customUpCodes.label" )} /> + + {t( + "pages.createMonitor.form.advanced.option.customUpCodes.description" + )} + {selectedOptions.length > 0 && ( Date: Thu, 18 Jun 2026 22:41:34 +0530 Subject: [PATCH 6/6] test(server): add comprehensive edge-case tests for customUpCodes validation --- .../unit/validation/monitorValidation.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts index c08b8be84..08f516f99 100644 --- a/server/test/unit/validation/monitorValidation.test.ts +++ b/server/test/unit/validation/monitorValidation.test.ts @@ -243,6 +243,14 @@ describe("monitorValidation — customUpCodes", () => { expect(parsed.customUpCodes).toEqual([200, 301, 404, 503]); }); + it("accepts non-standard HTTP status codes (Cloudflare, AWS ELB, etc.)", () => { + const parsed = createMonitorBodyValidation.parse({ + ...baseHttpBody, + customUpCodes: [419, 420, 440, 449, 460, 463, 497, 499, 509, 520, 521, 522, 523, 524, 525, 526, 527, 529, 530, 561], + }); + expect(parsed.customUpCodes).toEqual([419, 420, 440, 449, 460, 463, 497, 499, 509, 520, 521, 522, 523, 524, 525, 526, 527, 529, 530, 561]); + }); + it("rejects invalid HTTP status codes", () => { expect(() => createMonitorBodyValidation.parse({ @@ -259,6 +267,33 @@ describe("monitorValidation — customUpCodes", () => { ).toThrow(); }); + it("rejects fractional or floating-point status codes", () => { + expect(() => + createMonitorBodyValidation.parse({ + ...baseHttpBody, + customUpCodes: [200.5], + }) + ).toThrow(); + }); + + it("rejects status codes below 100", () => { + expect(() => + createMonitorBodyValidation.parse({ + ...baseHttpBody, + customUpCodes: [99], + }) + ).toThrow(); + }); + + it("rejects valid-looking but mathematically unsupported status codes (e.g. 599)", () => { + expect(() => + createMonitorBodyValidation.parse({ + ...baseHttpBody, + customUpCodes: [599], // Not in Node's default list nor our expanded list + }) + ).toThrow(); + }); + it("defaults to an empty array when not provided", () => { const parsed = createMonitorBodyValidation.parse(baseHttpBody); expect(parsed.customUpCodes).toEqual([]); @@ -273,6 +308,13 @@ describe("monitorValidation — customUpCodes", () => { expect(parsed.customUpCodes).toEqual([200, 201]); }); + it("accepts non-standard HTTP status codes on edits", () => { + const parsed = editMonitorBodyValidation.parse({ + customUpCodes: [419, 420, 440, 449, 460, 463, 497, 499, 509, 520, 521, 522, 523, 524, 525, 526, 527, 529, 530, 561], + }); + expect(parsed.customUpCodes).toEqual([419, 420, 440, 449, 460, 463, 497, 499, 509, 520, 521, 522, 523, 524, 525, 526, 527, 529, 530, 561]); + }); + it("rejects invalid HTTP status codes on edits", () => { expect(() => editMonitorBodyValidation.parse({ @@ -295,6 +337,20 @@ describe("monitorValidation — customUpCodes", () => { expect(parsed.monitors[0].customUpCodes).toEqual([404]); }); + it("accepts non-standard HTTP status codes on imported HTTP monitors", () => { + const parsed = importMonitorsBodyValidation.parse({ + monitors: [ + { + ...baseHttpBody, + customUpCodes: [419, 420, 440, 449, 460, 463, 497, 499, 509, 520, 521, 522, 523, 524, 525, 526, 527, 529, 530, 561], + }, + ], + }); + expect(parsed.monitors[0].customUpCodes).toEqual([ + 419, 420, 440, 449, 460, 463, 497, 499, 509, 520, 521, 522, 523, 524, 525, 526, 527, 529, 530, 561, + ]); + }); + it("rejects invalid HTTP status codes on imported HTTP monitors", () => { expect(() => importMonitorsBodyValidation.parse({