Skip to content

[Bug] patchGlobalFetch is not idempotent when called with new BudgetManager instances — second patch overrides the first without restoring original fetch #76

Description

@anshul23102

Description

In `src/interceptors/fetchInterceptor.ts`, `patchGlobalFetch` uses a module-level `isPatched` flag to prevent double-patching. However, `setBudgetManager` and `setModelRouter` are separate functions that update module-level variables after the patch:

```typescript
let isPatched = false;
let budgetManager: BudgetManager | null = null;

export function patchGlobalFetch(): void {
if (isPatched) {
return; // subsequent calls are no-ops
}
// patches globalThis.fetch once
isPatched = true;
}

export function setBudgetManager(manager: BudgetManager | null): void {
budgetManager = manager; // replaces previous manager silently
}
```

Impact

When an application calls `budgetGuard` twice (e.g. to update the monthly limit mid-month), the second call replaces `budgetManager` with a fresh instance that has `totalSpent = 0`. Because `isPatched = true`, the interceptor is already in place but now points to the new manager — effectively resetting the budget counter without any warning.

```typescript
budgetGuard({ monthlyLimit: 10, mode: 'block' });
// ... spend $9

budgetGuard({ monthlyLimit: 10, mode: 'block' }); // called again
// totalSpent resets to $0 in the new BudgetManager
// previous $9 spent is lost
```

Additionally, `unpatchGlobalFetch` (if it exists) must be called explicitly or `globalThis.fetch` is permanently patched for the process lifetime with no way to restore the original behavior.

Expected Behavior

Calling `budgetGuard` a second time should either:

  1. Reject with a clear error ("TokenFirewall already initialized"), or
  2. Update the existing `BudgetManager` limit rather than replacing the instance (preserving `totalSpent`), or
  3. Clearly document in the README that re-initialization resets the spent counter.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions