84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
/**
|
|
* Response Body Validator
|
|
*
|
|
* Detects "soft failures" where an HTTP 200 response actually contains
|
|
* an error page. For example, Vite returns 200 OK with a "Blocked request"
|
|
* HTML page when a host isn't in allowedHosts.
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
interface SoftFailurePattern {
|
|
test: RegExp;
|
|
reason: string;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Known Patterns
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Patterns that indicate an HTTP 200 response is actually an error.
|
|
* Each pattern is tested against the first 2KB of the response body.
|
|
*/
|
|
const SOFT_FAILURE_PATTERNS: SoftFailurePattern[] = [
|
|
{
|
|
test: /Blocked request[\s\S]*?allowedHosts/,
|
|
reason: 'Vite allowedHosts block detected',
|
|
},
|
|
{
|
|
test: /Host header validation failed/,
|
|
reason: 'Vite host validation failed',
|
|
},
|
|
];
|
|
|
|
/** Maximum bytes to read from response body for validation */
|
|
const MAX_BODY_BYTES = 2048;
|
|
|
|
// =============================================================================
|
|
// Validation
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Check a successful HTTP response for known error patterns in the body.
|
|
*
|
|
* Clones the response before reading the body, so the original response
|
|
* can still be consumed by the caller if needed.
|
|
*
|
|
* @returns The reason string if a soft failure is detected, or null if clean
|
|
*/
|
|
export async function detectSoftFailure(response: Response): Promise<string | null> {
|
|
try {
|
|
const cloned = response.clone();
|
|
const reader = cloned.body?.getReader();
|
|
if (!reader) return null;
|
|
|
|
// Read up to MAX_BODY_BYTES
|
|
let text = '';
|
|
const decoder = new TextDecoder();
|
|
|
|
while (text.length < MAX_BODY_BYTES) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
text += decoder.decode(value, { stream: true });
|
|
}
|
|
|
|
reader.cancel();
|
|
|
|
// Trim to limit
|
|
const sample = text.slice(0, MAX_BODY_BYTES);
|
|
|
|
for (const pattern of SOFT_FAILURE_PATTERNS) {
|
|
if (pattern.test.test(sample)) {
|
|
return pattern.reason;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
// If body reading fails, don't block the health check
|
|
return null;
|
|
}
|
|
}
|