Skip to content

Add rate limiting support #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# FetchClient - Copilot Instructions

## Project Architecture

This is a **Deno-first TypeScript library** that provides a typed HTTP client
with middleware support. The project builds to NPM for Node.js compatibility via
`@deno/dnt`.

### Core Components

- **`FetchClient`** - Main HTTP client class with typed JSON methods (`getJSON`,
`postJSON`, etc.)
- **`FetchClientProvider`** - Singleton pattern for shared configuration,
caching, and rate limiting across multiple client instances
- **Middleware System** - Pipeline architecture using `FetchClientContext` and
`next()` functions
- **Rate Limiting** - Built-in middleware for HTTP 429 responses with
`Retry-After` headers
- **Caching** - Key-based response caching with TTL support
- **Problem Details** - RFC 9457 compliant error handling

### Key Patterns

**Provider Pattern**: Use `FetchClientProvider` for shared state:

```typescript
const provider = new FetchClientProvider();
provider.enableRateLimit({ maxRequests: 100, windowMs: 60000 });
const client = provider.getFetchClient();
```

**Middleware Chain**: Always call `next()`, modify `context.response` after:

```typescript
provider.useMiddleware(async (ctx, next) => {
// pre-processing
await next();
// post-processing with ctx.response
});
```

**Global Helpers**: Default provider instance accessible via `useFetchClient()`,
`getJSON()`, `setBaseUrl()`

## Development Workflow

### Essential Commands

- `deno task test` - Run tests with network access
- `deno task build` - Generate NPM package in `./npm/`
- `deno task check` - Type check all TypeScript files
- `deno lint` and `deno fmt` - Linting and formatting

### Testing Patterns

- Use `FetchClientProvider` with `fakeFetch` for mocking
- Rate limiting tests require `await delay()` for time windows
- Cache tests use `provider.cache.has()` and `provider.cache.delete()` for
assertions
- Middleware tests check `ctx.response` before/after `next()`

### File Organization

- `src/` - Core TypeScript source
- `mod.ts` - Main export file
- `scripts/build.ts` - Deno-to-NPM build configuration
- Tests are co-located (`.test.ts` suffix)

## Critical Implementation Details

**Context Mutation**: Middleware modifies `FetchClientContext` in-place. The
`response` property is null before `next()` and populated after.

**Error Handling**: Uses `expectedStatusCodes` array instead of try/catch for
HTTP errors. `errorCallback` can suppress throwing.

**Cache Keys**: Use array format `["resource", "id"]` for hierarchical cache
invalidation.

**Rate Limiting**: Middleware returns HTTP 429 responses instead of throwing
errors. Check `response.status === 429` for rate limit handling.

**Schema Validation**: Use `meta: { schema: ZodSchema }` option with middleware
for runtime validation.

**Date Parsing**: Enable with `shouldParseDates: true` option or custom
`reviver` function.

## Integration Points

- **Zod**: Runtime schema validation via middleware
- **Problem Details**: Standard error format for HTTP APIs
- **AbortController**: Native timeout and cancellation support
- **Headers**: Link header parsing for pagination

When working on this codebase, always consider the middleware pipeline order and
the provider/client distinction for shared vs. instance-specific behavior.
8 changes: 4 additions & 4 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"test": "deno test --allow-net"
},
"imports": {
"@deno/dnt": "jsr:@deno/dnt@^0.41.3",
"@std/assert": "jsr:@std/assert@^1.0.10",
"@std/path": "jsr:@std/path@^1.0.8",
"zod": "npm:zod@^3.24.1"
"@deno/dnt": "jsr:@deno/dnt@^0.42.1",
"@std/assert": "jsr:@std/assert@^1.0.13",
"@std/path": "jsr:@std/path@^1.1.1",
"zod": "npm:zod@^4.0.5"
},
"exclude": ["npm"]
}
160 changes: 56 additions & 104 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export {
FetchClientProvider,
} from "./src/FetchClientProvider.ts";
export * from "./src/DefaultHelpers.ts";
export { type RateLimitConfig, RateLimiter } from "./src/RateLimiter.ts";
27 changes: 27 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ FetchClient is a library that makes it easier to use the fetch API for JSON APIs
* [Automatic model validation](#model-validator)
* [Caching](#caching)
* [Middleware](#middleware)
* [Rate limiting](#rate-limiting)
* [Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html) support
* Option to parse dates in responses

Expand Down Expand Up @@ -130,6 +131,32 @@ const response = await client.getJSON<Products>(
);
```

### Rate Limiting

```ts
import { FetchClientProvider } from '@exceptionless/fetchclient';

const provider = new FetchClientProvider();

// Enable rate limiting: max 100 requests per hour
provider.enableRateLimit({
maxRequests: 100,
windowMs: 60 * 60 * 1000, // 1 hour
});

const client = provider.getFetchClient();

// Requests exceeding the limit will receive HTTP 429 responses
const response = await client.getJSON('https://api.example.com/data', {
expectedStatusCodes: [200, 429] // Handle 429 without throwing
});

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
}
```

Also, take a look at the tests:

[FetchClient Tests](src/FetchClient.test.ts)
Expand Down
2 changes: 0 additions & 2 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ await build({
typeCheck: false,
test: true,

importMap: "deno.json",

package: {
name: "@exceptionless/fetchclient",
version: Deno.args[0],
Expand Down
9 changes: 9 additions & 0 deletions src/DefaultHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { FetchClientResponse } from "./FetchClientResponse.ts";
import type { ProblemDetails } from "./ProblemDetails.ts";
import type { GetRequestOptions, RequestOptions } from "./RequestOptions.ts";
import type { RateLimitConfig } from "./RateLimiter.ts";

let getCurrentProviderFunc: () => FetchClientProvider | null = () => null;

Expand Down Expand Up @@ -164,3 +165,11 @@ export function useMiddleware(middleware: FetchClientMiddleware) {
export function setRequestOptions(options: RequestOptions) {
getCurrentProvider().applyOptions({ defaultRequestOptions: options });
}

/**
* Enables rate limiting for the current provider.
* @param config - The rate limit configuration.
*/
export function enableRateLimit(config: RateLimitConfig) {
getCurrentProvider().enableRateLimit(config);
}
Loading