Documentation

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.

masterRequest API
class masterRequest<ContextType extends Record<string, unknown> = {}> {
// Core Properties
public request: Request; // Native Bun Request object
public URL: URL; // Parsed URL object
public currentState: RequestState; // "before_request" | "request" | "after_request"
public readonly isAskingHTML: boolean; // True if Accept header includes text/html
public readonly match?: RequestMatch; // Route matching information
public isStaticAsset: boolean; // True for static file requests
public context: ContextType; // Typed context object
public serverConfig: FrameMasterConfig;
public serverInstance: Bun.Server<undefined>;
// Response Management
setResponse(body: BodyInit | null, init?: ResponseInit): this
unsetResponse(): void
isResponseSetted(): boolean
sendNow(): void
get response(): Response | undefined
setHeader(name: string, value: string): this
// Context Management
getContext<CustomContextType = undefined>(): CustomContextType extends undefined ? ContextType : CustomContextType
setContext<CustomContextType = undefined>(context: CustomContextType extends undefined ? ContextType : CustomContextType): typeof context
// Cookie Management
setCookie<T extends Record<string, unknown>>(
name: string,
data: T,
options?: CookieOptions,
dataOptions?: SetDataOptions
): this
getCookie<T extends Record<string, unknown>>(
name: string,
encrypted?: boolean
): T | undefined
deleteCookie(name: string, options?: DeleteCookieOptions): this
// Global Values Injection
setGlobalValues<T extends Partial<typeof globalThis>>(values: T): this
preventGlobalValuesInjection(): this
isGlobalValuesInjectionPrevented(): boolean
// HTML Rewriting Control
preventRewrite(): this
isRewritePrevented(): boolean
// Utilities
formatParams(match: MatchedRoute["params"] | undefined): Params
}

Type Definitions

Cookie Options

Cookie Types
// Cookie creation options
export 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 options
export type DeleteCookieOptions = {
path?: string;
domain?: string;
secure?: boolean;
sameSite?: "Lax" | "Strict" | "None";
httpOnly?: boolean;
};
// SetDataOptions for encrypted cookies
type SetDataOptions = {
ttl?: number; // Time to live in seconds
};

Request Types

Request Types
// Request lifecycle states
export 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

Response Building Examples
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 setted
if (!master.URL.pathname.startsWith("/api") || master.isResponseSetted()) return;
// JSON response
if (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 response
else if (master.URL.pathname === "/api/status") {
master
.setResponse("<h1>Server is running</h1>", {
status: 200,
headers: { "Content-Type": "text/html" },
})
.sendNow();
}
// Error response
else 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

Header Management
router: {
request: async (master) => {
// Set headers when creating response
master.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 ready
master
.setHeader("X-Frame-Options", "DENY")
.setHeader("X-Content-Type-Options", "nosniff")
.setHeader("X-XSS-Protection", "1; mode=block");
},
}

Response Utilities

Response Utilities
router: {
request: async (master) => {
// Check if response already set
if (master.isResponseSetted()) {
console.log("Response already handled by previous plugin");
return;
}
// Set response
master.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

Typed Context Example
// Define your context type
type MyAppContext = {
user?: {
id: string;
email: string;
roles: string[];
};
requestId: string;
startTime: number;
metadata: Record<string, unknown>;
};
// Authentication plugin sets user in context
export 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 context
master.setContext<MyAppContext>({
user,
requestId: crypto.randomUUID(),
startTime: Date.now(),
metadata: {},
});
}
},
},
};
}
// Another plugin accesses the context
export function loggingPlugin(): FrameMasterPlugin {
return {
name: "logging-plugin",
router: {
request: async (master) => {
// Get typed context
const 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 logic
return {
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.

Context Merging
router: {
before_request: async (master) => {
// First plugin sets initial context
master.setContext({ requestId: "123" });
// Second plugin adds to context
master.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 ones
master.setContext({ requestId: "789" });
// Context now: { requestId: "789", userId: "user-456" }
},
}

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

Global Values Example
// 1. Declare global types
declare 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 plugin
export function configPlugin(): FrameMasterPlugin {
return {
name: "config-plugin",
router: {
before_request: async (master) => {
// Inject configuration
master.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 data
const 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

Preventing Global Injection
router: {
request: async (master) => {
// For API routes, prevent unnecessary global value injection
if (master.URL.pathname.startsWith("/api")) {
master.preventGlobalValuesInjection();
// API response without HTML or global values
master.setResponse(JSON.stringify({ data: "response" }), {
headers: { "Content-Type": "application/json" },
});
}
// Check if injection is prevented
if (master.isGlobalValuesInjectionPrevented()) {
console.log("Global values will not be injected");
}
},
}
💡

Performance Tip

Call 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:

  1. Global values are injected as <script> tags
  2. Plugin html_rewrite hooks are executed
  3. HTML is transformed by the HTMLRewriter
HTML Rewriting Example
export function htmlTransformPlugin(): FrameMasterPlugin {
return {
name: "html-transform",
router: {
html_rewrite: {
// Initialize context for rewriting
initContext: (master) => {
return {
userId: master.getContext().userId,
timestamp: Date.now(),
};
},
// Transform HTML
rewrite: async (rewriter, master, context) => {
// Add attributes to elements
rewriter.on("body", {
element(el) {
el.setAttribute("data-user", context.userId);
el.setAttribute("data-timestamp", context.timestamp.toString());
},
});
// Inject analytics script
rewriter.on("head", {
element(el) {
el.append(`
<script>
console.log('User:', '${context.userId}');
console.log('Page loaded at:', ${context.timestamp});
</script>
`, { html: true });
},
});
// Modify specific elements
rewriter.on("div.protected", {
element(el) {
if (!context.userId) {
el.setInnerContent("Please log in to view this content");
}
},
});
},
},
},
};
}

Preventing HTML Rewrite

Preventing HTML Rewrite
router: {
request: async (master) => {
// For raw HTML responses, prevent rewriting
if (master.URL.searchParams.get("raw") === "true") {
master.preventRewrite();
}
// For API documentation, skip global injection and rewriting
if (master.URL.pathname.startsWith("/docs/api")) {
master
.preventGlobalValuesInjection()
.preventRewrite();
}
// Check if rewriting is prevented
if (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

Request States
// State 1: before_request
// - Initialize context
// - Set global values
// - Perform authentication/authorization
// - Can NOT set response yet
before_request: async (master) => {
console.log("State:", master.currentState); // "before_request"
// Setup is allowed
master.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 plugins
request: async (master) => {
console.log("State:", master.currentState); // "request"
// Response building is allowed
master.setResponse("Hello World");
// Skip remaining plugins
master.sendNow();
}
// State 3: after_request
// - Response is finalized
// - Add/modify headers
// - Logging and cleanup
// - Can NOT change response body
after_request: async (master) => {
console.log("State:", master.currentState); // "after_request"
// Header modification allowed
master.setHeader("X-Response-Time", Date.now().toString());
// Access final response
const response = master.response;
console.log("Status:", response?.status);
// Body modification not allowed
// master.setResponse("..."); // ❌ Error!
}

State Transition Flow

State Transition Example
// 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 client
export 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 headers
master.setHeader("X-Plugin", "state-aware");
},
},
};
}

Practical Examples

Authentication Middleware

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 cookie
const session = master.getCookie<{
userId: string;
email: string;
roles: string[];
expiresAt: number;
}>("session", true); // encrypted
if (session && session.expiresAt > Date.now()) {
// Valid session
master.setContext<AuthContext>({
user: {
id: session.userId,
email: session.email,
roles: session.roles,
},
isAuthenticated: true,
});
// Inject user data for client
master.setGlobalValues({
__USER__: {
id: session.userId,
email: session.email,
},
});
} else {
// No valid session
master.setContext<AuthContext>({
isAuthenticated: false,
});
}
},
request: async (master) => {
const ctx = master.getContext<AuthContext>();
const pathname = master.URL.pathname;
// Protect routes
const protectedRoutes = ["/dashboard", "/profile", "/settings"];
const isProtected = protectedRoutes.some((route) =>
pathname.startsWith(route)
);
if (isProtected && !ctx.isAuthenticated) {
// Redirect to login
master
.setResponse("Redirecting to login...", {
status: 302,
headers: {
Location: `/login?redirect=${encodeURIComponent(pathname)}`,
},
})
.sendNow();
return;
}
// Check role-based access
if (pathname.startsWith("/admin")) {
if (!ctx.user?.roles.includes("admin")) {
master
.setResponse("Forbidden", { status: 403 })
.sendNow();
}
}
},
},
};
}

API Route Handler

API Route Handler
export function apiRouterPlugin(): FrameMasterPlugin {
return {
name: "api-router",
router: {
request: async (master) => {
const { pathname } = master.URL;
// Only handle /api routes
if (!pathname.startsWith("/api")) return;
// Parse request body for POST/PUT
let 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 handling
switch (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 routes
master.preventGlobalValuesInjection().preventRewrite();
},
},
};
}
// Mock database functions
async 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

Request Logging Plugin
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 timer
const 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 start
console.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 completion
console.log(
`[${ctx.requestId}] ${status} - ${duration}ms`
);
// Add request ID header
master.setHeader("X-Request-ID", ctx.requestId);
// Detailed logging for errors
if (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 warning
if (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:

Plugin Ordering
const config: FrameMasterConfig = {
plugins: [
loggingPlugin(), // 1. First: Set up request tracking
authPlugin(), // 2. Then: Authenticate user
rateLimitPlugin(), // 3. Then: Check rate limits
corsPlugin(), // 4. Then: Handle CORS
apiRouter(), // 5. Then: Handle API routes
pageRouter(), // 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

Error Handling
export function safePlugin(): FrameMasterPlugin {
return {
name: "safe-plugin",
router: {
request: async (master) => {
try {
// Risky operation
const data = await fetchExternalAPI();
master.setResponse(JSON.stringify(data));
} catch (error) {
// Log error
console.error("External API error:", error);
// Return error response
master.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

Type Safety
// Define types for your entire application
// Context types
type AppContext = {
requestId: string;
user?: User;
session?: SessionData;
};
// Cookie types
type SessionCookie = {
userId: string;
expiresAt: number;
roles: string[];
};
type PreferencesCookie = {
theme: "light" | "dark";
language: string;
};
// Use types consistently
export function typedPlugin(): FrameMasterPlugin {
return {
name: "typed-plugin",
router: {
before_request: async (master) => {
// Typed context
master.setContext<AppContext>({
requestId: crypto.randomUUID(),
});
},
request: async (master) => {
// Typed cookie access
const session = master.getCookie<SessionCookie>("session", true);
const prefs = master.getCookie<PreferencesCookie>("prefs");
// Typed context access
const ctx = master.getContext<AppContext>();
// TypeScript ensures type safety
if (session && session.expiresAt > Date.now()) {
ctx.session = session;
}
},
},
};
}

Advanced Topics

Conditional HTML Rewriting

Conditional Rewriting
export function conditionalRewritePlugin(): FrameMasterPlugin {
return {
name: "conditional-rewrite",
router: {
request: async (master) => {
// Conditionally prevent rewriting based on request
const 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 user
const ctx = master.getContext<{ user?: any }>();
if (ctx.user?.roles.includes("admin")) {
// Add admin toolbar
rewriter.on("body", {
element(el) {
el.prepend('<div class="admin-toolbar">Admin Mode</div>', {
html: true,
});
},
});
}
// Feature flag-based transformation
const features = master.getCookie("features");
if (features?.betaUI) {
rewriter.on('link[href="/styles.css"]', {
element(el) {
el.setAttribute("href", "/styles-beta.css");
},
});
}
},
},
},
};
}

Multi-Plugin Coordination

Multi-Plugin Coordination
// Plugin 1: Sets up shared data
export function dataProviderPlugin(): FrameMasterPlugin {
return {
name: "data-provider",
router: {
before_request: async (master) => {
const config = await loadConfig();
master.setContext({ sharedConfig: config });
},
},
};
}
// Plugin 2: Uses shared data
export 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 plugin
console.log("Using config:", ctx.sharedConfig);
}
},
},
};
}
// Plugin 3: Coordination via response checking
export function fallbackPlugin(): FrameMasterPlugin {
return {
name: "fallback",
router: {
request: async (master) => {
// Only handle if no other plugin set response
if (!master.isResponseSetted()) {
master.setResponse("Default fallback response");
}
},
},
};
}
async function loadConfig() {
return { apiUrl: "https://api.example.com" };
}

Streaming Responses

Streaming Responses
export function streamingPlugin(): FrameMasterPlugin {
return {
name: "streaming",
router: {
request: async (master) => {
if (master.URL.pathname === "/stream") {
// Create a ReadableStream
const 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

Debugging Plugin
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()
);
},
},
};
}