Skip to content

Instantly share code, notes, and snippets.

@maanimis
Last active October 30, 2025 09:06
Show Gist options
  • Select an option

  • Save maanimis/e36806517e809ac6b4ca9dfc79e1e041 to your computer and use it in GitHub Desktop.

Select an option

Save maanimis/e36806517e809ac6b4ca9dfc79e1e041 to your computer and use it in GitHub Desktop.
Provides comprehensive hooking capabilities for functions and classes
/**
* Function and Class Hooking Utility
* Provides comprehensive hooking capabilities for functions and classes
*/
// Types for hook callbacks
type BeforeHook<T extends any[] = any[]> = (...args: T) => void | [...T];
type AfterHook<T = any> = (result: T, ...args: any[]) => T | void;
type ErrorHook = (error: Error, ...args: any[]) => void;
interface HookConfig<T extends any[] = any[], R = any> {
before?: BeforeHook<T>;
after?: AfterHook<R>;
onError?: ErrorHook;
onFinally?: () => void;
}
/**
* Hook a single function with before/after/error/finally callbacks
*/
export function hookFunction<T extends (...args: any[]) => any>(
fn: T,
config: HookConfig<Parameters<T>, ReturnType<T>>
): T {
return function (this: any, ...args: Parameters<T>): ReturnType<T> {
let modifiedArgs = args;
// Execute before hook
if (config.before) {
const beforeResult = config.before(...args);
if (beforeResult !== undefined) {
modifiedArgs = beforeResult as Parameters<T>;
}
}
try {
// Execute original function
let result = fn.apply(this, modifiedArgs);
// Execute after hook
if (config.after) {
const afterResult = config.after(result, ...modifiedArgs);
if (afterResult !== undefined) {
result = afterResult;
}
}
return result;
} catch (error) {
// Execute error hook
if (config.onError) {
config.onError(error as Error, ...modifiedArgs);
}
throw error;
} finally {
// Execute finally hook
if (config.onFinally) {
config.onFinally();
}
}
} as T;
}
/**
* Hook all methods of a class instance
*/
export function hookInstance<T extends object>(
instance: T,
config: HookConfig | ((methodName: string) => HookConfig)
): T {
const proto = Object.getPrototypeOf(instance);
const propertyNames = Object.getOwnPropertyNames(proto);
propertyNames.forEach((name) => {
if (name === "constructor") return;
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
if (!descriptor || typeof descriptor.value !== "function") return;
const originalMethod = descriptor.value;
const methodConfig = typeof config === "function" ? config(name) : config;
descriptor.value = hookFunction(originalMethod, methodConfig);
Object.defineProperty(instance, name, descriptor);
});
return instance;
}
/**
* Hook a class constructor and all its methods
*/
export function hookClass<T extends new (...args: any[]) => any>(
TargetClass: T,
config: {
constructor?: HookConfig;
methods?: HookConfig | ((methodName: string) => HookConfig);
}
): T {
// Create a proxy class that wraps the original
const ProxyClass = new Proxy(TargetClass, {
construct(target, args) {
let modifiedArgs = args;
// Hook constructor before
if (config.constructor?.before) {
const beforeResult = config.constructor.before(...args);
if (beforeResult !== undefined) {
modifiedArgs = beforeResult;
}
}
let instance: InstanceType<T>;
try {
// Create instance
instance = new target(...modifiedArgs);
// Hook constructor after
if (config.constructor?.after) {
const afterResult = config.constructor.after(
instance,
...modifiedArgs
);
if (afterResult !== undefined) {
instance = afterResult;
}
}
// Hook all methods if specified
if (config.methods) {
hookInstance(instance, config.methods);
}
return instance;
} catch (error) {
if (config.constructor?.onError) {
config.constructor.onError(error as Error, ...modifiedArgs);
}
throw error;
} finally {
if (config.constructor?.onFinally) {
config.constructor.onFinally();
}
}
},
});
return ProxyClass as T;
}
/**
* Hook specific methods of a class by name
*/
export function hookMethods<T extends new (...args: any[]) => any>(
TargetClass: T,
methodHooks: Record<string, HookConfig>
): T {
const proto = TargetClass.prototype;
Object.entries(methodHooks).forEach(([methodName, hookConfig]) => {
const originalMethod = proto[methodName];
if (typeof originalMethod === "function") {
proto[methodName] = hookFunction(originalMethod, hookConfig);
}
});
return TargetClass;
}
/**
* Create a detachable hook that can be removed later
*/
export function createDetachableHook<T extends (...args: any[]) => any>(
obj: any,
methodName: string,
config: HookConfig<Parameters<T>, ReturnType<T>>
): () => void {
const original: T = obj[methodName];
if (typeof original !== "function") {
throw new Error(`${methodName} is not a function`);
}
obj[methodName] = hookFunction<T>(original, config);
// Return detach function
return () => {
obj[methodName] = original;
};
}
// ============================================
// Usage Examples
// ============================================
// Example 1: Hook a simple function
function greet(name: string): string {
return `Hello, ${name}!`;
}
const hookedGreet = hookFunction(greet, {
before: (name) => {
console.log(`Before: greeting ${name}`);
return [name.toUpperCase()]; // Modify arguments
},
after: (result) => {
console.log(`After: result is "${result}"`);
return result + " Welcome!"; // Modify return value
},
});
// Example 2: Hook a class
class Calculator {
constructor(public name: string) {}
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
const HookedCalculator = hookClass(Calculator, {
constructor: {
before: (name) => {
console.log(`Creating calculator: ${name}`);
},
},
methods: (methodName) => ({
before: (...args) => {
console.log(`Calling ${methodName} with args:`, args);
},
after: (result) => {
console.log(`${methodName} returned:`, result);
return result;
},
}),
});
// Example 3: Hook specific methods
class UserService {
login(username: string, password: string): boolean {
return username === "admin" && password === "secret";
}
logout(): void {
console.log("User logged out");
}
}
const HookedUserService = hookMethods(UserService, {
login: {
before: (username, password) => {
console.log(`Login attempt: ${username}`);
},
after: (success) => {
console.log(`Login ${success ? "successful" : "failed"}`);
},
onError: (error) => {
console.error("Login error:", error);
},
},
});
// Example 4: Detachable hook
const service = new UserService();
const detach = createDetachableHook(service, "login", {
before: () => console.log("Hooked login called"),
});
// Later: detach(); // Removes the hook
export {
greet,
hookedGreet,
Calculator,
HookedCalculator,
UserService,
HookedUserService,
};
@maanimis
Copy link
Author

/**
 * Examples of hooking fetch() and XMLHttpRequest using the hook utility
 */

import { hookFunction, createDetachableHook } from './hook-utility';

// ============================================
// Example 1: Hook the global fetch function
// ============================================

const originalFetch = window.fetch;

window.fetch = hookFunction(originalFetch, {
  before: (input, init) => {
    console.log('🌐 Fetch Request:', {
      url: typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url,
      method: init?.method || 'GET',
      headers: init?.headers,
      body: init?.body,
    });
    
    // You can modify the request here
    // For example, add authentication headers
    const modifiedInit = {
      ...init,
      headers: {
        ...init?.headers,
        'X-Custom-Header': 'hooked',
      },
    };
    
    return [input, modifiedInit];
  },
  after: async (response, input, init) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url;
    
    console.log('βœ… Fetch Response:', {
      url,
      status: response.status,
      statusText: response.statusText,
      headers: Object.fromEntries(response.headers.entries()),
    });
    
    // Clone response to read body without consuming it
    const cloned = response.clone();
    try {
      const body = await cloned.text();
      console.log('πŸ“¦ Response Body:', body.substring(0, 200));
    } catch (e) {
      // Body might not be text
    }
    
    return response;
  },
  onError: (error, input) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url;
    console.error('❌ Fetch Error:', url, error);
  },
});

// ============================================
// Example 2: Hook XMLHttpRequest.prototype.open
// ============================================

const xhrProto = XMLHttpRequest.prototype;

XMLHttpRequest.prototype.open = hookFunction(xhrProto.open, {
  before: function(method, url, async = true, user, password) {
    console.log('πŸ“‘ XHR Open:', { method, url, async });
    
    // Modify URL if needed
    let modifiedUrl = url;
    if (typeof url === 'string' && !url.includes('?')) {
      modifiedUrl = `${url}?hooked=true`;
    }
    
    return [method, modifiedUrl, async, user, password];
  },
  after: function(result, method, url) {
    console.log('βœ“ XHR Opened successfully');
    return result;
  },
});

// ============================================
// Example 3: Hook XMLHttpRequest.prototype.send
// ============================================

XMLHttpRequest.prototype.send = hookFunction(xhrProto.send, {
  before: function(body) {
    console.log('πŸ“€ XHR Send:', {
      body: body ? (typeof body === 'string' ? body.substring(0, 100) : body) : null,
      readyState: this.readyState,
    });
    
    // Modify request body if needed
    if (typeof body === 'string') {
      try {
        const parsed = JSON.parse(body);
        parsed._hooked = true;
        return [JSON.stringify(parsed)];
      } catch (e) {
        // Not JSON, return as is
      }
    }
    
    return [body];
  },
  after: function(result) {
    console.log('βœ“ XHR Sent');
    return result;
  },
  onError: function(error) {
    console.error('❌ XHR Send Error:', error);
  },
});

// ============================================
// Example 4: Hook XHR response handlers
// ============================================

const originalSetRequestHeader = xhrProto.setRequestHeader;
xhrProto.setRequestHeader = hookFunction(originalSetRequestHeader, {
  before: (header, value) => {
    console.log('πŸ“‹ XHR Header:', { header, value });
    return [header, value];
  },
});

// ============================================
// Example 5: Detachable hooks for testing
// ============================================

// Hook fetch with ability to unhook later
const detachFetch = createDetachableHook<typeof window.fetch>(
  window,
  'fetch',
  {
    before: (input, init) => {
      console.log('πŸ”— Detachable Fetch Hook:', input);
      return [input, init];
    },
  }
);

// Later you can remove the hook:
// detachFetch();

// ============================================
// Example 6: Complete XHR lifecycle monitoring
// ============================================

function hookXHRInstance(xhr: XMLHttpRequest) {
  // Hook the load event
  const originalOnLoad = xhr.onload;
  xhr.onload = hookFunction(
    function(this: XMLHttpRequest, ev: ProgressEvent) {
      console.log('πŸ“₯ XHR Load:', {
        status: this.status,
        statusText: this.statusText,
        response: typeof this.response === 'string' 
          ? this.response.substring(0, 100) 
          : this.response,
      });
      
      if (originalOnLoad) {
        return originalOnLoad.call(this, ev);
      }
    },
    {
      after: (result) => {
        console.log('βœ“ XHR onload handler completed');
        return result;
      },
    }
  );

  // Hook error event
  const originalOnError = xhr.onerror;
  xhr.onerror = function(this: XMLHttpRequest, ev: ProgressEvent) {
    console.error('❌ XHR Error Event:', {
      status: this.status,
      statusText: this.statusText,
    });
    
    if (originalOnError) {
      return originalOnError.call(this, ev);
    }
  };

  return xhr;
}

// Override XMLHttpRequest constructor to auto-hook new instances
const OriginalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = class HookedXHR extends OriginalXHR {
  constructor() {
    super();
    hookXHRInstance(this);
  }
} as any;

// ============================================
// Example 7: Request/Response logging middleware
// ============================================

interface HttpLog {
  timestamp: number;
  type: 'fetch' | 'xhr';
  method: string;
  url: string;
  status?: number;
  duration?: number;
  error?: string;
}

const httpLogs: HttpLog[] = [];

// Enhanced fetch hook with logging
const enhancedFetch = window.fetch;
window.fetch = hookFunction(enhancedFetch, {
  before: (input, init) => {
    const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url;
    const log: HttpLog = {
      timestamp: Date.now(),
      type: 'fetch',
      method: init?.method || 'GET',
      url,
    };
    
    // Store log with a marker to update later
    (input as any).__logIndex = httpLogs.length;
    httpLogs.push(log);
    
    return [input, init];
  },
  after: (response, input) => {
    const logIndex = (input as any).__logIndex;
    if (logIndex !== undefined && httpLogs[logIndex]) {
      httpLogs[logIndex].status = response.status;
      httpLogs[logIndex].duration = Date.now() - httpLogs[logIndex].timestamp;
    }
    return response;
  },
  onError: (error, input) => {
    const logIndex = (input as any).__logIndex;
    if (logIndex !== undefined && httpLogs[logIndex]) {
      httpLogs[logIndex].error = error.message;
      httpLogs[logIndex].duration = Date.now() - httpLogs[logIndex].timestamp;
    }
  },
});

// Function to view all HTTP logs
export function getHttpLogs(): HttpLog[] {
  return httpLogs;
}

export function clearHttpLogs(): void {
  httpLogs.length = 0;
}

// ============================================
// Usage Examples
// ============================================

// Test fetch hook
async function testFetchHook() {
  try {
    const response = await fetch('https://api.github.com/users/github');
    const data = await response.json();
    console.log('GitHub API Response:', data);
  } catch (error) {
    console.error('Fetch failed:', error);
  }
}

// Test XHR hook
function testXHRHook() {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', 'https://api.github.com/users/github');
  xhr.onload = function() {
    console.log('XHR Response:', this.responseText);
  };
  xhr.send();
}

// Uncomment to test:
// testFetchHook();
// testXHRHook();

export { detachFetch };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment