Request Handling
Deep dive into Frame-Master's masterRequest class and request lifecycle management.
Overview
The masterRequest class is the heart of Frame-Master's request handling system. It provides a powerful, chainable API for managing HTTP requests throughout their entire lifecycle—from initial processing to response delivery.
Every incoming HTTP request is wrapped in a masterRequest instance, which travels through three distinct phases (before_request, request, after_request) and provides plugins with comprehensive control over request/response processing, context management, cookie handling, and HTML transformation.
Key Capabilities
- Typed context sharing between plugins using TypeScript generics
- Encrypted and plain cookie management with automatic serialization
- Global value injection for client-side access
- Response building with chainable methods
- HTML rewriting control with HTMLRewriter integration
- Request state management across three phases
- Header manipulation at any lifecycle phase
API Reference
The masterRequest class is generic over ContextType, allowing you to define typed context data that persists across plugin hooks.
class masterRequest<ContextType extends Record<string, unknown> = {}> {// Core Propertiespublic request: Request; // Native Bun Request objectpublic URL: URL; // Parsed URL objectpublic currentState: RequestState; // "before_request" | "request" | "after_request"public readonly isAskingHTML: boolean; // True if Accept header includes text/htmlpublic readonly match?: RequestMatch; // Route matching informationpublic isStaticAsset: boolean; // True for static file requestspublic context: ContextType; // Typed context objectpublic serverConfig: FrameMasterConfig;public serverInstance: Bun.Server<undefined>;// Response ManagementsetResponse(body: BodyInit | null, init?: ResponseInit): thisunsetResponse(): voidisResponseSetted(): booleansendNow(): voidget response(): Response | undefinedsetHeader(name: string, value: string): this// Context ManagementgetContext<CustomContextType = undefined>(): CustomContextType extends undefined ? ContextType : CustomContextTypesetContext<CustomContextType = undefined>(context: CustomContextType extends undefined ? ContextType : CustomContextType): typeof context// Cookie ManagementsetCookie<T extends Record<string, unknown>>(name: string,data: T,options?: CookieOptions,dataOptions?: SetDataOptions): thisgetCookie<T extends Record<string, unknown>>(name: string,encrypted?: boolean): T | undefineddeleteCookie(name: string, options?: DeleteCookieOptions): this// Global Values InjectionsetGlobalValues<T extends Partial<typeof globalThis>>(values: T): thispreventGlobalValuesInjection(): thisisGlobalValuesInjectionPrevented(): boolean// HTML Rewriting ControlpreventRewrite(): thisisRewritePrevented(): boolean// UtilitiesformatParams(match: MatchedRoute["params"] | undefined): Params}
Type Definitions
Cookie Options
// Cookie creation optionsexport type CookieOptions = Omit<_webToken, "cookieName"> & {encrypted?: boolean; // Whether to encrypt the cookie data (default: false)// From webToken:// - sameSite?: "Lax" | "Strict" | "None"// - httpOnly?: boolean// - secure?: boolean// - path?: string// - domain?: string// - maxAge?: number (in seconds)// - expires?: Date};// Cookie deletion optionsexport type DeleteCookieOptions = {path?: string;domain?: string;secure?: boolean;sameSite?: "Lax" | "Strict" | "None";httpOnly?: boolean;};// SetDataOptions for encrypted cookiestype SetDataOptions = {ttl?: number; // Time to live in seconds};
Request Types
// Request lifecycle statesexport type RequestState = "before_request" | "request" | "after_request";
Response Building
Build and manage HTTP responses using the chainable response API. Responses must be set during the request phase.
Setting Responses
import type { FrameMasterPlugin } from "frame-master/plugin/types";export function apiPlugin(): FrameMasterPlugin {return {name: "api-plugin",router: {request: async (master) => {// Check if this is an API request or if the response is already settedif (!master.URL.pathname.startsWith("/api") || master.isResponseSetted()) return;// JSON responseif (master.URL.pathname === "/api/users") {const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];master.setResponse(JSON.stringify(users), {headers: {"Content-Type": "application/json",},}).sendNow(); // Skip other request plugins}// HTML responseelse if (master.URL.pathname === "/api/status") {master.setResponse("<h1>Server is running</h1>", {status: 200,headers: { "Content-Type": "text/html" },}).sendNow();}// Error responseelse if (master.URL.pathname === "/api/error") {master.setResponse(JSON.stringify({ error: "Resource not found" }),{status: 404,headers: { "Content-Type": "application/json" },});}},},};}
Response State Rules
- Responses can only be set in the 'request' phase
- Once set, a response cannot be changed unless unsetResponse() is called
- sendNow() skips remaining request plugins but still runs after_request hooks
- Headers can be added in any phase using setHeader()
Header Management
router: {request: async (master) => {// Set headers when creating responsemaster.setResponse("Hello World", {headers: {"X-Custom-Header": "value","Content-Type": "text/plain",},});// Or add headers separately (works in any phase)master.setHeader("X-Request-ID", crypto.randomUUID());},after_request: async (master) => {// Add security headers after response is readymaster.setHeader("X-Frame-Options", "DENY").setHeader("X-Content-Type-Options", "nosniff").setHeader("X-XSS-Protection", "1; mode=block");},}
Response Utilities
router: {request: async (master) => {// Check if response already setif (master.isResponseSetted()) {console.log("Response already handled by previous plugin");return;}// Set responsemaster.setResponse("Initial response");// Later, if you need to change it...master.unsetResponse();master.setResponse("New response");},}
Context Management
Context provides a typed way to share data between plugins throughout the request lifecycle. Define your context type for full TypeScript support.
Typed Context
// Define your context typetype MyAppContext = {user?: {id: string;email: string;roles: string[];};requestId: string;startTime: number;metadata: Record<string, unknown>;};// Authentication plugin sets user in contextexport function authPlugin(): FrameMasterPlugin {return {name: "auth-plugin",router: {before_request: async (master) => {const token = master.request.headers.get("Authorization");if (token) {const user = await validateToken(token);// Set typed contextmaster.setContext<MyAppContext>({user,requestId: crypto.randomUUID(),startTime: Date.now(),metadata: {},});}},},};}// Another plugin accesses the contextexport function loggingPlugin(): FrameMasterPlugin {return {name: "logging-plugin",router: {request: async (master) => {// Get typed contextconst context = master.getContext<MyAppContext>();if (context.user) {console.log(`Request by user: ${context.user.email}`);console.log(`Request ID: ${context.requestId}`);}},after_request: async (master) => {const context = master.getContext<MyAppContext>();const duration = Date.now() - context.startTime;console.log(`Request completed in ${duration}ms`);},},};}async function validateToken(token: string) {// Your token validation logicreturn {id: "user-123",roles: ["user", "admin"],};}
Context Merging
Context values are merged when you call setContext(), allowing multiple plugins to contribute to the shared context.
router: {before_request: async (master) => {// First plugin sets initial contextmaster.setContext({ requestId: "123" });// Second plugin adds to contextmaster.setContext({ userId: "user-456" });// Context now contains: { requestId: "123", userId: "user-456" }const ctx = master.getContext();console.log(ctx); // { requestId: "123", userId: "user-456" }// Later values override earlier onesmaster.setContext({ requestId: "789" });// Context now: { requestId: "789", userId: "user-456" }},}
Cookie Management
Frame-Master provides comprehensive cookie management with support for both encrypted and plain JSON cookies, built on the @shpaw415/webtoken↗ library.
Setting Cookies
router: {request: async (master) => {// Plain JSON cookie (not encrypted)master.setCookie("preferences",{theme: "dark",language: "en",notifications: true,},{maxAge: 60 * 60 * 24 * 30, // 30 days in secondspath: "/",sameSite: "Lax",httpOnly: true,});// Encrypted cookie (secure sensitive data)master.setCookie("session",{userId: "user-123",roles: ["admin", "user"],},{encrypted: true, // Enable encryptionmaxAge: 60 * 60 * 24 * 7, // 7 dayssecure: true, // HTTPS onlyhttpOnly: true, // Not accessible via JavaScriptsameSite: "Strict",},{ttl: 60 * 60 * 24 * 7, // Data TTL (7 days)});// Set response (cookies are applied automatically)master.setResponse("Cookie set successfully");},}
Cookie Application
after_request phase completes. You can set cookies in any phase, and they'll be queued and applied at the end.Reading Cookies
router: {request: async (master) => {// Read plain JSON cookieconst preferences = master.getCookie<{theme: string;language: string;notifications: boolean;}>("preferences", false); // false = not encryptedif (preferences) {console.log(`User theme: ${preferences.theme}`);}// Read encrypted cookieconst session = master.getCookie<{userId: string;email: string;roles: string[];}>("session", true); // true = encryptedif (session) {// Use session datamaster.setContext({ user: session });} else {// No valid sessionmaster.setResponse("Unauthorized", { status: 401 });}},}
Deleting Cookies
router: {request: async (master) => {// Delete a cookie (match the original options)master.deleteCookie("session", {path: "/",secure: true,httpOnly: true,sameSite: "Strict",});// Simple delete with defaultsmaster.deleteCookie("preferences");// Logout exampleif (master.URL.pathname === "/logout") {master.deleteCookie("session", {path: "/",secure: true,httpOnly: true,}).setResponse("Logged out successfully", {status: 302,headers: { Location: "/login" },});}},}
Cookie Caching
Cookies are automatically cached during the request lifecycle. The first getCookie() call parses and caches the cookie, subsequent calls return the cached value for better performance.
Global Values Injection
Inject server-side values into the global scope for client-side access. This is useful for configuration, feature flags, initial data, and more.
Injecting Global Values
// 1. Declare global typesdeclare global {var __APP_CONFIG__: {apiUrl: string;features: string[];} | undefined;var __USER_DATA__: {id: string;name: string;} | undefined;var __BUILD_INFO__: {version: string;buildTime: string;} | undefined;}// 2. Inject values in pluginexport function configPlugin(): FrameMasterPlugin {return {name: "config-plugin",router: {before_request: async (master) => {// Inject configurationmaster.setGlobalValues({__APP_CONFIG__: {apiUrl: process.env.API_URL || "http://localhost:3000",features: ["auth", "payments", "notifications"],},__BUILD_INFO__: {version: "1.0.0",buildTime: new Date().toISOString(),},});// Inject user-specific dataconst session = master.getCookie("session", true);if (session) {master.setGlobalValues({__USER_DATA__: {id: session.userId,name: session.name,},});}},},};}// 3. Access in client-side code// In your React component or client script:export function MyComponent() {const config = globalThis.__APP_CONFIG__;const user = globalThis.__USER_DATA__;return (<div><h1>Welcome {user?.name}</h1><p>API: {config?.apiUrl}</p><p>Features: {config?.features.join(", ")}</p></div>);}
Serialization Requirements
- Global values must be JSON-serializable
- Functions, symbols, and circular references cannot be injected
- undefined values are converted to the string 'undefined'
- Use typed declarations for full TypeScript support
Preventing Global Injection
router: {request: async (master) => {// For API routes, prevent unnecessary global value injectionif (master.URL.pathname.startsWith("/api")) {master.preventGlobalValuesInjection();// API response without HTML or global valuesmaster.setResponse(JSON.stringify({ data: "response" }), {headers: { "Content-Type": "application/json" },});}// Check if injection is preventedif (master.isGlobalValuesInjectionPrevented()) {console.log("Global values will not be injected");}},}
Performance Tip
preventGlobalValuesInjection() for non-HTML responses (JSON, files, etc.) to avoid unnecessary processing.HTML Rewriting Control
Frame-Master uses Bun's HTMLRewriter↗ to transform HTML responses. Control when and how HTML rewriting occurs.
HTML Rewrite Phase
After the request phase, if the response is HTML:
- Global values are injected as
<script>tags - Plugin
html_rewritehooks are executed - HTML is transformed by the HTMLRewriter
export function htmlTransformPlugin(): FrameMasterPlugin {return {name: "html-transform",router: {html_rewrite: {// Initialize context for rewritinginitContext: (master) => {return {userId: master.getContext().userId,timestamp: Date.now(),};},// Transform HTMLrewrite: async (rewriter, master, context) => {// Add attributes to elementsrewriter.on("body", {element(el) {el.setAttribute("data-user", context.userId);el.setAttribute("data-timestamp", context.timestamp.toString());},});// Inject analytics scriptrewriter.on("head", {element(el) {el.append(`<script>console.log('User:', '${context.userId}');console.log('Page loaded at:', ${context.timestamp});</script>`, { html: true });},});// Modify specific elementsrewriter.on("div.protected", {element(el) {if (!context.userId) {el.setInnerContent("Please log in to view this content");}},});},},},};}
Preventing HTML Rewrite
router: {request: async (master) => {// For raw HTML responses, prevent rewritingif (master.URL.searchParams.get("raw") === "true") {master.preventRewrite();}// For API documentation, skip global injection and rewritingif (master.URL.pathname.startsWith("/docs/api")) {master.preventGlobalValuesInjection().preventRewrite();}// Check if rewriting is preventedif (master.isRewritePrevented()) {console.log("HTML rewriting is disabled for this request");}},}
When to Prevent Rewriting
- Raw HTML files that shouldn't be modified
- Email templates or static HTML content
- Performance-critical responses unless a cache system is used
- Third-party HTML content
Request State Management
Every request progresses through three states. Understanding these states is crucial for proper plugin implementation.
Request States
// State 1: before_request// - Initialize context// - Set global values// - Perform authentication/authorization// - Can NOT set response yetbefore_request: async (master) => {console.log("State:", master.currentState); // "before_request"// Setup is allowedmaster.setContext({ requestId: crypto.randomUUID() });master.setGlobalValues({ __REQUEST_ID__: "123" });// Response setting throws error// master.setResponse("..."); // ❌ Error!}// State 2: request// - Handle route logic// - Build and set response// - Can call sendNow() to skip other pluginsrequest: async (master) => {console.log("State:", master.currentState); // "request"// Response building is allowedmaster.setResponse("Hello World");// Skip remaining pluginsmaster.sendNow();}// State 3: after_request// - Response is finalized// - Add/modify headers// - Logging and cleanup// - Can NOT change response bodyafter_request: async (master) => {console.log("State:", master.currentState); // "after_request"// Header modification allowedmaster.setHeader("X-Response-Time", Date.now().toString());// Access final responseconst response = master.response;console.log("Status:", response?.status);// Body modification not allowed// master.setResponse("..."); // ❌ Error!}
State Transition Flow
// The request flows through states in order://// 1. before_request (all plugins)// ↓// 2. request (plugins until sendNow() or all complete)// ↓// 3. toResponse() - internal method converts to Response// ↓// 4. HTML Rewrite (if HTML response)// ↓// 5. after_request (all plugins)// ↓// 6. Cookie application// ↓// 7. Response sent to clientexport function stateAwarePlugin(): FrameMasterPlugin {return {name: "state-aware",router: {before_request: async (master) => {console.log("🔵 Phase 1: Preparing request");master.setContext({ phase: "before_request" });},request: async (master) => {console.log("🟢 Phase 2: Handling request");const ctx = master.getContext();console.log("Previous phase:", ctx.phase);if (!master.isResponseSetted()) {master.setResponse("Response from plugin");}},after_request: async (master) => {console.log("🟡 Phase 3: Finalizing request");const response = master.response;console.log("Final status:", response?.status);// Add timing headersmaster.setHeader("X-Plugin", "state-aware");},},};}
Practical Examples
Authentication Middleware
type AuthContext = {user?: {id: string;email: string;roles: string[];};isAuthenticated: boolean;};export function authMiddleware(): FrameMasterPlugin {return {name: "auth-middleware",router: {before_request: async (master) => {// Get session cookieconst session = master.getCookie<{userId: string;email: string;roles: string[];expiresAt: number;}>("session", true); // encryptedif (session && session.expiresAt > Date.now()) {// Valid sessionmaster.setContext<AuthContext>({user: {id: session.userId,email: session.email,roles: session.roles,},isAuthenticated: true,});// Inject user data for clientmaster.setGlobalValues({__USER__: {id: session.userId,email: session.email,},});} else {// No valid sessionmaster.setContext<AuthContext>({isAuthenticated: false,});}},request: async (master) => {const ctx = master.getContext<AuthContext>();const pathname = master.URL.pathname;// Protect routesconst protectedRoutes = ["/dashboard", "/profile", "/settings"];const isProtected = protectedRoutes.some((route) =>pathname.startsWith(route));if (isProtected && !ctx.isAuthenticated) {// Redirect to loginmaster.setResponse("Redirecting to login...", {status: 302,headers: {Location: `/login?redirect=${encodeURIComponent(pathname)}`,},}).sendNow();return;}// Check role-based accessif (pathname.startsWith("/admin")) {if (!ctx.user?.roles.includes("admin")) {master.setResponse("Forbidden", { status: 403 }).sendNow();}}},},};}
API Route Handler
export function apiRouterPlugin(): FrameMasterPlugin {return {name: "api-router",router: {request: async (master) => {const { pathname } = master.URL;// Only handle /api routesif (!pathname.startsWith("/api")) return;// Parse request body for POST/PUTlet body: any;if (["POST", "PUT", "PATCH"].includes(master.request.method)) {try {body = await master.request.json();} catch (e) {master.setResponse(JSON.stringify({ error: "Invalid JSON" }),{status: 400,headers: { "Content-Type": "application/json" },}).sendNow();return;}}// Route handlingswitch (true) {case pathname === "/api/users" && master.request.method === "GET": {const users = await getUsers();master.setResponse(JSON.stringify({ data: users }), {headers: { "Content-Type": "application/json" },}).sendNow();break;}case pathname === "/api/users" && master.request.method === "POST": {const newUser = await createUser(body);master.setResponse(JSON.stringify({ data: newUser }), {status: 201,headers: { "Content-Type": "application/json" },}).sendNow();break;}case pathname.match(/^\/api\/users\/([^/]+)$/)?.length! > 0 &&master.request.method === "GET": {const userId = pathname.split("/").pop();const user = await getUser(userId!);if (!user) {master.setResponse(JSON.stringify({ error: "User not found" }),{status: 404,headers: { "Content-Type": "application/json" },}).sendNow();} else {master.setResponse(JSON.stringify({ data: user }), {headers: { "Content-Type": "application/json" },}).sendNow();}break;}default: {master.setResponse(JSON.stringify({ error: "Route not found" }),{status: 404,headers: { "Content-Type": "application/json" },}).sendNow();}}// Prevent global injection for API routesmaster.preventGlobalValuesInjection().preventRewrite();},},};}// Mock database functionsasync function getUsers() {return [{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }];}async function createUser(data: any) {return { id: crypto.randomUUID(), ...data };}async function getUser(id: string) {const users = await getUsers();return users.find((u) => u.id === id);}
Request Logging
type LogContext = {requestId: string;startTime: number;userAgent: string;ip: string;};export function loggingPlugin(): FrameMasterPlugin {return {name: "request-logger",router: {before_request: async (master) => {// Generate request ID and start timerconst requestId = crypto.randomUUID();const startTime = Date.now();master.setContext<LogContext>({requestId,startTime,userAgent: master.request.headers.get("User-Agent") || "unknown",ip: master.serverInstance.requestIP(master.request)?.address || "unknown",});// Log request startconsole.log(`[${requestId}] ${master.request.method} ${master.URL.pathname}`);},after_request: async (master) => {const ctx = master.getContext<LogContext>();const duration = Date.now() - ctx.startTime;const status = master.response?.status || 0;// Log request completionconsole.log(`[${ctx.requestId}] ${status} - ${duration}ms`);// Add request ID headermaster.setHeader("X-Request-ID", ctx.requestId);// Detailed logging for errorsif (status >= 400) {console.error(`[${ctx.requestId}] Error Response:`, {method: master.request.method,path: master.URL.pathname,status,duration: `${duration}ms`,userAgent: ctx.userAgent,ip: ctx.ip,});}// Performance warningif (duration > 1000) {console.warn(`[${ctx.requestId}] Slow request: ${duration}ms`);}},},};}
Best Practices
Plugin Ordering
Plugin execution order matters. Place plugins in the correct order in your config:
const config: FrameMasterConfig = {plugins: [loggingPlugin(), // 1. First: Set up request trackingauthPlugin(), // 2. Then: Authenticate userrateLimitPlugin(), // 3. Then: Check rate limitscorsPlugin(), // 4. Then: Handle CORSapiRouter(), // 5. Then: Handle API routespageRouter(), // 6. Finally: Handle page routes],};// Execution flow:// before_request: logging → auth → rateLimit → cors → api → page// request: logging → auth → rateLimit → cors → api → page (stops at first sendNow())// after_request: logging → auth → rateLimit → cors → api → page
Error Handling
export function safePlugin(): FrameMasterPlugin {return {name: "safe-plugin",router: {request: async (master) => {try {// Risky operationconst data = await fetchExternalAPI();master.setResponse(JSON.stringify(data));} catch (error) {// Log errorconsole.error("External API error:", error);// Return error responsemaster.setResponse(JSON.stringify({error: "Service temporarily unavailable",requestId: master.getContext().requestId,}),{status: 503,headers: { "Content-Type": "application/json" },});}},},};}async function fetchExternalAPI() {throw new Error("API down");}
Performance Tips
- Call preventGlobalValuesInjection() for non-HTML responses
- Call preventRewrite() when HTML rewriting isn't needed
- Use cookie caching - getCookie() caches results automatically
- Call sendNow() to skip unnecessary plugin execution
- Minimize context data - only store what's needed
- Avoid heavy operations in before_request - defer to request phase
Type Safety
// Define types for your entire application// Context typestype AppContext = {requestId: string;user?: User;session?: SessionData;};// Cookie typestype SessionCookie = {userId: string;expiresAt: number;roles: string[];};type PreferencesCookie = {theme: "light" | "dark";language: string;};// Use types consistentlyexport function typedPlugin(): FrameMasterPlugin {return {name: "typed-plugin",router: {before_request: async (master) => {// Typed contextmaster.setContext<AppContext>({requestId: crypto.randomUUID(),});},request: async (master) => {// Typed cookie accessconst session = master.getCookie<SessionCookie>("session", true);const prefs = master.getCookie<PreferencesCookie>("prefs");// Typed context accessconst ctx = master.getContext<AppContext>();// TypeScript ensures type safetyif (session && session.expiresAt > Date.now()) {ctx.session = session;}},},};}
Advanced Topics
Conditional HTML Rewriting
export function conditionalRewritePlugin(): FrameMasterPlugin {return {name: "conditional-rewrite",router: {request: async (master) => {// Conditionally prevent rewriting based on requestconst skipRewrite =master.URL.searchParams.get("raw") === "true" ||master.request.headers.get("X-Skip-Transform") === "true" ||master.URL.pathname.includes("/embed");if (skipRewrite) {master.preventRewrite();}},html_rewrite: {rewrite: async (rewriter, master, context) => {// This won't run if preventRewrite() was called// Conditional transformation based on userconst ctx = master.getContext<{ user?: any }>();if (ctx.user?.roles.includes("admin")) {// Add admin toolbarrewriter.on("body", {element(el) {el.prepend('<div class="admin-toolbar">Admin Mode</div>', {html: true,});},});}// Feature flag-based transformationconst features = master.getCookie("features");if (features?.betaUI) {rewriter.on('link[href="/styles.css"]', {element(el) {el.setAttribute("href", "/styles-beta.css");},});}},},},};}
Multi-Plugin Coordination
// Plugin 1: Sets up shared dataexport function dataProviderPlugin(): FrameMasterPlugin {return {name: "data-provider",router: {before_request: async (master) => {const config = await loadConfig();master.setContext({ sharedConfig: config });},},};}// Plugin 2: Uses shared dataexport function dataConsumerPlugin(): FrameMasterPlugin {return {name: "data-consumer",router: {request: async (master) => {const ctx = master.getContext<{ sharedConfig?: any }>();if (ctx.sharedConfig) {// Use config from data-provider pluginconsole.log("Using config:", ctx.sharedConfig);}},},};}// Plugin 3: Coordination via response checkingexport function fallbackPlugin(): FrameMasterPlugin {return {name: "fallback",router: {request: async (master) => {// Only handle if no other plugin set responseif (!master.isResponseSetted()) {master.setResponse("Default fallback response");}},},};}async function loadConfig() {return { apiUrl: "https://api.example.com" };}
Streaming Responses
export function streamingPlugin(): FrameMasterPlugin {return {name: "streaming",router: {request: async (master) => {if (master.URL.pathname === "/stream") {// Create a ReadableStreamconst stream = new ReadableStream({async start(controller) {for (let i = 0; i < 10; i++) {await new Promise((resolve) => setTimeout(resolve, 100));controller.enqueue(new TextEncoder().encode(`Chunk ${i}\n`));}controller.close();},});master.setResponse(stream, {headers: {"Content-Type": "text/plain","Transfer-Encoding": "chunked",},}).sendNow();}},},};}
Troubleshooting
Common Errors
ResponseAlreadySetError
Cause: Attempting to call setResponse() when a response is already set.
Solution: Check isResponseSetted() before setting, or call unsetResponse() first.
Cannot set response in [phase]
Cause: Calling setResponse() outside the request phase.
Solution: Move response building logic to the request hook.
Cookies not persisting
Cause: Cookie options don't match between setting and reading.
Solution: Ensure path, domain, and other options are consistent.
Global values not appearing
Cause: Values not JSON-serializable, or preventGlobalValuesInjection() was called.
Solution: Check serialization and ensure injection isn't prevented. Verify response is HTML.
Debugging Tips
export function debugPlugin(): FrameMasterPlugin {return {name: "debug",router: {before_request: async (master) => {console.log("=== REQUEST START ===");console.log("Method:", master.request.method);console.log("URL:", master.URL.href);console.log("Headers:", Object.fromEntries(master.request.headers));console.log("State:", master.currentState);},request: async (master) => {console.log("=== REQUEST PHASE ===");console.log("Context:", master.getContext());console.log("Response set:", master.isResponseSetted());console.log("Cookies:", {session: master.getCookie("session", true),prefs: master.getCookie("prefs"),});},after_request: async (master) => {console.log("=== REQUEST END ===");console.log("Response:", {status: master.response?.status,headers: Object.fromEntries(master.response?.headers || []),});console.log("Global values:", master.globalDataInjection.rawData);console.log("Rewrite prevented:", master.isRewritePrevented());console.log("Global injection prevented:",master.isGlobalValuesInjectionPrevented());},},};}
